Compare commits

...

24 Commits

Author SHA1 Message Date
GitHub Actions
8f4a8a2af4 New Crowdin translations 2025-12-12 04:47:00 +00:00
Claire
88c0f52e99 Merge pull request #3313 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to d730f6b0c5
2025-12-11 19:23:18 +01:00
Claire
a56b739c68 [Glitch] Fix wrapstodon modal closing on any click
Port dfbf908870 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-11 19:07:26 +01:00
diondiondion
183a42a5ee [Glitch] Add Wrapstodon footer links
Port c06eb371e6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-11 19:06:54 +01:00
Claire
303a5478af Merge commit 'dfbf908870fcde76396ebccfb3d71ee1a06ffe82' into glitch-soc/merge-upstream
Conflicts:
- `app/views/wrapstodon/show.html.haml`:
  Conflict because of glitch-soc's theming system.
  Applied upstream's changes.
2025-12-11 19:05:32 +01:00
Claire
dfbf908870 Fix wrapstodon modal closing on any click (#37209) 2025-12-11 17:49:26 +00:00
diondiondion
aa067370d8 [Glitch] Fix Wrapstodon modal scrolling not working on iOS
Port 4323963053 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-11 18:07:59 +01:00
diondiondion
5e0db46b2a [Glitch] Wrapstodon design QA tweaks
Port 5651900b89 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-11 18:07:34 +01:00
diondiondion
c06eb371e6 Add Wrapstodon footer links (#37207) 2025-12-11 17:06:26 +00:00
Claire
53617cef5a Merge commit 'd730f6b0c5cfb18894d1a9e34d0aa2556dda3c62' into glitch-soc/merge-upstream 2025-12-11 18:05:28 +01:00
Emelia Smith
d730f6b0c5 Add spec for client_credentials being used with /api/v1/apps/verify_credentials (#37195) 2025-12-11 16:40:22 +00:00
Claire
addeb28292 Change wrapstodon 2025 to allow unlisted posts in top statuses (#37206) 2025-12-11 16:35:35 +00:00
Claire
5e3387539e Add image to Wrapstodon OpenGraph banner (#37205) 2025-12-11 16:22:48 +00:00
diondiondion
4323963053 Fix Wrapstodon modal scrolling not working on iOS (#37203) 2025-12-11 14:25:28 +00:00
diondiondion
5651900b89 Wrapstodon design QA tweaks (#37201) 2025-12-11 11:40:53 +00:00
renovate[bot]
d1b996b7e3 Update dependency omniauth-rails_csrf_protection to v2.0.1 (#37199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 11:12:07 +00:00
renovate[bot]
fed26a41fa Update dependency jsdom to v27.3.0 (#37165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:33:47 +00:00
Claire
37d309bcaf Fix Wrapstodon font loading by disabling inlining of fonts in Vite (#37198) 2025-12-11 10:33:15 +00:00
renovate[bot]
d25f672c50 Update dependency active_model_serializers to v0.10.16 (#37167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:07:52 +00:00
renovate[bot]
15c9088761 Update dependency vite to v7.2.7 (#37156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:07:38 +00:00
renovate[bot]
da1505a495 Update dependency @vitejs/plugin-react to v5.1.2 (#37155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:07:30 +00:00
renovate[bot]
d1f690f50c Update dependency stoplight to v5.7.0 (#37151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 10:07:25 +00:00
Claire
da2b75bdcd Change build-releases workflow to tag images latest based on latest stable-x.y branch (#37179)
Co-authored-by: emilweth <7402764+emilweth@users.noreply.github.com>
2025-12-10 17:01:25 +00:00
David Roetzel
adf8a3601d Add service to add item to a collection (#37192) 2025-12-10 16:59:21 +00:00
52 changed files with 893 additions and 180 deletions

View File

@@ -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}}

View File

@@ -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)

View File

@@ -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>
);
};

View File

@@ -4,10 +4,14 @@
@typescript-eslint/no-unsafe-member-access,
@typescript-eslint/no-unsafe-call */
import type { ComponentPropsWithoutRef } from 'react';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { InterceptStatusClicks } from 'flavours/glitch/components/status/intercept_status_clicks';
import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted';
import type { TopStatuses } from 'flavours/glitch/models/annual_report';
import { makeGetStatus } from 'flavours/glitch/selectors';
@@ -29,6 +33,24 @@ export const HighlightedPost: React.FC<{
statusId ? getStatus(state, { id: statusId }) : undefined,
);
const handleClick = useCallback<
ComponentPropsWithoutRef<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)} />;
}
@@ -72,7 +94,9 @@ export const HighlightedPost: React.FC<{
{context === 'modal' && <p>{label}</p>}
</div>
<InterceptStatusClicks onPreventedClick={handleClick}>
<StatusQuoteManager showActions={false} id={statusId} />
</InterceptStatusClicks>
</div>
);
};

View File

@@ -10,18 +10,17 @@ $mobile-breakpoint: 540px;
}
.modalWrapper {
position: absolute;
inset: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 40px;
overflow-y: auto;
pointer-events: none;
scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary);
@media (width < $mobile-breakpoint) {
padding-inline: 10px;
padding: 0;
}
.loading-indicator .circular-progress {
@@ -50,37 +49,51 @@ $mobile-breakpoint: 540px;
}
.wrapper {
--gradient-strength: 0.4;
box-sizing: border-box;
position: relative;
max-width: 600px;
padding: 24px;
padding-top: 40px;
contain: layout;
flex: 0 0 auto;
pointer-events: auto;
pointer-events: all;
color: var(--color-text-primary);
background: var(--color-bg-primary);
background:
radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%),
radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%),
radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%),
radial-gradient(at 16% 95%, #1e948299 0, transparent 50%),
radial-gradient(at 80% 91%, #16dae499 0, transparent 50%)
radial-gradient(
at 10% 27%,
rgba(83, 12, 154, var(--gradient-strength)) 0,
transparent 50%
),
radial-gradient(
at 91% 10%,
rgba(30, 24, 223, var(--gradient-strength)) 0,
transparent 25%
),
radial-gradient(
at 10% 91%,
rgba(22, 218, 228, var(--gradient-strength)) 0,
transparent 40%
),
radial-gradient(
at 75% 87%,
rgba(37, 31, 217, var(--gradient-strength)) 0,
transparent 20%
),
radial-gradient(
at 84% 60%,
rgba(95, 30, 148, var(--gradient-strength)) 0,
transparent 40%
)
var(--color-bg-primary);
border-radius: 40px;
@media (width < $mobile-breakpoint) {
padding-inline: 12px;
padding-bottom: 12px;
border-radius: 28px;
}
&::after {
content: '';
position: absolute;
inset: 0;
z-index: -1;
background: inherit;
border-radius: inherit;
filter: blur(20px);
border-radius: 0;
}
}
@@ -92,7 +105,7 @@ $mobile-breakpoint: 540px;
font-family: silkscreen-wrapstodon, monospace;
font-size: 28px;
line-height: 1;
margin-bottom: 8px;
margin-bottom: 4px;
padding-inline: 40px; // Prevent overlap with close button
@media (width < $mobile-breakpoint) {
@@ -116,7 +129,7 @@ $mobile-breakpoint: 540px;
.box {
position: relative;
padding: 16px;
padding: 24px;
border-radius: 16px;
background: rgb(from var(--color-bg-primary) r g b / 60%);
box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%);
@@ -150,7 +163,6 @@ $mobile-breakpoint: 540px;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px;
font-size: 14px;
text-align: center;
text-wrap: balance;
@@ -164,6 +176,10 @@ $mobile-breakpoint: 540px;
text-transform: uppercase;
color: #c2c8ff;
font-weight: 500;
&:last-child {
margin-bottom: -3px;
}
}
.statLarge {
@@ -185,7 +201,7 @@ $mobile-breakpoint: 540px;
.mostBoostedPost {
padding: 0;
padding-top: 8px;
padding-top: 24px;
overflow: hidden;
}
@@ -260,7 +276,7 @@ $mobile-breakpoint: 540px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
gap: 16px;
p {
max-width: 460px;

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { defineMessage, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import { useLocation } from 'react-router';
@@ -23,11 +23,6 @@ import { NewPosts } from './new_posts';
const moduleClassNames = classNames.bind(styles);
export const shareMessage = defineMessage({
id: 'annual_report.summary.share_message',
defaultMessage: 'I got the {archetype} archetype!',
});
export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
context = 'standalone',
}) => {

View File

@@ -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>

View File

@@ -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}
/>
);
};

View File

@@ -1,20 +0,0 @@
.wrapper {
max-width: max-content;
margin-inline: auto;
padding: 40px 10px;
}
.footer {
text-align: center;
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: center;
color: var(--color-text-secondary);
}
.logo {
width: 2rem;
opacity: 0.6;
}

View File

@@ -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);
}
}

View File

@@ -3,9 +3,10 @@ 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 (
@@ -18,6 +19,28 @@ export const WrapstodonSharedPage: FC = () => {
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>
);

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -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>
);
};

