Merge branch 'glitch-soc' into develop

This commit is contained in:
Jeremy Kescher 2024-03-03 03:12:37 +01:00
commit 6aa17a7c1b
No known key found for this signature in database
GPG key ID: 80A419A7A613DFA4
45 changed files with 553 additions and 382 deletions

4
.github/codecov.yml vendored
View file

@ -1,3 +1,4 @@
comment: false # Do not leave PR comments
coverage:
status:
project:
@ -8,6 +9,3 @@ coverage:
default:
# Github status check is not blocking
informational: true
comment:
# Only write a comment in PR if there are changes
require_changes: true

View file

@ -4,7 +4,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_account
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
after_action :insert_pagination_headers
def index
cache_if_unauthenticated!

View file

@ -1,224 +0,0 @@
// This file will be loaded on admin pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
import ready from '../mastodon/ready';
const setAnnouncementEndsAttributes = (target) => {
const valid = target?.value && target?.validity?.valid;
const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at');
if (valid) {
element.classList.remove('optional');
element.required = true;
element.min = target.value;
} else {
element.classList.add('optional');
element.removeAttribute('required');
element.removeAttribute('min');
}
};
Rails.delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
setAnnouncementEndsAttributes(target);
});
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
selectAllMatchingElement.classList.add('active');
};
const hideSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
const hiddenField = document.querySelector('#select_all_matching');
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
selectAllMatchingElement.classList.remove('active');
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
hiddenField.value = '0';
};
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
document.querySelectorAll(batchCheckboxClassName).forEach((content) => {
content.checked = target.checked;
});
if (selectAllMatchingElement) {
if (target.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
});
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
const hiddenField = document.querySelector('#select_all_matching');
const active = hiddenField.value === '1';
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
if (active) {
hiddenField.value = '0';
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
} else {
hiddenField.value = '1';
notSelectedMsg.classList.remove('active');
selectedMsg.classList.add('active');
}
});
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector('#batch_checkbox_all');
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
if (checkAllElement) {
const allCheckboxes = Array.from(
document.querySelectorAll(batchCheckboxClassName)
);
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && allCheckboxes.some((content) => content.checked);
if (selectAllMatchingElement) {
if (checkAllElement.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
}
});
Rails.delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
target.form.submit();
});
const onDomainBlockSeverityChange = (target) => {
const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
if (rejectMediaDiv) {
rejectMediaDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
}
if (rejectReportsDiv) {
rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
}
};
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
const onEnableBootstrapTimelineAccountsChange = (target) => {
const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
if (bootstrapTimelineAccountsField) {
bootstrapTimelineAccountsField.disabled = !target.checked;
if (target.checked) {
bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
bootstrapTimelineAccountsField.parentElement.parentElement.classList.remove('disabled');
} else {
bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
bootstrapTimelineAccountsField.parentElement.parentElement.classList.add('disabled');
}
}
};
Rails.delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
const onChangeRegistrationMode = (target) => {
const enabled = target.value === 'approved';
document.querySelectorAll('.form_admin_settings_registrations_mode .warning-hint').forEach((warning_hint) => {
warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
});
document.querySelectorAll('#form_admin_settings_require_invite_text').forEach((input) => {
input.disabled = !enabled;
if (enabled) {
let element = input;
do {
element.classList.remove('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
} else {
let element = input;
do {
element.classList.add('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
}
});
};
const convertUTCDateTimeToLocal = (value) => {
const date = new Date(value + 'Z');
const twoChars = (x) => (x.toString().padStart(2, '0'));
return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
};
const convertLocalDatetimeToUTC = (value) => {
const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/;
const match = re.exec(value);
const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]);
const fullISO8601 = date.toISOString();
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
};
Rails.delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
ready(() => {
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
const registrationMode = document.getElementById('form_admin_settings_registrations_mode');
if (registrationMode) onChangeRegistrationMode(registrationMode);
const checkAllElement = document.querySelector('#batch_checkbox_all');
if (checkAllElement) {
const allCheckboxes = Array.from(document.querySelectorAll(batchCheckboxClassName));
checkAllElement.checked = allCheckboxes.every( (content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && allCheckboxes.some((content) => content.checked);
}
document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => {
const domain = document.querySelector('input[type="text"]#by_domain')?.value;
if (domain) {
const url = new URL(event.target.href);
url.searchParams.set('_domain', domain);
e.target.href = url;
}
});
document.querySelectorAll('input[type="datetime-local"]').forEach(element => {
if (element.value) {
element.value = convertUTCDateTimeToLocal(element.value);
}
if (element.placeholder) {
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
}
});
Rails.delegate(document, 'form', 'submit', ({ target }) => {
target.querySelectorAll('input[type="datetime-local"]').forEach(element => {
if (element.value && element.validity.valid) {
element.value = convertLocalDatetimeToUTC(element.value);
}
});
});
const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at');
if (announcementStartsAt) {
setAnnouncementEndsAttributes(announcementStartsAt);
}
});

View file

@ -0,0 +1,340 @@
// This file will be loaded on admin pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
import ready from '../mastodon/ready';
const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
const valid = target.value && target.validity.valid;
const element = document.querySelector<HTMLInputElement>(
'input[type="datetime-local"]#announcement_ends_at',
);
if (!element) return;
if (valid) {
element.classList.remove('optional');
element.required = true;
element.min = target.value;
} else {
element.classList.add('optional');
element.removeAttribute('required');
element.removeAttribute('min');
}
};
Rails.delegate(
document,
'input[type="datetime-local"]#announcement_starts_at',
'change',
({ target }) => {
if (target instanceof HTMLInputElement)
setAnnouncementEndsAttributes(target);
},
);
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
selectAllMatchingElement?.classList.add('active');
};
const hideSelectAll = () => {
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
const hiddenField = document.querySelector<HTMLInputElement>(
'input#select_all_matching',
);
const selectedMsg = document.querySelector(
'.batch-table__select-all .selected',
);
const notSelectedMsg = document.querySelector(
'.batch-table__select-all .not-selected',
);
selectAllMatchingElement?.classList.remove('active');
selectedMsg?.classList.remove('active');
notSelectedMsg?.classList.add('active');
if (hiddenField) hiddenField.value = '0';
};
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
document
.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
.forEach((content) => {
content.checked = target.checked;
});
if (selectAllMatchingElement) {
if (target.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
});
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
const hiddenField = document.querySelector<HTMLInputElement>(
'#select_all_matching',
);
if (!hiddenField) return;
const active = hiddenField.value === '1';
const selectedMsg = document.querySelector(
'.batch-table__select-all .selected',
);
const notSelectedMsg = document.querySelector(
'.batch-table__select-all .not-selected',
);
if (!selectedMsg || !notSelectedMsg) return;
if (active) {
hiddenField.value = '0';
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
} else {
hiddenField.value = '1';
notSelectedMsg.classList.remove('active');
selectedMsg.classList.add('active');
}
});
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector<HTMLInputElement>(
'input#batch_checkbox_all',
);
const selectAllMatchingElement = document.querySelector(
'.batch-table__select-all',
);
if (checkAllElement) {
const allCheckboxes = Array.from(
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
);
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
checkAllElement.indeterminate =
!checkAllElement.checked &&
allCheckboxes.some((content) => content.checked);
if (selectAllMatchingElement) {
if (checkAllElement.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
}
});
Rails.delegate(
document,
'.filter-subset--with-select select',
'change',
({ target }) => {
if (target instanceof HTMLSelectElement) target.form?.submit();
},
);
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
const rejectMediaDiv = document.querySelector(
'.input.with_label.domain_block_reject_media',
);
const rejectReportsDiv = document.querySelector(
'.input.with_label.domain_block_reject_reports',
);
if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) {
rejectMediaDiv.style.display =
target.value === 'suspend' ? 'none' : 'block';
}
if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) {
rejectReportsDiv.style.display =
target.value === 'suspend' ? 'none' : 'block';
}
};
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
});
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
const bootstrapTimelineAccountsField =
document.querySelector<HTMLInputElement>(
'#form_admin_settings_bootstrap_timeline_accounts',
);
if (bootstrapTimelineAccountsField) {
bootstrapTimelineAccountsField.disabled = !target.checked;
if (target.checked) {
bootstrapTimelineAccountsField.parentElement?.classList.remove(
'disabled',
);
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove(
'disabled',
);
} else {
bootstrapTimelineAccountsField.parentElement?.classList.add('disabled');
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add(
'disabled',
);
}
}
};
Rails.delegate(
document,
'#form_admin_settings_enable_bootstrap_timeline_accounts',
'change',
({ target }) => {
if (target instanceof HTMLInputElement)
onEnableBootstrapTimelineAccountsChange(target);
},
);
const onChangeRegistrationMode = (target: HTMLSelectElement) => {
const enabled = target.value === 'approved';
document
.querySelectorAll<HTMLElement>(
'.form_admin_settings_registrations_mode .warning-hint',
)
.forEach((warning_hint) => {
warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
});
document
.querySelectorAll<HTMLInputElement>(
'input#form_admin_settings_require_invite_text',
)
.forEach((input) => {
input.disabled = !enabled;
if (enabled) {
let element: HTMLElement | null = input;
do {
element.classList.remove('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
} else {
let element: HTMLElement | null = input;
do {
element.classList.add('disabled');
element = element.parentElement;
} while (element && !element.classList.contains('fields-group'));
}
});
};
const convertUTCDateTimeToLocal = (value: string) => {
const date = new Date(value + 'Z');
const twoChars = (x: number) => x.toString().padStart(2, '0');
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
};
function convertLocalDatetimeToUTC(value: string) {
const date = new Date(value);
const fullISO8601 = date.toISOString();
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
}
Rails.delegate(
document,
'#form_admin_settings_registrations_mode',
'change',
({ target }) => {
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
},
);
ready(() => {
const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
'select#domain_block_severity',
);
if (domainBlockSeveritySelect)
onDomainBlockSeverityChange(domainBlockSeveritySelect);
const enableBootstrapTimelineAccounts =
document.querySelector<HTMLInputElement>(
'input#form_admin_settings_enable_bootstrap_timeline_accounts',
);
if (enableBootstrapTimelineAccounts)
onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
const registrationMode = document.querySelector<HTMLSelectElement>(
'select#form_admin_settings_registrations_mode',
);
if (registrationMode) onChangeRegistrationMode(registrationMode);
const checkAllElement = document.querySelector<HTMLInputElement>(
'input#batch_checkbox_all',
);
if (checkAllElement) {
const allCheckboxes = Array.from(
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
);
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
checkAllElement.indeterminate =
!checkAllElement.checked &&
allCheckboxes.some((content) => content.checked);
}
document
.querySelector('a#add-instance-button')
?.addEventListener('click', (e) => {
const domain = document.querySelector<HTMLInputElement>(
'input[type="text"]#by_domain',
)?.value;
if (domain && e.target instanceof HTMLAnchorElement) {
const url = new URL(e.target.href);
url.searchParams.set('_domain', domain);
e.target.href = url.toString();
}
});
document
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
.forEach((element) => {
if (element.value) {
element.value = convertUTCDateTimeToLocal(element.value);
}
if (element.placeholder) {
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
}
});
Rails.delegate(document, 'form', 'submit', ({ target }) => {
if (target instanceof HTMLFormElement)
target
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
.forEach((element) => {
if (element.value && element.validity.valid) {
element.value = convertLocalDatetimeToUTC(element.value);
}
});
});
const announcementStartsAt = document.querySelector<HTMLInputElement>(
'input[type="datetime-local"]#announcement_starts_at',
);
if (announcementStartsAt) {
setAnnouncementEndsAttributes(announcementStartsAt);
}
}).catch((reason) => {
throw reason;
});

View file

@ -2,7 +2,7 @@
# theme.
pack:
about:
admin: admin.js
admin: admin.ts
auth: auth.js
common:
filename: common.js

View file

@ -68,7 +68,7 @@ class EditedTimestamp extends PureComponent {
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
</button>
</DropdownMenu>
);

View file

@ -53,7 +53,6 @@ const messages = defineMessages({
});
const dateFormatOptions = {
hour12: false,
year: 'numeric',
month: 'short',
day: '2-digit',

View file

@ -362,7 +362,7 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar-spacer' />
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>
</div>
);

View file

@ -84,7 +84,6 @@ const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
};

