Merge commit '37ccffa95a30772b55e3f18d486d699ee6c5f9e8' into glitch-soc/merge-upstream
This commit is contained in:
commit
99ff59b7b2
49
.github/workflows/chromatic.yml
vendored
49
.github/workflows/chromatic.yml
vendored
@ -1,31 +1,51 @@
|
|||||||
name: 'Chromatic'
|
name: 'Chromatic'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- renovate/*
|
- renovate/*
|
||||||
- stable-*
|
- stable-*
|
||||||
paths:
|
|
||||||
- 'package.json'
|
|
||||||
- 'yarn.lock'
|
|
||||||
- '**/*.js'
|
|
||||||
- '**/*.jsx'
|
|
||||||
- '**/*.ts'
|
|
||||||
- '**/*.tsx'
|
|
||||||
- '**/*.css'
|
|
||||||
- '**/*.scss'
|
|
||||||
- '.github/workflows/chromatic.yml'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
chromatic:
|
pathcheck:
|
||||||
name: Run Chromatic
|
name: Check for relevant changes
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'mastodon/mastodon'
|
outputs:
|
||||||
|
changed: ${{ steps.filter.outputs.src }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
src:
|
||||||
|
- 'package.json'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- '**/*.js'
|
||||||
|
- '**/*.jsx'
|
||||||
|
- '**/*.ts'
|
||||||
|
- '**/*.tsx'
|
||||||
|
- '**/*.css'
|
||||||
|
- '**/*.scss'
|
||||||
|
- '.github/workflows/chromatic.yml'
|
||||||
|
|
||||||
|
chromatic:
|
||||||
|
name: Run Chromatic
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: pathcheck
|
||||||
|
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|
||||||
@ -35,7 +55,8 @@ jobs:
|
|||||||
- name: Run Chromatic
|
- name: Run Chromatic
|
||||||
uses: chromaui/action@v13
|
uses: chromaui/action@v13
|
||||||
with:
|
with:
|
||||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
zip: true
|
zip: true
|
||||||
storybookBuildDir: 'storybook-static'
|
storybookBuildDir: 'storybook-static'
|
||||||
|
exitZeroOnChanges: false # Fail workflow if changes are found
|
||||||
|
autoAcceptChanges: 'main' # Auto-accept changes on main branch only
|
||||||
|
|||||||
@ -538,7 +538,7 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [Drew Schuster](mailto:dtschust@gmail.com)
|
* [Drew Schuster](mailto:dtschust@gmail.com)
|
||||||
* [Dryusdan](mailto:dryusdan@dryusdan.fr)
|
* [Dryusdan](mailto:dryusdan@dryusdan.fr)
|
||||||
* [Eai](mailto:eai@mizle.net)
|
* [Eai](mailto:eai@mizle.net)
|
||||||
* [Eashwar Ranganathan](mailto:eranganathan@lyft.com)
|
* [Eashwar Ranganathan](mailto:eashwar@eashwar.com)
|
||||||
* [Ed Knutson](mailto:knutsoned@gmail.com)
|
* [Ed Knutson](mailto:knutsoned@gmail.com)
|
||||||
* [Elizabeth Martín Campos](mailto:me@elizabeth.sh)
|
* [Elizabeth Martín Campos](mailto:me@elizabeth.sh)
|
||||||
* [Elizabeth Myers](mailto:elizabeth@interlinked.me)
|
* [Elizabeth Myers](mailto:elizabeth@interlinked.me)
|
||||||
|
|||||||
29
app/controllers/api/v1_alpha/collections_controller.rb
Normal file
29
app/controllers/api/v1_alpha/collections_controller.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||||
|
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||||
|
render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
before_action :check_feature_enabled
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create]
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def create
|
||||||
|
@collection = CreateCollectionService.new.call(collection_params, current_user.account)
|
||||||
|
|
||||||
|
render json: @collection, serializer: REST::CollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def collection_params
|
||||||
|
params.permit(:name, :description, :sensitive, :discoverable, :tag, account_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_feature_enabled
|
||||||
|
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1,9 +1,5 @@
|
|||||||
import Rails from '@rails/ujs';
|
import { setupLinkListeners } from './utils/links';
|
||||||
|
|
||||||
export function start() {
|
export function start() {
|
||||||
try {
|
setupLinkListeners();
|
||||||
Rails.start();
|
|
||||||
} catch {
|
|
||||||
// If called twice
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ const renderHashtags = (hashtags: HashtagType[]) =>
|
|||||||
|
|
||||||
const renderStatuses = (statusIds: string[]) =>
|
const renderStatuses = (statusIds: string[]) =>
|
||||||
hidePeek<string>(statusIds).map((id) => (
|
hidePeek<string>(statusIds).map((id) => (
|
||||||
<StatusQuoteManager key={id} id={id} />
|
<StatusQuoteManager contextType='search' key={id} id={id} />
|
||||||
));
|
));
|
||||||
|
|
||||||
type SearchType = 'all' | ApiSearchType;
|
type SearchType = 'all' | ApiSearchType;
|
||||||
@ -189,7 +189,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
onClickMore={handleSelectStatuses}
|
onClickMore={handleSelectStatuses}
|
||||||
>
|
>
|
||||||
{results.statuses.slice(0, INITIAL_DISPLAY).map((id) => (
|
{results.statuses.slice(0, INITIAL_DISPLAY).map((id) => (
|
||||||
<StatusQuoteManager key={id} id={id} />
|
<StatusQuoteManager contextType='search' key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import punycode from 'node:punycode';
|
|
||||||
|
|
||||||
import { useCallback, useId, useState } from 'react';
|
import { useCallback, useId, useState } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import punycode from 'punycode/';
|
||||||
|
|
||||||
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
|
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
|
||||||
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
||||||
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { initialState } from '@/mastodon/initial_state';
|
|
||||||
|
|
||||||
interface FocusColumnOptions {
|
interface FocusColumnOptions {
|
||||||
index?: number;
|
index?: number;
|
||||||
focusItem?: 'first' | 'first-visible';
|
focusItem?: 'first' | 'first-visible';
|
||||||
@ -14,7 +12,10 @@ export function focusColumn({
|
|||||||
focusItem = 'first',
|
focusItem = 'first',
|
||||||
}: FocusColumnOptions = {}) {
|
}: FocusColumnOptions = {}) {
|
||||||
// Skip the leftmost drawer in multi-column mode
|
// Skip the leftmost drawer in multi-column mode
|
||||||
const indexOffset = initialState?.meta.advanced_layout ? 1 : 0;
|
const isMultiColumnLayout = !!document.querySelector(
|
||||||
|
'body.layout-multiple-columns',
|
||||||
|
);
|
||||||
|
const indexOffset = isMultiColumnLayout ? 1 : 0;
|
||||||
|
|
||||||
const column = document.querySelector(
|
const column = document.querySelector(
|
||||||
`.column:nth-child(${index + indexOffset})`,
|
`.column:nth-child(${index + indexOffset})`,
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
"account.disable_notifications": "@{name}さんの投稿時の通知を停止",
|
"account.disable_notifications": "@{name}さんの投稿時の通知を停止",
|
||||||
"account.domain_blocking": "ブロックしているドメイン",
|
"account.domain_blocking": "ブロックしているドメイン",
|
||||||
"account.edit_profile": "プロフィール編集",
|
"account.edit_profile": "プロフィール編集",
|
||||||
|
"account.edit_profile_short": "編集",
|
||||||
"account.enable_notifications": "@{name}さんの投稿時に通知",
|
"account.enable_notifications": "@{name}さんの投稿時に通知",
|
||||||
"account.endorse": "プロフィールで紹介する",
|
"account.endorse": "プロフィールで紹介する",
|
||||||
"account.familiar_followers_many": "{name1}、{name2}、他{othersCount, plural, one {one other you know} other {# others you know}}人のユーザーにフォローされています",
|
"account.familiar_followers_many": "{name1}、{name2}、他{othersCount, plural, one {one other you know} other {# others you know}}人のユーザーにフォローされています",
|
||||||
@ -172,6 +173,8 @@
|
|||||||
"column.edit_list": "リストを編集",
|
"column.edit_list": "リストを編集",
|
||||||
"column.favourites": "お気に入り",
|
"column.favourites": "お気に入り",
|
||||||
"column.firehose": "リアルタイムフィード",
|
"column.firehose": "リアルタイムフィード",
|
||||||
|
"column.firehose_local": "このサーバーのリアルタイムフィード",
|
||||||
|
"column.firehose_singular": "リアルタイムフィード",
|
||||||
"column.follow_requests": "フォローリクエスト",
|
"column.follow_requests": "フォローリクエスト",
|
||||||
"column.home": "ホーム",
|
"column.home": "ホーム",
|
||||||
"column.list_members": "リストのメンバーを管理",
|
"column.list_members": "リストのメンバーを管理",
|
||||||
@ -251,6 +254,7 @@
|
|||||||
"confirmations.remove_from_followers.message": "{name}さんはあなたをフォローしなくなります。本当によろしいですか?",
|
"confirmations.remove_from_followers.message": "{name}さんはあなたをフォローしなくなります。本当によろしいですか?",
|
||||||
"confirmations.remove_from_followers.title": "フォロワーを削除しますか?",
|
"confirmations.remove_from_followers.title": "フォロワーを削除しますか?",
|
||||||
"confirmations.revoke_quote.confirm": "投稿を削除",
|
"confirmations.revoke_quote.confirm": "投稿を削除",
|
||||||
|
"confirmations.revoke_quote.message": "この操作は元に戻せません。",
|
||||||
"confirmations.revoke_quote.title": "投稿を削除しますか?",
|
"confirmations.revoke_quote.title": "投稿を削除しますか?",
|
||||||
"confirmations.unblock.confirm": "ブロック解除",
|
"confirmations.unblock.confirm": "ブロック解除",
|
||||||
"confirmations.unblock.title": "@{name}さんのブロックを解除しますか?",
|
"confirmations.unblock.title": "@{name}さんのブロックを解除しますか?",
|
||||||
@ -477,6 +481,7 @@
|
|||||||
"keyboard_shortcuts.home": "ホームタイムラインを開く",
|
"keyboard_shortcuts.home": "ホームタイムラインを開く",
|
||||||
"keyboard_shortcuts.hotkey": "ホットキー",
|
"keyboard_shortcuts.hotkey": "ホットキー",
|
||||||
"keyboard_shortcuts.legend": "この一覧を表示",
|
"keyboard_shortcuts.legend": "この一覧を表示",
|
||||||
|
"keyboard_shortcuts.load_more": "「もっと見る」ボタンに移動",
|
||||||
"keyboard_shortcuts.local": "ローカルタイムラインを開く",
|
"keyboard_shortcuts.local": "ローカルタイムラインを開く",
|
||||||
"keyboard_shortcuts.mention": "メンション",
|
"keyboard_shortcuts.mention": "メンション",
|
||||||
"keyboard_shortcuts.muted": "ミュートしたユーザーのリストを開く",
|
"keyboard_shortcuts.muted": "ミュートしたユーザーのリストを開く",
|
||||||
@ -497,6 +502,7 @@
|
|||||||
"keyboard_shortcuts.translate": "投稿を翻訳する",
|
"keyboard_shortcuts.translate": "投稿を翻訳する",
|
||||||
"keyboard_shortcuts.unfocus": "投稿の入力欄・検索欄から離れる",
|
"keyboard_shortcuts.unfocus": "投稿の入力欄・検索欄から離れる",
|
||||||
"keyboard_shortcuts.up": "カラム内一つ上に移動",
|
"keyboard_shortcuts.up": "カラム内一つ上に移動",
|
||||||
|
"learn_more_link.got_it": "了解",
|
||||||
"learn_more_link.learn_more": "もっと見る",
|
"learn_more_link.learn_more": "もっと見る",
|
||||||
"lightbox.close": "閉じる",
|
"lightbox.close": "閉じる",
|
||||||
"lightbox.next": "次",
|
"lightbox.next": "次",
|
||||||
@ -611,6 +617,7 @@
|
|||||||
"notification.moderation_warning.action_suspend": "あなたのアカウントは停止されました。",
|
"notification.moderation_warning.action_suspend": "あなたのアカウントは停止されました。",
|
||||||
"notification.own_poll": "アンケートが終了しました",
|
"notification.own_poll": "アンケートが終了しました",
|
||||||
"notification.poll": "投票したアンケートが終了しました",
|
"notification.poll": "投票したアンケートが終了しました",
|
||||||
|
"notification.quoted_update": "あなたが引用した投稿を {name} が編集しました",
|
||||||
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
||||||
"notification.reblog.name_and_others_with_link": "{name}さんと<a>ほか{count, plural, other {#人}}</a>がブーストしました",
|
"notification.reblog.name_and_others_with_link": "{name}さんと<a>ほか{count, plural, other {#人}}</a>がブーストしました",
|
||||||
"notification.relationships_severance_event": "{name} との関係が失われました",
|
"notification.relationships_severance_event": "{name} との関係が失われました",
|
||||||
@ -753,6 +760,7 @@
|
|||||||
"relative_time.minutes": "{number}分前",
|
"relative_time.minutes": "{number}分前",
|
||||||
"relative_time.seconds": "{number}秒前",
|
"relative_time.seconds": "{number}秒前",
|
||||||
"relative_time.today": "今日",
|
"relative_time.today": "今日",
|
||||||
|
"remove_quote_hint.button_label": "了解",
|
||||||
"reply_indicator.attachments": "{count, plural, other {#件のメディア}}",
|
"reply_indicator.attachments": "{count, plural, other {#件のメディア}}",
|
||||||
"reply_indicator.cancel": "キャンセル",
|
"reply_indicator.cancel": "キャンセル",
|
||||||
"reply_indicator.poll": "アンケート",
|
"reply_indicator.poll": "アンケート",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const getStatusInputSelectors = [
|
|||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||||
getFilters,
|
getFilters,
|
||||||
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
|
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites', 'search'].includes(contextType),
|
||||||
];
|
];
|
||||||
|
|
||||||
function getStatusResultFunction(
|
function getStatusResultFunction(
|
||||||
|
|||||||
88
app/javascript/mastodon/utils/links.ts
Normal file
88
app/javascript/mastodon/utils/links.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { on } from 'delegated-events';
|
||||||
|
|
||||||
|
export function setupLinkListeners() {
|
||||||
|
on('click', 'a[data-confirm]', handleConfirmLink);
|
||||||
|
|
||||||
|
// We don't want to target links with data-confirm here, as those are handled already.
|
||||||
|
on('click', 'a[data-method]:not([data-confirm])', handleMethodLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirmLink(event: MouseEvent) {
|
||||||
|
const anchor = event.currentTarget;
|
||||||
|
if (!(anchor instanceof HTMLAnchorElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = anchor.dataset.confirm;
|
||||||
|
if (!message || !window.confirm(message)) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.dataset.method) {
|
||||||
|
handleMethodLink(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMethodLink(event: MouseEvent) {
|
||||||
|
const anchor = event.currentTarget;
|
||||||
|
if (!(anchor instanceof HTMLAnchorElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = anchor.dataset.method?.toLowerCase();
|
||||||
|
if (!method) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Create and submit a form with the specified method.
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.method = 'post';
|
||||||
|
form.action = anchor.href;
|
||||||
|
|
||||||
|
// Add the hidden _method input to simulate other HTTP methods.
|
||||||
|
const methodInput = document.createElement('input');
|
||||||
|
methodInput.type = 'hidden';
|
||||||
|
methodInput.name = '_method';
|
||||||
|
methodInput.value = method;
|
||||||
|
form.appendChild(methodInput);
|
||||||
|
|
||||||
|
// Add CSRF token if available for same-origin requests.
|
||||||
|
const csrf = getCSRFToken();
|
||||||
|
if (csrf && !isCrossDomain(anchor.href)) {
|
||||||
|
const csrfInput = document.createElement('input');
|
||||||
|
csrfInput.type = 'hidden';
|
||||||
|
csrfInput.name = csrf.param;
|
||||||
|
csrfInput.value = csrf.token;
|
||||||
|
form.appendChild(csrfInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The form needs to be in the document to be submitted.
|
||||||
|
form.style.display = 'none';
|
||||||
|
document.body.appendChild(form);
|
||||||
|
|
||||||
|
// We use requestSubmit to ensure any form submit handlers are properly invoked.
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCSRFToken() {
|
||||||
|
const param = document.querySelector<HTMLMetaElement>(
|
||||||
|
'meta[name="csrf-param"]',
|
||||||
|
);
|
||||||
|
const token = document.querySelector<HTMLMetaElement>(
|
||||||
|
'meta[name="csrf-token"]',
|
||||||
|
);
|
||||||
|
if (param && token) {
|
||||||
|
return { param: param.content, token: token.content };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCrossDomain(href: string) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = href;
|
||||||
|
return (
|
||||||
|
link.protocol !== window.location.protocol ||
|
||||||
|
link.host !== window.location.host
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6204,12 +6204,14 @@ a.status-card {
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around; // If set to center, the fullscreen image overlay is misaligned.
|
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7373,6 +7375,13 @@ a.status-card {
|
|||||||
grid-template-rows: 1fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|
||||||
|
&--layout-1 {
|
||||||
|
// The size of single images is determined by their
|
||||||
|
// aspect-ratio, applied via inline style attribute
|
||||||
|
width: initial;
|
||||||
|
max-height: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
&--layout-2 {
|
&--layout-2 {
|
||||||
& > .media-gallery__item:nth-child(1) {
|
& > .media-gallery__item:nth-child(1) {
|
||||||
border-end-end-radius: 0;
|
border-end-end-radius: 0;
|
||||||
@ -7698,14 +7707,11 @@ a.status-card {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $base-shadow-color;
|
background: $base-shadow-color;
|
||||||
max-width: 100%;
|
max-height: 460px;
|
||||||
max-height: max(400px, 60vh);
|
|
||||||
margin-inline: auto;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: $white;
|
color: $white;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
outline: 1px solid var(--media-outline-color);
|
outline: 1px solid var(--media-outline-color);
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@ -113,8 +113,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.current {
|
.current {
|
||||||
color: var(--color-bg-inverted);
|
color: var(--color-bg-primary);
|
||||||
background: var(--color-text-on-inverted);
|
background: var(--color-text-primary);
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
|
|||||||
@ -6085,12 +6085,14 @@ a.status-card {
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around; // If set to center, the fullscreen image overlay is misaligned.
|
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7159,6 +7161,13 @@ a.status-card {
|
|||||||
grid-template-rows: 1fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|
||||||
|
&--layout-1 {
|
||||||
|
// The size of single images is determined by their
|
||||||
|
// aspect-ratio, applied via inline style attribute
|
||||||
|
width: initial;
|
||||||
|
max-height: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
&--layout-2 {
|
&--layout-2 {
|
||||||
& > .media-gallery__item:nth-child(1) {
|
& > .media-gallery__item:nth-child(1) {
|
||||||
border-end-end-radius: 0;
|
border-end-end-radius: 0;
|
||||||
@ -7491,13 +7500,10 @@ a.status-card {
|
|||||||
position: relative;
|
position: relative;
|
||||||
color: var(--color-text-on-media);
|
color: var(--color-text-on-media);
|
||||||
background: var(--color-bg-media);
|
background: var(--color-bg-media);
|
||||||
max-width: 100%;
|
max-height: 460px;
|
||||||
max-height: max(400px, 60vh);
|
|
||||||
margin-inline: auto;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
outline: 1px solid var(--color-border-media);
|
outline: 1px solid var(--color-border-media);
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@ -8,14 +8,11 @@ class AnnualReport
|
|||||||
AnnualReport::TypeDistribution,
|
AnnualReport::TypeDistribution,
|
||||||
AnnualReport::TopStatuses,
|
AnnualReport::TopStatuses,
|
||||||
AnnualReport::MostUsedApps,
|
AnnualReport::MostUsedApps,
|
||||||
AnnualReport::CommonlyInteractedWithAccounts,
|
|
||||||
AnnualReport::TimeSeries,
|
AnnualReport::TimeSeries,
|
||||||
AnnualReport::TopHashtags,
|
AnnualReport::TopHashtags,
|
||||||
AnnualReport::MostRebloggedAccounts,
|
|
||||||
AnnualReport::Percentiles,
|
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
SCHEMA = 1
|
SCHEMA = 2
|
||||||
|
|
||||||
def self.table_name_prefix
|
def self.table_name_prefix
|
||||||
'annual_report_'
|
'annual_report_'
|
||||||
@ -26,12 +23,6 @@ class AnnualReport
|
|||||||
@year = year
|
@year = year
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.prepare(year)
|
|
||||||
SOURCES.each do |klass|
|
|
||||||
klass.prepare(year)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate
|
def generate
|
||||||
return if GeneratedAnnualReport.exists?(account: @account, year: @year)
|
return if GeneratedAnnualReport.exists?(account: @account, year: @year)
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AnnualReport::CommonlyInteractedWithAccounts < AnnualReport::Source
|
|
||||||
MINIMUM_INTERACTIONS = 1
|
|
||||||
SET_SIZE = 40
|
|
||||||
|
|
||||||
def generate
|
|
||||||
{
|
|
||||||
commonly_interacted_with_accounts: commonly_interacted_with_accounts.map do |(account_id, count)|
|
|
||||||
{
|
|
||||||
account_id: account_id.to_s,
|
|
||||||
count: count,
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def commonly_interacted_with_accounts
|
|
||||||
report_statuses.not_replying_to_account(@account).group(:in_reply_to_account_id).having(minimum_interaction_count).order(count_all: :desc).limit(SET_SIZE).count
|
|
||||||
end
|
|
||||||
|
|
||||||
def minimum_interaction_count
|
|
||||||
Arel.star.count.gt(MINIMUM_INTERACTIONS)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AnnualReport::MostRebloggedAccounts < AnnualReport::Source
|
|
||||||
MINIMUM_REBLOGS = 1
|
|
||||||
SET_SIZE = 10
|
|
||||||
|
|
||||||
def generate
|
|
||||||
{
|
|
||||||
most_reblogged_accounts: most_reblogged_accounts.map do |(account_id, count)|
|
|
||||||
{
|
|
||||||
account_id: account_id.to_s,
|
|
||||||
count: count,
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def most_reblogged_accounts
|
|
||||||
report_statuses.only_reblogs.joins(reblog: :account).group(accounts: [:id]).having(minimum_reblog_count).order(count_all: :desc).limit(SET_SIZE).count
|
|
||||||
end
|
|
||||||
|
|
||||||
def minimum_reblog_count
|
|
||||||
Arel.star.count.gt(MINIMUM_REBLOGS)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AnnualReport::Percentiles < AnnualReport::Source
|
|
||||||
def self.prepare(year)
|
|
||||||
AnnualReport::StatusesPerAccountCount.connection.exec_query(<<~SQL.squish, nil, [year, Mastodon::Snowflake.id_at(DateTime.new(year).beginning_of_year), Mastodon::Snowflake.id_at(DateTime.new(year).end_of_year)])
|
|
||||||
INSERT INTO annual_report_statuses_per_account_counts (year, account_id, statuses_count)
|
|
||||||
SELECT $1, account_id, count(*)
|
|
||||||
FROM statuses
|
|
||||||
WHERE id BETWEEN $2 AND $3
|
|
||||||
AND (local OR uri IS NULL)
|
|
||||||
GROUP BY account_id
|
|
||||||
ON CONFLICT (year, account_id) DO NOTHING
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate
|
|
||||||
{
|
|
||||||
percentiles: {
|
|
||||||
statuses: 100.0 - ((total_with_fewer_statuses / (total_with_any_statuses + 1.0)) * 100),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def statuses_created
|
|
||||||
@statuses_created ||= report_statuses.count
|
|
||||||
end
|
|
||||||
|
|
||||||
def total_with_fewer_statuses
|
|
||||||
@total_with_fewer_statuses ||= AnnualReport::StatusesPerAccountCount.where(year: year).where(statuses_count: ...statuses_created).count
|
|
||||||
end
|
|
||||||
|
|
||||||
def total_with_any_statuses
|
|
||||||
@total_with_any_statuses ||= AnnualReport::StatusesPerAccountCount.where(year: year).count
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -8,10 +8,6 @@ class AnnualReport::Source
|
|||||||
@year = year
|
@year = year
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.prepare(_year)
|
|
||||||
# Use this method if any pre-calculations must be made before individual annual reports are generated
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate
|
def generate
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class AnnualReport::TopHashtags < AnnualReport::Source
|
class AnnualReport::TopHashtags < AnnualReport::Source
|
||||||
MINIMUM_TAGGINGS = 1
|
MINIMUM_TAGGINGS = 1
|
||||||
SET_SIZE = 40
|
SET_SIZE = 5
|
||||||
|
|
||||||
def generate
|
def generate
|
||||||
{
|
{
|
||||||
|
|||||||
8
app/serializers/rest/collection_serializer.rb
Normal file
8
app/serializers/rest/collection_serializer.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::CollectionSerializer < ActiveModel::Serializer
|
||||||
|
attributes :uri, :name, :description, :local, :sensitive, :discoverable,
|
||||||
|
:created_at, :updated_at
|
||||||
|
|
||||||
|
belongs_to :account, serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
@ -75,6 +75,7 @@ Doorkeeper.configure do
|
|||||||
:'write:accounts',
|
:'write:accounts',
|
||||||
:'write:blocks',
|
:'write:blocks',
|
||||||
:'write:bookmarks',
|
:'write:bookmarks',
|
||||||
|
:'write:collections',
|
||||||
:'write:conversations',
|
:'write:conversations',
|
||||||
:'write:favourites',
|
:'write:favourites',
|
||||||
:'write:filters',
|
:'write:filters',
|
||||||
@ -89,6 +90,7 @@ Doorkeeper.configure do
|
|||||||
:'read:accounts',
|
:'read:accounts',
|
||||||
:'read:blocks',
|
:'read:blocks',
|
||||||
:'read:bookmarks',
|
:'read:bookmarks',
|
||||||
|
:'read:collections',
|
||||||
:'read:favourites',
|
:'read:favourites',
|
||||||
:'read:filters',
|
:'read:filters',
|
||||||
:'read:follows',
|
:'read:follows',
|
||||||
|
|||||||
@ -60,6 +60,7 @@ ja:
|
|||||||
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
|
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
|
||||||
setting_display_media_hide_all: メディアを常に隠す
|
setting_display_media_hide_all: メディアを常に隠す
|
||||||
setting_display_media_show_all: メディアを常に表示する
|
setting_display_media_show_all: メディアを常に表示する
|
||||||
|
setting_emoji_style: 絵文字の表示方法。「オート」の場合、可能ならネイティブの絵文字を使用し、レガシーなブラウザではTwemojiで代替します。
|
||||||
setting_system_scrollbars_ui: Safari/Chromeベースのデスクトップブラウザーでのみ有効です
|
setting_system_scrollbars_ui: Safari/Chromeベースのデスクトップブラウザーでのみ有効です
|
||||||
setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています
|
setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています
|
||||||
setting_use_pending_items: 新着があってもタイムラインを自動的にスクロールしないようにします
|
setting_use_pending_items: 新着があってもタイムラインを自動的にスクロールしないようにします
|
||||||
@ -225,6 +226,7 @@ ja:
|
|||||||
setting_default_privacy: 投稿の公開範囲
|
setting_default_privacy: 投稿の公開範囲
|
||||||
setting_default_quote_policy: 引用できるユーザー
|
setting_default_quote_policy: 引用できるユーザー
|
||||||
setting_default_sensitive: メディアを常に閲覧注意としてマークする
|
setting_default_sensitive: メディアを常に閲覧注意としてマークする
|
||||||
|
setting_delete_modal: 投稿を削除する前に警告する
|
||||||
setting_disable_hover_cards: マウスオーバーでプロフィールをポップアップしない
|
setting_disable_hover_cards: マウスオーバーでプロフィールをポップアップしない
|
||||||
setting_disable_swiping: スワイプでの切り替えを無効にする
|
setting_disable_swiping: スワイプでの切り替えを無効にする
|
||||||
setting_display_media: メディアの表示
|
setting_display_media: メディアの表示
|
||||||
@ -234,6 +236,7 @@ ja:
|
|||||||
setting_emoji_style: 絵文字スタイル
|
setting_emoji_style: 絵文字スタイル
|
||||||
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
|
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
|
||||||
setting_hide_network: 繋がりを隠す
|
setting_hide_network: 繋がりを隠す
|
||||||
|
setting_missing_alt_text_modal: 代替テキストなしでメディアを投稿する前に警告する
|
||||||
setting_quick_boosting: クイックブーストの有効化
|
setting_quick_boosting: クイックブーストの有効化
|
||||||
setting_reduce_motion: アニメーションの動きを減らす
|
setting_reduce_motion: アニメーションの動きを減らす
|
||||||
setting_system_font_ui: システムのデフォルトフォントを使う
|
setting_system_font_ui: システムのデフォルトフォントを使う
|
||||||
|
|||||||
@ -7,6 +7,8 @@ namespace :api, format: false do
|
|||||||
# Experimental JSON / REST API
|
# Experimental JSON / REST API
|
||||||
namespace :v1_alpha do
|
namespace :v1_alpha do
|
||||||
resources :async_refreshes, only: :show
|
resources :async_refreshes, only: :show
|
||||||
|
|
||||||
|
resources :collections, only: [:create]
|
||||||
end
|
end
|
||||||
|
|
||||||
# JSON / REST API
|
# JSON / REST API
|
||||||
|
|||||||
@ -5,9 +5,33 @@ require_relative 'base'
|
|||||||
|
|
||||||
module Mastodon::CLI
|
module Mastodon::CLI
|
||||||
class EmailDomainBlocks < Base
|
class EmailDomainBlocks < Base
|
||||||
|
option :only_blocked, type: :boolean, defaut: false
|
||||||
|
option :only_with_approval, type: :boolean, default: false
|
||||||
desc 'list', 'List blocked e-mail domains'
|
desc 'list', 'List blocked e-mail domains'
|
||||||
|
long_desc <<-LONG_DESC
|
||||||
|
By default this command lists all domains in the email domain block list
|
||||||
|
and their associated MX records (if included).
|
||||||
|
|
||||||
|
If the --only-blocked option is provided, this command will list only email
|
||||||
|
domains that are fully blocked from signup.
|
||||||
|
|
||||||
|
If the --only-with-approval option is provided, this command will list only
|
||||||
|
email domains that are allowed to be used but require manual approval.
|
||||||
|
|
||||||
|
The --only-blocked and --only-with-approval options are mutually exclusive.
|
||||||
|
LONG_DESC
|
||||||
def list
|
def list
|
||||||
EmailDomainBlock.parents.find_each do |parent|
|
fail_with_message 'Cannot specify both --only-blocked and --only-with-approval' if options[:only_blocked] && options[:only_with_approval]
|
||||||
|
|
||||||
|
base_query = EmailDomainBlock.parents
|
||||||
|
|
||||||
|
if options[:only_blocked]
|
||||||
|
base_query = base_query.where(allow_with_approval: false)
|
||||||
|
elsif options[:only_with_approval]
|
||||||
|
base_query = base_query.where(allow_with_approval: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
base_query.find_each do |parent|
|
||||||
say(parent.domain.to_s, :white)
|
say(parent.domain.to_s, :white)
|
||||||
|
|
||||||
shell.indent do
|
shell.indent do
|
||||||
@ -19,6 +43,7 @@ module Mastodon::CLI
|
|||||||
end
|
end
|
||||||
|
|
||||||
option :with_dns_records, type: :boolean
|
option :with_dns_records, type: :boolean
|
||||||
|
option :allow_with_approval, type: :boolean, defaut: false
|
||||||
desc 'add DOMAIN...', 'Block e-mail domain(s)'
|
desc 'add DOMAIN...', 'Block e-mail domain(s)'
|
||||||
long_desc <<-LONG_DESC
|
long_desc <<-LONG_DESC
|
||||||
Blocking an e-mail domain prevents users from signing up
|
Blocking an e-mail domain prevents users from signing up
|
||||||
@ -30,6 +55,9 @@ module Mastodon::CLI
|
|||||||
This can be helpful if you are blocking an e-mail server that has many
|
This can be helpful if you are blocking an e-mail server that has many
|
||||||
different domains pointing to it as it allows you to essentially block
|
different domains pointing to it as it allows you to essentially block
|
||||||
it at the root.
|
it at the root.
|
||||||
|
|
||||||
|
When the --allow-with-approval option is set, the email domains provided will
|
||||||
|
have to be manually approved for signup.
|
||||||
LONG_DESC
|
LONG_DESC
|
||||||
def add(*domains)
|
def add(*domains)
|
||||||
fail_with_message 'No domain(s) given' if domains.empty?
|
fail_with_message 'No domain(s) given' if domains.empty?
|
||||||
@ -47,19 +75,18 @@ module Mastodon::CLI
|
|||||||
other_domains = []
|
other_domains = []
|
||||||
other_domains = DomainResource.new(domain).mx if options[:with_dns_records]
|
other_domains = DomainResource.new(domain).mx if options[:with_dns_records]
|
||||||
|
|
||||||
email_domain_block = EmailDomainBlock.new(domain: domain, other_domains: other_domains)
|
email_domain_block = EmailDomainBlock.new(domain: domain, other_domains: other_domains, allow_with_approval: options[:allow_with_approval])
|
||||||
email_domain_block.save!
|
email_domain_block.save!
|
||||||
processed += 1
|
processed += 1
|
||||||
|
|
||||||
(email_domain_block.other_domains || []).uniq.each do |hostname|
|
(email_domain_block.other_domains || []).uniq.each do |hostname|
|
||||||
another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: email_domain_block)
|
|
||||||
|
|
||||||
if EmailDomainBlock.exists?(domain: hostname)
|
if EmailDomainBlock.exists?(domain: hostname)
|
||||||
say("#{hostname} is already blocked.", :yellow)
|
say("#{hostname} is already blocked.", :yellow)
|
||||||
skipped += 1
|
skipped += 1
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
|
another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: email_domain_block, allow_with_approval: options[:allow_with_approval])
|
||||||
another_email_domain_block.save!
|
another_email_domain_block.save!
|
||||||
processed += 1
|
processed += 1
|
||||||
end
|
end
|
||||||
|
|||||||
@ -48,7 +48,6 @@
|
|||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@github/webauthn-json": "^2.1.1",
|
"@github/webauthn-json": "^2.1.1",
|
||||||
"@optimize-lodash/rollup-plugin": "^5.0.2",
|
"@optimize-lodash/rollup-plugin": "^5.0.2",
|
||||||
"@rails/ujs": "7.1.600",
|
|
||||||
"@react-spring/web": "^9.7.5",
|
"@react-spring/web": "^9.7.5",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@use-gesture/react": "^10.3.1",
|
"@use-gesture/react": "^10.3.1",
|
||||||
@ -151,7 +150,6 @@
|
|||||||
"@types/object-assign": "^4.0.30",
|
"@types/object-assign": "^4.0.30",
|
||||||
"@types/prop-types": "^15.7.5",
|
"@types/prop-types": "^15.7.5",
|
||||||
"@types/punycode": "^2.1.0",
|
"@types/punycode": "^2.1.0",
|
||||||
"@types/rails__ujs": "^6.0.4",
|
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.7",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.6",
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe AnnualReport::CommonlyInteractedWithAccounts do
|
|
||||||
describe '#generate' do
|
|
||||||
subject { described_class.new(account, Time.zone.now.year) }
|
|
||||||
|
|
||||||
context 'with an inactive account' do
|
|
||||||
let(:account) { Fabricate :account }
|
|
||||||
|
|
||||||
it 'builds a report for an account' do
|
|
||||||
expect(subject.generate)
|
|
||||||
.to include(
|
|
||||||
commonly_interacted_with_accounts: be_an(Array).and(be_empty)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with an active account' do
|
|
||||||
let(:account) { Fabricate :account }
|
|
||||||
|
|
||||||
let(:other_account) { Fabricate :account }
|
|
||||||
let(:most_other_account) { Fabricate :account }
|
|
||||||
|
|
||||||
before do
|
|
||||||
_other = Fabricate :status
|
|
||||||
|
|
||||||
Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: other_account).id
|
|
||||||
Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: other_account).id
|
|
||||||
|
|
||||||
Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: most_other_account).id
|
|
||||||
Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: most_other_account).id
|
|
||||||
Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: most_other_account).id
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'builds a report for an account' do
|
|
||||||
expect(subject.generate)
|
|
||||||
.to include(
|
|
||||||
commonly_interacted_with_accounts: eq(
|
|
||||||
[
|
|
||||||
{ account_id: most_other_account.id.to_s, count: 3 },
|
|
||||||
{ account_id: other_account.id.to_s, count: 2 },
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe AnnualReport::MostRebloggedAccounts do
|
|
||||||
describe '#generate' do
|
|
||||||
subject { described_class.new(account, Time.zone.now.year) }
|
|
||||||
|
|
||||||
context 'with an inactive account' do
|
|
||||||
let(:account) { Fabricate :account }
|
|
||||||
|
|
||||||
it 'builds a report for an account' do
|
|
||||||
expect(subject.generate)
|
|
||||||
.to include(
|
|
||||||
most_reblogged_accounts: be_an(Array).and(be_empty)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with an active account' do
|
|
||||||
let(:account) { Fabricate :account }
|
|
||||||
|
|
||||||
let(:other_account) { Fabricate :account }
|
|
||||||
let(:most_other_account) { Fabricate :account }
|
|
||||||
|
|
||||||
before do
|
|
||||||
_other = Fabricate :status
|
|
||||||
Fabricate :status, account: account, reblog: Fabricate(:status, account: other_account)
|
|
||||||
Fabricate :status, account: account, reblog: Fabricate(:status, account: other_account)
|
|
||||||
|
|
||||||
Fabricate :status, account: account, reblog: Fabricate(:status, account: most_other_account)
|
|
||||||
Fabricate :status, account: account, reblog: Fabricate(:status, account: most_other_account)
|
|
||||||
Fabricate :status, account: account, reblog: Fabricate(:status, account: most_other_account)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'builds a report for an account' do
|
|
||||||
expect(subject.generate)
|
|
||||||
.to include(
|
|
||||||
most_reblogged_accounts: eq(
|
|
||||||
[
|
|
||||||
{ account_id: most_other_account.id.to_s, count: 3 },
|
|
||||||
{ account_id: other_account.id.to_s, count: 2 },
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe AnnualReport::Percentiles do
|
|
||||||
describe '#generate' do
|
|
||||||
subject { described_class.new(account, year) }
|
|
||||||
|
|
||||||
let(:year) { Time.zone.now.year }
|
|
||||||
|
|
||||||
context 'with an inactive account' do
|
|
||||||
let(:account) { Fabricate :account }
|
|
||||||
|
|
||||||
it 'builds a report for an account' do
|
|
||||||
described_class.prepare(year)
|
|
||||||
|
|
||||||
expect(subject.generate)
|
|
||||||
.to include(
|
|
||||||
percentiles: include(
|
|
||||||
statuses: 100
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with an active account' do
|
|
||||||
let(:account) { Fabricate :account }
|
|
||||||
|
|
||||||
before do
|
|
||||||
Fabricate.times 2, :status # Others as `account`
|
|
||||||
Fabricate.times 2, :status, account: account
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'builds a report for an account' do
|
|
||||||
described_class.prepare(year)
|
|
||||||
|
|
||||||
expect(subject.generate)
|
|
||||||
.to include(
|
|
||||||
percentiles: include(
|
|
||||||
statuses: 50
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -13,13 +13,4 @@ RSpec.describe AnnualReport do
|
|||||||
.to change(GeneratedAnnualReport, :count).by(1)
|
.to change(GeneratedAnnualReport, :count).by(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.prepare' do
|
|
||||||
before { Fabricate :status }
|
|
||||||
|
|
||||||
it 'generates records from source class which prepare data' do
|
|
||||||
expect { described_class.prepare(Time.current.year) }
|
|
||||||
.to change(AnnualReport::StatusesPerAccountCount, :count).by(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -15,17 +15,62 @@ RSpec.describe Mastodon::CLI::EmailDomainBlocks do
|
|||||||
describe '#list' do
|
describe '#list' do
|
||||||
let(:action) { :list }
|
let(:action) { :list }
|
||||||
|
|
||||||
|
context 'with both --only-blocked and --only-with-approval' do
|
||||||
|
let(:options) { { only_blocked: true, only_with_approval: true } }
|
||||||
|
|
||||||
|
it 'warns about usage and exits' do
|
||||||
|
expect { subject }
|
||||||
|
.to raise_error(Thor::Error, 'Cannot specify both --only-blocked and --only-with-approval')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with email domain block records' do
|
context 'with email domain block records' do
|
||||||
let!(:parent_block) { Fabricate(:email_domain_block) }
|
let!(:parent_block) { Fabricate(:email_domain_block) }
|
||||||
let!(:child_block) { Fabricate(:email_domain_block, parent: parent_block) }
|
let!(:child_block) { Fabricate(:email_domain_block, parent: parent_block) }
|
||||||
|
let!(:parent_allow_block) { Fabricate(:email_domain_block, allow_with_approval: true) }
|
||||||
|
let!(:child_allow_block) { Fabricate(:email_domain_block, parent: parent_allow_block, allow_with_approval: true) }
|
||||||
|
|
||||||
it 'lists the blocks' do
|
it 'lists all the blocks by default' do
|
||||||
expect { subject }
|
expect { subject }
|
||||||
.to output_results(
|
.to output_results(
|
||||||
parent_block.domain,
|
parent_block.domain,
|
||||||
child_block.domain
|
child_block.domain,
|
||||||
|
parent_allow_block.domain,
|
||||||
|
child_allow_block.domain
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with the --only-blocked flag set' do
|
||||||
|
let(:options) { { only_blocked: true } }
|
||||||
|
|
||||||
|
it 'lists only blocked domains' do
|
||||||
|
expect { subject }
|
||||||
|
.to output_results(
|
||||||
|
parent_block.domain,
|
||||||
|
child_block.domain
|
||||||
|
)
|
||||||
|
.and not_output_results(
|
||||||
|
parent_allow_block.domain,
|
||||||
|
child_allow_block.domain
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the --only-with-approval flag set' do
|
||||||
|
let(:options) { { only_with_approval: true } }
|
||||||
|
|
||||||
|
it 'lists only manually approvable domains' do
|
||||||
|
expect { subject }
|
||||||
|
.to output_results(
|
||||||
|
parent_allow_block.domain,
|
||||||
|
child_allow_block.domain
|
||||||
|
)
|
||||||
|
.and not_output_results(
|
||||||
|
parent_block.domain,
|
||||||
|
child_block.domain
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -56,6 +101,7 @@ RSpec.describe Mastodon::CLI::EmailDomainBlocks do
|
|||||||
context 'when no blocks exist' do
|
context 'when no blocks exist' do
|
||||||
let(:domain) { 'host.example' }
|
let(:domain) { 'host.example' }
|
||||||
let(:arguments) { [domain] }
|
let(:arguments) { [domain] }
|
||||||
|
let(:options) { { allow_with_approval: false } }
|
||||||
|
|
||||||
it 'adds a new block' do
|
it 'adds a new block' do
|
||||||
expect { subject }
|
expect { subject }
|
||||||
@ -67,7 +113,7 @@ RSpec.describe Mastodon::CLI::EmailDomainBlocks do
|
|||||||
context 'with --with-dns-records true' do
|
context 'with --with-dns-records true' do
|
||||||
let(:domain) { 'host.example' }
|
let(:domain) { 'host.example' }
|
||||||
let(:arguments) { [domain] }
|
let(:arguments) { [domain] }
|
||||||
let(:options) { { with_dns_records: true } }
|
let(:options) { { allow_with_approval: false, with_dns_records: true } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
configure_mx(domain: domain, exchange: 'other.host')
|
configure_mx(domain: domain, exchange: 'other.host')
|
||||||
|
|||||||
54
spec/requests/api/v1_alpha/collections_spec.rb
Normal file
54
spec/requests/api/v1_alpha/collections_spec.rb
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
|
||||||
|
include_context 'with API authentication', oauth_scopes: 'read:collections write:collections'
|
||||||
|
|
||||||
|
describe 'POST /api/v1_alpha/collections' do
|
||||||
|
subject do
|
||||||
|
post '/api/v1_alpha/collections', headers: headers, params: params
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'read'
|
||||||
|
|
||||||
|
context 'with valid params' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
name: 'Low-traffic bots',
|
||||||
|
description: 'Really nice bots, please follow',
|
||||||
|
sensitive: '0',
|
||||||
|
discoverable: '1',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a collection and returns http success' do
|
||||||
|
expect do
|
||||||
|
subject
|
||||||
|
end.to change(Collection, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid params' do
|
||||||
|
it 'returns http unprocessable content and detailed errors' do
|
||||||
|
expect do
|
||||||
|
subject
|
||||||
|
end.to_not change(Collection, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
expect(response.parsed_body).to include({
|
||||||
|
'error' => a_hash_including({
|
||||||
|
'details' => a_hash_including({
|
||||||
|
'name' => [{ 'error' => 'ERR_BLANK', 'description' => "can't be blank" }],
|
||||||
|
'description' => [{ 'error' => 'ERR_BLANK', 'description' => "can't be blank" }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
30
spec/serializers/rest/collection_serializer_spec.rb
Normal file
30
spec/serializers/rest/collection_serializer_spec.rb
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe REST::CollectionSerializer do
|
||||||
|
subject { serialized_record_json(collection, described_class) }
|
||||||
|
|
||||||
|
let(:collection) do
|
||||||
|
Fabricate(:collection,
|
||||||
|
name: 'Exquisite follows',
|
||||||
|
description: 'Always worth a follow',
|
||||||
|
local: true,
|
||||||
|
sensitive: true,
|
||||||
|
discoverable: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes the relevant attributes' do
|
||||||
|
expect(subject)
|
||||||
|
.to include(
|
||||||
|
'account' => an_instance_of(Hash),
|
||||||
|
'name' => 'Exquisite follows',
|
||||||
|
'description' => 'Always worth a follow',
|
||||||
|
'local' => true,
|
||||||
|
'sensitive' => true,
|
||||||
|
'discoverable' => false,
|
||||||
|
'created_at' => match_api_datetime_format,
|
||||||
|
'updated_at' => match_api_datetime_format
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -7,3 +7,5 @@ module CommandLineHelpers
|
|||||||
).to_stdout
|
).to_stdout
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
RSpec::Matchers.define_negated_matcher :not_output_results, :output_results
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@ -2708,7 +2708,6 @@ __metadata:
|
|||||||
"@gamestdio/websocket": "npm:^0.3.2"
|
"@gamestdio/websocket": "npm:^0.3.2"
|
||||||
"@github/webauthn-json": "npm:^2.1.1"
|
"@github/webauthn-json": "npm:^2.1.1"
|
||||||
"@optimize-lodash/rollup-plugin": "npm:^5.0.2"
|
"@optimize-lodash/rollup-plugin": "npm:^5.0.2"
|
||||||
"@rails/ujs": "npm:7.1.600"
|
|
||||||
"@react-spring/web": "npm:^9.7.5"
|
"@react-spring/web": "npm:^9.7.5"
|
||||||
"@reduxjs/toolkit": "npm:^2.0.1"
|
"@reduxjs/toolkit": "npm:^2.0.1"
|
||||||
"@storybook/addon-a11y": "npm:^10.0.6"
|
"@storybook/addon-a11y": "npm:^10.0.6"
|
||||||
@ -2728,7 +2727,6 @@ __metadata:
|
|||||||
"@types/object-assign": "npm:^4.0.30"
|
"@types/object-assign": "npm:^4.0.30"
|
||||||
"@types/prop-types": "npm:^15.7.5"
|
"@types/prop-types": "npm:^15.7.5"
|
||||||
"@types/punycode": "npm:^2.1.0"
|
"@types/punycode": "npm:^2.1.0"
|
||||||
"@types/rails__ujs": "npm:^6.0.4"
|
|
||||||
"@types/react": "npm:^18.2.7"
|
"@types/react": "npm:^18.2.7"
|
||||||
"@types/react-dom": "npm:^18.2.4"
|
"@types/react-dom": "npm:^18.2.4"
|
||||||
"@types/react-helmet": "npm:^6.1.6"
|
"@types/react-helmet": "npm:^6.1.6"
|
||||||
@ -3207,13 +3205,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@rails/ujs@npm:7.1.600":
|
|
||||||
version: 7.1.600
|
|
||||||
resolution: "@rails/ujs@npm:7.1.600"
|
|
||||||
checksum: 10c0/0ccaa68a08fbc7b084ab89a1fe49520a5cba6d99f4b0feaf0cb3d00334c59d8d798932d7e49b84aa388875d039ea1e17eb115ed96a80ad157e408a13eceef53e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@react-spring/animated@npm:~9.7.5":
|
"@react-spring/animated@npm:~9.7.5":
|
||||||
version: 9.7.5
|
version: 9.7.5
|
||||||
resolution: "@react-spring/animated@npm:9.7.5"
|
resolution: "@react-spring/animated@npm:9.7.5"
|
||||||
@ -4299,13 +4290,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/rails__ujs@npm:^6.0.4":
|
|
||||||
version: 6.0.4
|
|
||||||
resolution: "@types/rails__ujs@npm:6.0.4"
|
|
||||||
checksum: 10c0/7477cb03a0e1339b9cd5c8ac4a197a153e2ff48742b2f527c5a39dcdf80f01493011e368483290d3717662c63066fada3ab203a335804cbb3573cf575f37007e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/range-parser@npm:*":
|
"@types/range-parser@npm:*":
|
||||||
version: 1.2.7
|
version: 1.2.7
|
||||||
resolution: "@types/range-parser@npm:1.2.7"
|
resolution: "@types/range-parser@npm:1.2.7"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user