View File

@@ -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';
@@ -29,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)} />;
}
@@ -72,7 +94,9 @@ export const HighlightedPost: React.FC<{
{context === 'modal' && <p>{label}</p>}
</div>
<InterceptStatusClicks onPreventedClick={handleClick}>
<StatusQuoteManager showActions={false} id={statusId} />
</InterceptStatusClicks>
</div>
);
};

View File

@@ -10,18 +10,17 @@ $mobile-breakpoint: 540px;
}
.modalWrapper {
position: absolute;
inset: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 40px;
overflow-y: auto;
pointer-events: none;
scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary);
@media (width < $mobile-breakpoint) {
padding-inline: 10px;
padding: 0;
}
.loading-indicator .circular-progress {
@@ -50,37 +49,51 @@ $mobile-breakpoint: 540px;
}
.wrapper {
--gradient-strength: 0.4;
box-sizing: border-box;
position: relative;
max-width: 600px;
padding: 24px;
padding-top: 40px;
contain: layout;
flex: 0 0 auto;
pointer-events: auto;
pointer-events: all;
color: var(--color-text-primary);
background: var(--color-bg-primary);
background:
radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%),
radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%),
radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%),
radial-gradient(at 16% 95%, #1e948299 0, transparent 50%),
radial-gradient(at 80% 91%, #16dae499 0, transparent 50%)
radial-gradient(
at 10% 27%,
rgba(83, 12, 154, var(--gradient-strength)) 0,
transparent 50%
),
radial-gradient(
at 91% 10%,
rgba(30, 24, 223, var(--gradient-strength)) 0,
transparent 25%
),
radial-gradient(
at 10% 91%,
rgba(22, 218, 228, var(--gradient-strength)) 0,
transparent 40%
),
radial-gradient(
at 75% 87%,
rgba(37, 31, 217, var(--gradient-strength)) 0,
transparent 20%
),
radial-gradient(
at 84% 60%,
rgba(95, 30, 148, var(--gradient-strength)) 0,
transparent 40%
)
var(--color-bg-primary);
border-radius: 40px;
@media (width < $mobile-breakpoint) {
padding-inline: 12px;
padding-bottom: 12px;
border-radius: 28px;
}
&::after {
content: '';
position: absolute;
inset: 0;
z-index: -1;
background: inherit;
border-radius: inherit;
filter: blur(20px);
border-radius: 0;
}
}
@@ -92,7 +105,7 @@ $mobile-breakpoint: 540px;
font-family: silkscreen-wrapstodon, monospace;
font-size: 28px;
line-height: 1;
margin-bottom: 8px;
margin-bottom: 4px;
padding-inline: 40px; // Prevent overlap with close button
@media (width < $mobile-breakpoint) {
@@ -116,7 +129,7 @@ $mobile-breakpoint: 540px;
.box {
position: relative;
padding: 16px;
padding: 24px;
border-radius: 16px;
background: rgb(from var(--color-bg-primary) r g b / 60%);
box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%);
@@ -150,7 +163,6 @@ $mobile-breakpoint: 540px;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px;
font-size: 14px;
text-align: center;
text-wrap: balance;
@@ -164,6 +176,10 @@ $mobile-breakpoint: 540px;
text-transform: uppercase;
color: #c2c8ff;
font-weight: 500;
&:last-child {
margin-bottom: -3px;
}
}
.statLarge {
@@ -185,7 +201,7 @@ $mobile-breakpoint: 540px;
.mostBoostedPost {
padding: 0;
padding-top: 8px;
padding-top: 24px;
overflow: hidden;
}
@@ -260,7 +276,7 @@ $mobile-breakpoint: 540px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
gap: 16px;
p {
max-width: 460px;

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { defineMessage, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import { useLocation } from 'react-router';
@@ -23,11 +23,6 @@ import { NewPosts } from './new_posts';
const moduleClassNames = classNames.bind(styles);
export const shareMessage = defineMessage({
id: 'annual_report.summary.share_message',
defaultMessage: 'I got the {archetype} archetype!',
});
export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
context = 'standalone',
}) => {

View File

@@ -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>

View File

@@ -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}
/>
);
};