View file

@ -343,7 +343,7 @@ class Announcement extends ImmutablePureComponent {
<div className='announcements__item'>
<strong className='announcements__item__range'>
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
{hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
{hasTimeRange && <span> · <FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
</strong>
<Content announcement={announcement} />

View file

@ -82,6 +82,10 @@ export default class Card extends PureComponent {
this.setState({ embedded: true });
};
handleExternalLinkClick = (e) => {
e.stopPropagation();
};
setRef = c => {
this.node = c;
};
@ -191,7 +195,7 @@ export default class Card extends PureComponent {
<div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
<div>
<button type='button' onClick={this.handleEmbedClick}><Icon id='play' icon={PlayArrowIcon} /></button>
<a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' icon={OpenInNewIcon} /></a>
<a href={card.get('url')} onClick={this.handleExternalLinkClick} target='_blank' rel='noopener noreferrer'><Icon id='external-link' icon={OpenInNewIcon} /></a>
</div>
</div>
) : spoilerButton}

View file

@ -346,7 +346,7 @@ class DetailedStatus extends ImmutablePureComponent {
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
</div>
</div>

View file

@ -1,25 +0,0 @@
import 'packs/public-path';
import { createRoot } from 'react-dom/client';
import ready from 'flavours/glitch/ready';
ready(() => {
document.querySelectorAll('[data-admin-component]').forEach(element => {
const componentName = element.getAttribute('data-admin-component');
const componentProps = JSON.parse(element.getAttribute('data-props'));
import('flavours/glitch/containers/admin_component').then(({ default: AdminComponent }) => {
return import('flavours/glitch/components/admin/' + componentName).then(({ default: Component }) => {
const root = createRoot(element);
root.render (
<AdminComponent>
<Component {...componentProps} />
</AdminComponent>,
);
});
}).catch(error => {
console.error(error);
});
});
});

View file

@ -0,0 +1,37 @@
import 'packs/public-path';
import { createRoot } from 'react-dom/client';
import ready from 'flavours/glitch/ready';
async function mountReactComponent(element: Element) {
const componentName = element.getAttribute('data-admin-component');
const stringProps = element.getAttribute('data-props');
if (!stringProps) return;
const componentProps = JSON.parse(stringProps) as object;
const { default: AdminComponent } = await import(
'@/flavours/glitch/containers/admin_component'
);
const { default: Component } = (await import(
`@/flavours/glitch/components/admin/${componentName}`
)) as { default: React.ComponentType };
const root = createRoot(element);
root.render(
<AdminComponent>
<Component {...componentProps} />
</AdminComponent>,
);
}
ready(() => {
document.querySelectorAll('[data-admin-component]').forEach((element) => {
void mountReactComponent(element);
});
}).catch((reason) => {
throw reason;
});

View file

@ -60,7 +60,6 @@ function main() {
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
hour12: false,
});
const formatMessage = ({ id, defaultMessage }, values) => {

View file

@ -5752,10 +5752,6 @@ a.status-card {
pointer-events: auto;
opacity: 1;
}
@media screen and (min-width: $no-gap-breakpoint) {
inset-inline-start: 16px - 2px;
}
}
.icon-search {
@ -8902,7 +8898,7 @@ noscript {
.search__input {
border: 1px solid lighten($ui-base-color, 8%);
padding: 10px;
padding-inline-end: 28px;
padding-inline-end: 30px;
}
.search__popout {

View file

@ -1,7 +1,7 @@
# (REQUIRED) The location of the pack files.
pack:
admin:
- packs/admin.jsx
- packs/admin.tsx
- packs/public.jsx
auth: packs/public.jsx
common:

View file

@ -1,7 +1,7 @@
# (REQUIRED) The location of the pack files inside `pack_directory`.
pack:
admin:
- admin.jsx
- admin.tsx
- public.jsx
auth: public.jsx
common:

View file

@ -67,7 +67,7 @@ class EditedTimestamp extends PureComponent {
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
</button>
</DropdownMenu>
);

View file

@ -53,7 +53,6 @@ const messages = defineMessages({
});
const dateFormatOptions = {
hour12: false,
year: 'numeric',
month: 'short',
day: '2-digit',

View file

@ -554,7 +554,7 @@ class Status extends ImmutablePureComponent {
<div onClick={this.handleClick} className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>

View file

@ -101,7 +101,6 @@ const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
};

View file

@ -343,7 +343,7 @@ class Announcement extends ImmutablePureComponent {
<div className='announcements__item'>
<strong className='announcements__item__range'>
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
{hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
{hasTimeRange && <span> · <FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
</strong>
<Content announcement={announcement} />

View file

@ -92,6 +92,10 @@ export default class Card extends PureComponent {
this.setState({ embedded: true });
};
handleExternalLinkClick = (e) => {
e.stopPropagation();
};
setRef = c => {
this.node = c;
};
@ -201,7 +205,7 @@ export default class Card extends PureComponent {
<div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
<div>
<button type='button' onClick={this.handleEmbedClick}><Icon id='play' icon={PlayArrowIcon} /></button>
<a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' icon={OpenInNewIcon} /></a>
<a href={card.get('url')} onClick={this.handleExternalLinkClick} target='_blank' rel='noopener noreferrer'><Icon id='external-link' icon={OpenInNewIcon} /></a>
</div>
</div>
) : spoilerButton}

View file

@ -311,7 +311,7 @@ class DetailedStatus extends ImmutablePureComponent {
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
</div>
</div>

View file

@ -1,6 +1,6 @@
{
"about.blocks": "Prižiūrimi serveriai",
"about.contact": "Kontaktuoti:",
"about.contact": "Kontaktai:",
"about.disclaimer": "Mastodon nemokama atvirojo kodo programa ir Mastodon gGmbH prekės ženklas.",
"about.domain_blocks.no_reason_available": "Priežastis nepateikta",
"about.domain_blocks.preamble": "Mastodon paprastai leidžia peržiūrėti turinį ir bendrauti su naudotojais iš bet kurio kito fediverse esančio serverio. Šios yra išimtys, kurios buvo padarytos šiame konkrečiame serveryje.",
@ -424,7 +424,7 @@
"notifications.column_settings.mention": "Paminėjimai:",
"notifications.column_settings.poll": "Balsavimo rezultatai:",
"notifications.column_settings.push": "\"Push\" pranešimai",
"notifications.column_settings.reblog": "\"Boost\" kiekis:",
"notifications.column_settings.reblog": "Pakėlimai:",
"notifications.column_settings.show": "Rodyti stulpelyje",
"notifications.column_settings.sound": "Paleisti garsą",
"notifications.column_settings.status": "New toots:",
@ -636,6 +636,7 @@
"status.translate": "Versti",
"status.translated_from_with": "Išversta iš {lang} naudojant {provider}",
"status.uncached_media_warning": "Peržiūra nepasiekiama",
"subscribed_languages.lead": "Po pakeitimo tavo pagrindinėje ir sąrašo laiko juostose bus rodomi tik įrašai pasirinktomis kalbomis. Jei nori gauti įrašus visomis kalbomis, pasirink nė vieno.",
"tabs_bar.home": "Pradžia",
"tabs_bar.notifications": "Pranešimai",
"time_remaining.days": "Liko {number, plural, one {# diena} few {# dienos} many {# dieno} other {# dienų}}",

View file

@ -529,8 +529,8 @@
"poll_button.add_poll": "Peiling toevoegen",
"poll_button.remove_poll": "Peiling verwijderen",
"privacy.change": "Zichtbaarheid van bericht aanpassen",
"privacy.direct.long": "Iedereen die in het bericht wordt vermeld",
"privacy.direct.short": "Bepaalde mensen",
"privacy.direct.long": "Alleen voor mensen die specifiek in het bericht worden vermeld",
"privacy.direct.short": "Specifieke mensen",
"privacy.private.long": "Alleen jouw volgers",
"privacy.private.short": "Volgers",
"privacy.public.long": "Iedereen op Mastodon en daarbuiten",

View file

@ -668,7 +668,7 @@
"status.mute": "靜音 @{name}",
"status.mute_conversation": "靜音對話",
"status.open": "展開此嘟文",
"status.pin": "釘選個人檔案頁面",
"status.pin": "釘選個人檔案頁面",
"status.pinned": "釘選嘟文",
"status.read_more": "閱讀更多",
"status.reblog": "轉嘟",

View file

@ -1,25 +0,0 @@
import './public-path';
import { createRoot } from 'react-dom/client';
import ready from '../mastodon/ready';
ready(() => {
document.querySelectorAll('[data-admin-component]').forEach(element => {
const componentName = element.getAttribute('data-admin-component');
const componentProps = JSON.parse(element.getAttribute('data-props'));
import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => {
return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => {
const root = createRoot(element);
root.render (
<AdminComponent>
<Component {...componentProps} />
</AdminComponent>,
);
});
}).catch(error => {
console.error(error);
});
});
});

View file

@ -0,0 +1,37 @@
import './public-path';
import { createRoot } from 'react-dom/client';
import ready from '../mastodon/ready';
async function mountReactComponent(element: Element) {
const componentName = element.getAttribute('data-admin-component');
const stringProps = element.getAttribute('data-props');
if (!stringProps) return;
const componentProps = JSON.parse(stringProps) as object;
const { default: AdminComponent } = await import(
'@/mastodon/containers/admin_component'
);
const { default: Component } = (await import(
`@/mastodon/components/admin/${componentName}`
)) as { default: React.ComponentType };
const root = createRoot(element);
root.render(
<AdminComponent>
<Component {...componentProps} />
</AdminComponent>,
);
}
ready(() => {
document.querySelectorAll('[data-admin-component]').forEach((element) => {
void mountReactComponent(element);
});
}).catch((reason) => {
throw reason;
});

View file

@ -49,7 +49,6 @@ function loaded() {
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
hour12: false,
});
const formatMessage = ({ id, defaultMessage }, values) => {

View file

@ -5210,10 +5210,6 @@ a.status-card {
pointer-events: auto;
opacity: 1;
}
@media screen and (min-width: $no-gap-breakpoint) {
inset-inline-start: 16px - 2px;
}
}
.icon-search {
@ -8265,7 +8261,7 @@ noscript {
.search__input {
border: 1px solid lighten($ui-base-color, 8%);
padding: 10px;
padding-inline-end: 28px;
padding-inline-end: 30px;
}
.search__popout {

View file

@ -28,14 +28,17 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base
def sql_query_string
<<~SQL.squish
SELECT axis.*, (
SELECT count(distinct accounts.domain) AS value
FROM statuses
INNER JOIN statuses_tags ON statuses.id = statuses_tags.status_id
INNER JOIN accounts ON statuses.account_id = accounts.id
WHERE statuses_tags.tag_id = :tag_id
AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id
AND date_trunc('day', statuses.created_at)::date = axis.period
)
WITH tag_servers AS (
SELECT DISTINCT accounts.domain
FROM statuses
INNER JOIN statuses_tags ON statuses.id = statuses_tags.status_id
INNER JOIN accounts ON statuses.account_id = accounts.id
WHERE statuses_tags.tag_id = :tag_id
AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id
AND date_trunc('day', statuses.created_at)::date = axis.period
)
SELECT COUNT(*) FROM tag_servers
) AS value
FROM (
SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
) as axis

View file

@ -39,6 +39,7 @@ class UserRole < ApplicationRecord
}.freeze
EVERYONE_ROLE_ID = -99
NOBODY_POSITION = -1
module Flags
NONE = 0
@ -104,7 +105,7 @@ class UserRole < ApplicationRecord
has_many :users, inverse_of: :role, foreign_key: 'role_id', dependent: :nullify
def self.nobody
@nobody ||= UserRole.new(permissions: Flags::NONE, position: -1)
@nobody ||= UserRole.new(permissions: Flags::NONE, position: NOBODY_POSITION)
end
def self.everyone
@ -173,7 +174,7 @@ class UserRole < ApplicationRecord
end
def set_position
self.position = -1 if everyone?
self.position = NOBODY_POSITION if everyone?
end
def validate_own_role_edition

View file

@ -823,6 +823,7 @@ cy:
disabled: I neb
users: I ddefnyddwyr lleol wedi'u mewngofnodi
registrations:
moderation_recommandation: Gwnewch yn siŵr bod gennych chi dîm cymedroli digonol ac adweithiol cyn i chi agor cofrestriadau i bawb!
preamble: Rheoli pwy all greu cyfrif ar eich gweinydd.
title: Cofrestriadau
registrations_mode:
@ -830,6 +831,7 @@ cy:
approved: Mae angen cymeradwyaeth i gofrestru
none: Nid oes neb yn gallu cofrestru
open: Gall unrhyw un cofrestru
warning_hint: Rydym yn argymell defnyddio “Mae angen cymeradwyaeth ar gyfer cofrestru” oni bai eich bod yn hyderus y gall eich tîm cymedroli ymdrin â chofrestriadau sbam a rhai maleisus mewn modd amserol.
security:
authorized_fetch: Mae angen dilysu gan weinyddion ffederal
authorized_fetch_hint: Mae gofyn am ddilysiad gan weinyddion ffederal yn galluogi gorfodi llymach ar flociau lefel defnyddiwr a lefel gweinydd. Fodd bynnag, daw hyn ar gost perfformiad arafach, mae'n lleihau cyrhaeddiad eich atebion, a gall gyflwyno materion cydnawsedd gyda rhai gwasanaethau ffederal. Yn ogystal, ni fydd hyn yn atal actorion ymroddedig rhag estyn eich postiadau a'ch cyfrifon cyhoeddus.
@ -1038,6 +1040,9 @@ cy:
title: Bachau Gwe
webhook: Bachyn Gwe
admin_mailer:
auto_close_registrations:
body: Oherwydd diffyg gweithgarwch cymedrolwr diweddar, mae cofrestriadau ar %{instance} wedi'u newid yn awtomatig i'r angen i gael adolygiad â llaw, er mwyn atal %{instance} rhag cael ei ddefnyddio fel llwyfan ar gyfer actorion drwg posibl. Gallwch ei newid yn ôl i agor cofrestriadau ar unrhyw adeg.
subject: Mae cofrestriadau ar gyfer %{instance} wedi'u newid yn awtomatig i'r angen i gael cymeradwyaeth
new_appeal:
actions:
delete_statuses: i ddileu eu postiadau

View file

@ -769,6 +769,7 @@ is:
disabled: Til engra
users: Til innskráðra staðværra notenda
registrations:
moderation_recommandation: Tryggðu að þú hafir hæft og aðgengilegt umsjónarteymi til taks áður en þú opnar á skráningar fyrir alla!
preamble: Stýrðu því hverjir geta útbúið notandaaðgang á netþjóninum þínum.
title: Nýskráningar
registrations_mode:
@ -776,6 +777,7 @@ is:
approved: Krafist er samþykkt nýskráningar
none: Enginn getur nýskráð sig
open: Allir geta nýskráð sig
warning_hint: Við mælum með því að nota "Krafist er samþykkt nýskráningar" nema ef þú sért viss um að umsjónarteymið þitt geti brugðist tímanlega við ruslpósti og skráningum í misjöfnum tilgangi.
security:
authorized_fetch: Krefjast auðkenningar frá netþjónum í skýjasambandi
authorized_fetch_hint: Að krefjast auðkenningar frá netþjónum í skýjasambandi kallar fram strangari útfærslu á útilokunum, bæði varðandi notendur og netþjóna. Hins vegar kemur þetta niður á afköstum, minnkar útbreiðslu á svörum þínum og gæti valdið samhæfnivandamálum við sumar sambandsþjónustur. Að auki kemur þetta ekki í veg fyrir að aðilar sem ætla sér að ná í opinberar færslur og notendaaðganga frá þér geri það.
@ -968,6 +970,9 @@ is:
title: Webhook-vefkrækjur
webhook: Webhook-vefkrækja
admin_mailer:
auto_close_registrations:
body: Vegna skorts á virkni umsjónaraðila að undanförnu, hafa nýskráningar á %{instance} sjálfkrafa verið stilltar á að þarfnast samþykktar umsjónaraðila, til að koma í veg fyrir að %{instance} verði mögulega misnotað af óprúttnum aðilum. Þú getur skipt hvenær sem er aftur yfir í opnar nýskráningar.
subject: Nýskráningar á %{instance} hafa sjálfkrafa verið stilltar á að krefjast samþykktar
new_appeal:
actions:
delete_statuses: að eyða færslum viðkomandi

View file

@ -319,7 +319,7 @@ lt:
guide_link_text: Visi gali prisidėti.
application_mailer:
notification_preferences: Keisti el pašto parinktis
settings: 'Keisti el pašto parinktis: %{link}'
settings: 'Keisti el. pašto nuostatas: %{link}'
view: 'Peržiūra:'
view_profile: Peržiurėti profilį
view_status: Peržiūrėti statusą

View file

@ -39,12 +39,14 @@ cy:
text: Dim ond unwaith y gallwch apelio yn erbyn rhybudd
defaults:
autofollow: Bydd pobl sy'n cofrestru drwy'r gwahoddiad yn eich dilyn yn awtomatig
avatar: WEBP, PNG, GIF neu JPG. %{size} ar y mwyaf. Bydd yn cael ei leihau i %{dimensions}px
bot: Mae'r cyfrif hwn yn perfformio gweithredoedd awtomatig yn bennaf ac mae'n bosib nad yw'n cael ei fonitro
context: Un neu fwy cyd-destun lle dylai'r hidlydd weithio
current_password: At ddibenion diogelwch, nodwch gyfrinair y cyfrif cyfredol
current_username: I gadarnhau, nodwch enw defnyddiwr y cyfrif cyfredol
digest: Ond yn cael eu hanfon ar ôl cyfnod hir o anweithgarwch ac ond os ydych wedi derbyn unrhyw negeseuon personol yn eich absenoldeb
email: Byddwch yn derbyn e-bost cadarnhau
header: WEBP, PNG, GIF neu JPG. %{size} ar y mwyaf. Bydd yn cael ei leihau i %{dimensions}px
inbox_url: Copïwch yr URL o dudalen flaen y relái yr ydych am ei ddefnyddio
irreversible: Bydd postiadau wedi'u hidlo'n diflannu'n ddiwrthdro, hyd yn oed os caiff yr hidlydd ei dynnu'n ddiweddarach
locale: Iaith y rhyngwyneb, e-byst a hysbysiadau gwthiadwy

View file

@ -39,12 +39,14 @@ is:
text: Þú getur aðeins áfrýjað refsingu einu sinni
defaults:
autofollow: Fólk sem skráir sig í gegnum boðið mun sjálfkrafa fylgjast með þér
avatar: WEBP, PNG, GIF eða JPG. Mest %{size}. Verður smækkað í %{dimensions}px
bot: Þessi aðgangur er aðallega til að framkvæma sjálfvirkar aðgerðir og gæti verið án þess að hann sé vaktaður reglulega
context: Eitt eða fleiri samhengi þar sem sían ætti að gilda
current_password: Í öryggisskyni skaltu setja inn lykilorðið fyrir þennan notandaaðgang
current_username: Til að staðfesta skaltu setja inn notandanafnið fyrir þennan notandaaðgang
digest: Er aðeins sent eftir lengri tímabil án virkni og þá aðeins ef þú hefur fengið persónuleg skilaboð á meðan þú hefur ekki verið á línunni
email: Þú munt fá sendan staðfestingarpóst
header: WEBP, PNG, GIF eða JPG. Mest %{size}. Verður smækkað í %{dimensions}px
inbox_url: Afritaðu slóðina af forsíðu endurvarpans sem þú vilt nota
irreversible: Síaðar færslur munu hverfa óendurkræft, jafnvel þó sían sé seinna fjarlægð
locale: Tungumál notandaviðmótsins, tölvupósts og ýti-tilkynninga

View file

@ -39,14 +39,14 @@ nl:
text: Je kunt maar eenmalig bezwaar indienen tegen een vastgestelde overtreding
defaults:
autofollow: Mensen die zich via de uitnodiging hebben geregistreerd, volgen jou automatisch
avatar: WEBP, PNG, GIF of JPG. Hoogstens %{size}. Wordt verkleind naar %{dimensions}px
avatar: WEBP, PNG, GIF of JPG. Maximaal %{size}. Wordt verkleind naar %{dimensions}px
bot: Signaal aan anderen dat het account voornamelijk geautomatiseerde acties uitvoert en mogelijk niet wordt gecontroleerd
context: Een of meerdere locaties waar de filter actief moet zijn
current_password: Voer voor veiligheidsredenen het wachtwoord van je huidige account in
current_username: Voer ter bevestiging de gebruikersnaam van je huidige account in
digest: Wordt alleen na een lange periode van inactiviteit verzonden en alleen wanneer je tijdens jouw afwezigheid persoonlijke berichten hebt ontvangen
email: Je krijgt een bevestigingsmail
header: WEBP, PNG, GIF of JPG. Hoogstens %{size}. Wordt verkleind naar %{dimensions}px
header: WEBP, PNG, GIF of JPG. Maximaal %{size}. Wordt verkleind naar %{dimensions}px
inbox_url: Kopieer de URL van de voorpagina van de relayserver die je wil gebruiken
irreversible: Gefilterde berichten verdwijnen onomkeerbaar, zelfs als de filter later wordt verwijderd
locale: De taal van de gebruikersomgeving, e-mails en pushmeldingen

View file

@ -90,7 +90,7 @@ RSpec.describe ActivityPub::RepliesController do
context 'when there are few self-replies' do
it 'points next to replies from other people' do
expect(page_json).to be_a Hash
expect(Addressable::URI.parse(page_json[:next]).query.split('&')).to include('only_other_accounts=true', 'page=true')
expect(parsed_uri_query_values(page_json[:next])).to include('only_other_accounts=true', 'page=true')
end
end
@ -101,7 +101,7 @@ RSpec.describe ActivityPub::RepliesController do
it 'points next to other self-replies' do
expect(page_json).to be_a Hash
expect(Addressable::URI.parse(page_json[:next]).query.split('&')).to include('only_other_accounts=false', 'page=true')
expect(parsed_uri_query_values(page_json[:next])).to include('only_other_accounts=false', 'page=true')
end
end
end
@ -140,7 +140,7 @@ RSpec.describe ActivityPub::RepliesController do
it 'points next to other replies' do
expect(page_json).to be_a Hash
expect(Addressable::URI.parse(page_json[:next]).query.split('&')).to include('only_other_accounts=true', 'page=true')
expect(parsed_uri_query_values(page_json[:next])).to include('only_other_accounts=true', 'page=true')
end
end
end
@ -196,6 +196,13 @@ RSpec.describe ActivityPub::RepliesController do
private
def parsed_uri_query_values(uri)
Addressable::URI
.parse(uri)
.query
.split('&')
end
def ap_public_collection
ActivityPub::TagManager::COLLECTIONS[:public]
end

View file

@ -18,7 +18,8 @@ describe Api::V1::Accounts::StatusesController do
get :index, params: { account_id: user.account.id, limit: 1 }
expect(response).to have_http_status(200)
expect(response.headers['Link'].links.size).to eq(2)
expect(links_from_header.size)
.to eq(2)
end
context 'with only media' do
@ -55,10 +56,45 @@ describe Api::V1::Accounts::StatusesController do
Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account))
end
it 'returns http success' do
it 'returns http success and includes a header link' do
get :index, params: { account_id: user.account.id, pinned: true }
expect(response).to have_http_status(200)
expect(links_from_header.size)
.to eq(1)
expect(links_from_header)
.to contain_exactly(
have_attributes(
href: /pinned=true/,
attr_pairs: contain_exactly(['rel', 'prev'])
)
)
end
end
context 'with enough pinned statuses to paginate' do
before do
stub_const 'Api::BaseController::DEFAULT_STATUSES_LIMIT', 1
2.times { Fabricate(:status_pin, account: user.account) }
end
it 'returns http success and header pagination links to prev and next' do
get :index, params: { account_id: user.account.id, pinned: true }
expect(response).to have_http_status(200)
expect(links_from_header.size)
.to eq(2)
expect(links_from_header)
.to contain_exactly(
have_attributes(
href: /pinned=true/,
attr_pairs: contain_exactly(['rel', 'next'])
),
have_attributes(
href: /pinned=true/,
attr_pairs: contain_exactly(['rel', 'prev'])
)
)
end
end
@ -98,4 +134,12 @@ describe Api::V1::Accounts::StatusesController do
end
end
end
private
def links_from_header
response
.headers['Link']
.links
end
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:software_update) do
version '99.99.99'
version { sequence(:version) { |point| "99.99.#{point}" } }
urgent false
type 'patch'
end

View file

@ -139,7 +139,7 @@ RSpec.describe UserRole do
end
it 'has negative position' do
expect(subject.position).to eq(-1)
expect(subject.position).to eq(described_class::NOBODY_POSITION)
end
end
@ -159,7 +159,7 @@ RSpec.describe UserRole do
end
it 'has negative position' do
expect(subject.position).to eq(-1)
expect(subject.position).to eq(described_class::NOBODY_POSITION)
end
end

View file

@ -42,7 +42,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5":
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.23.5":
version: 7.23.5
resolution: "@babel/code-frame@npm:7.23.5"
dependencies:
@ -373,15 +373,6 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.22.15":
version: 7.23.6
resolution: "@babel/parser@npm:7.23.6"
bin:
parser: ./bin/babel-parser.js
checksum: 10c0/6f76cd5ccae1fa9bcab3525b0865c6222e9c1d22f87abc69f28c5c7b2c8816a13361f5bd06bddbd5faf903f7320a8feba02545c981468acec45d12a03db7755e
languageName: node
linkType: hard
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3":
version: 7.23.3
resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3"
@ -1500,18 +1491,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3":
version: 7.22.15
resolution: "@babel/template@npm:7.22.15"
dependencies:
"@babel/code-frame": "npm:^7.22.13"
"@babel/parser": "npm:^7.22.15"
"@babel/types": "npm:^7.22.15"
checksum: 10c0/9312edd37cf1311d738907003f2aa321a88a42ba223c69209abe4d7111db019d321805504f606c7fd75f21c6cf9d24d0a8223104cd21ebd207e241b6c551f454
languageName: node
linkType: hard
"@babel/template@npm:^7.23.9":
"@babel/template@npm:^7.22.15, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3":
version: 7.23.9
resolution: "@babel/template@npm:7.23.9"
dependencies:
@ -1540,18 +1520,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.23.6
resolution: "@babel/types@npm:7.23.6"
dependencies:
"@babel/helper-string-parser": "npm:^7.23.4"
"@babel/helper-validator-identifier": "npm:^7.22.20"
to-fast-properties: "npm:^2.0.0"
checksum: 10c0/42cefce8a68bd09bb5828b4764aa5586c53c60128ac2ac012e23858e1c179347a4aac9c66fc577994fbf57595227611c5ec8270bf0cfc94ff033bbfac0550b70
languageName: node
linkType: hard
"@babel/types@npm:^7.23.9":
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.23.9
resolution: "@babel/types@npm:7.23.9"
dependencies:
@ -5036,12 +5005,12 @@ __metadata:
languageName: node
linkType: hard
"body-parser@npm:1.20.1":
version: 1.20.1
resolution: "body-parser@npm:1.20.1"
"body-parser@npm:1.20.2":
version: 1.20.2
resolution: "body-parser@npm:1.20.2"
dependencies:
bytes: "npm:3.1.2"
content-type: "npm:~1.0.4"
content-type: "npm:~1.0.5"
debug: "npm:2.6.9"
depd: "npm:2.0.0"
destroy: "npm:1.2.0"
@ -5049,10 +5018,10 @@ __metadata:
iconv-lite: "npm:0.4.24"
on-finished: "npm:2.4.1"
qs: "npm:6.11.0"
raw-body: "npm:2.5.1"
raw-body: "npm:2.5.2"
type-is: "npm:~1.6.18"
unpipe: "npm:1.0.0"
checksum: 10c0/a202d493e2c10a33fb7413dac7d2f713be579c4b88343cd814b6df7a38e5af1901fc31044e04de176db56b16d9772aa25a7723f64478c20f4d91b1ac223bf3b8
checksum: 10c0/06f1438fff388a2e2354c96aa3ea8147b79bfcb1262dfcc2aae68ec13723d01d5781680657b74e9f83c808266d5baf52804032fbde2b7382b89bd8cdb273ace9
languageName: node
linkType: hard
@ -5882,7 +5851,7 @@ __metadata:
languageName: node
linkType: hard
"content-type@npm:~1.0.4":
"content-type@npm:~1.0.4, content-type@npm:~1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af
@ -7757,12 +7726,12 @@ __metadata:
linkType: hard
"express@npm:^4.17.1, express@npm:^4.18.2":
version: 4.18.2
resolution: "express@npm:4.18.2"
version: 4.18.3
resolution: "express@npm:4.18.3"
dependencies:
accepts: "npm:~1.3.8"
array-flatten: "npm:1.1.1"
body-parser: "npm:1.20.1"
body-parser: "npm:1.20.2"
content-disposition: "npm:0.5.4"
content-type: "npm:~1.0.4"
cookie: "npm:0.5.0"
@ -7791,7 +7760,7 @@ __metadata:
type-is: "npm:~1.6.18"
utils-merge: "npm:1.0.1"
vary: "npm:~1.1.2"
checksum: 10c0/75af556306b9241bc1d7bdd40c9744b516c38ce50ae3210658efcbf96e3aed4ab83b3432f06215eae5610c123bc4136957dc06e50dfc50b7d4d775af56c4c59c
checksum: 10c0/0b9eeafbac549e3c67d92d083bf1773e358359f41ad142b92121935c6348d29079b75054555b3f62de39263fffc8ba06898b09fdd3e213e28e714c03c5d9f44c
languageName: node
linkType: hard
@ -13436,15 +13405,15 @@ __metadata:
languageName: node
linkType: hard
"raw-body@npm:2.5.1":
version: 2.5.1
resolution: "raw-body@npm:2.5.1"
"raw-body@npm:2.5.2":
version: 2.5.2
resolution: "raw-body@npm:2.5.2"
dependencies:
bytes: "npm:3.1.2"
http-errors: "npm:2.0.0"
iconv-lite: "npm:0.4.24"
unpipe: "npm:1.0.0"
checksum: 10c0/5dad5a3a64a023b894ad7ab4e5c7c1ce34d3497fc7138d02f8c88a3781e68d8a55aa7d4fd3a458616fa8647cc228be314a1c03fb430a07521de78b32c4dd09d2
checksum: 10c0/b201c4b66049369a60e766318caff5cb3cc5a900efd89bdac431463822d976ad0670912c931fdbdcf5543207daf6f6833bca57aa116e1661d2ea91e12ca692c4
languageName: node
linkType: hard