View File

@@ -1,20 +0,0 @@
.wrapper {
max-width: max-content;
margin-inline: auto;
padding: 40px 10px;
}
.footer {
text-align: center;
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: center;
color: var(--color-text-secondary);
}
.logo {
width: 2rem;
opacity: 0.6;
}

View File

@@ -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);
}
}

View File

@@ -3,9 +3,10 @@ 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 (
@@ -18,6 +19,28 @@ export const WrapstodonSharedPage: FC = () => {
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>
);

View File

@@ -117,7 +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",
@@ -152,6 +154,7 @@
"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.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.",

View File

@@ -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

View File

@@ -451,6 +451,10 @@ class Account < ApplicationRecord
save!
end
def featureable?
local? && discoverable?
end
private
def prepare_contents

View File

@@ -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

View File

@@ -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

View 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

View 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'

View File

@@ -9,7 +9,9 @@
= 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'

View File

@@ -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: Χρησιμοποιήστε αυτό

View File

@@ -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

View File

@@ -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

View File

@@ -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: Προεπιλεγμένη μορφή για τουτς

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) { {} }

View 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

View File

@@ -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: {

View File

@@ -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:
@@ -3341,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
@@ -4830,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
@@ -6253,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
@@ -8959,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"
@@ -8987,7 +8987,7 @@ __metadata:
peerDependenciesMeta:
canvas:
optional: true
checksum: 10c0/52d847e1aef099071d66d1d9aedcdd2f15e7ea781da9cfb41dc0d4caf741c5870c346396f8d1182d611427ae47a53f69a6f16410c698950e5809d3fed5a1672d
checksum: 10c0/b022ed8f6ce175afd97fbd42eb65b03b2be3b23df86cf87f018b6d2e757682fe8348e719a14780d6fa3fe8a65e531ba71b38db80f312818a32b77f01e31f267e
languageName: node
linkType: hard
@@ -9394,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
@@ -14077,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"
@@ -14127,7 +14127,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 10c0/d444a159ab8f0f854d596d1938f201b449d59ed4d336e587be9dc89005467214d85848c212c2495f76a8421372ffe4d061d023d659600f1aaa3ba5ac13e804f7
checksum: 10c0/0c502d9eb898d9c05061dbd8fd199f280b524bbb4c12ab5f88c7b12779947386684a269e4dd0aa424aa35bcd857f1aa44aadb9ea764702a5043af433052455b5
languageName: node
linkType: hard