Merge tag 'v4.3.0-rc.1'

This commit is contained in:
Mike Barnes 2024-10-02 10:34:27 +10:00
commit 26c9b9ba39
3459 changed files with 130932 additions and 69993 deletions

View file

@ -0,0 +1,3 @@
const ReactComponent = 'div';
export default ReactComponent;

View file

@ -0,0 +1,368 @@
import './public-path';
import { createRoot } from 'react-dom/client';
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);
},
);
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(() => {
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);
}
document.querySelectorAll('[data-admin-component]').forEach((element) => {
void mountReactComponent(element);
});
}).catch((reason: unknown) => {
throw reason;
});

View file

@ -1,5 +1,5 @@
import './public-path';
import main from "mastodon/main"
import main from 'mastodon/main';
import { start } from '../mastodon/common';
import { loadLocale } from '../mastodon/locales';
@ -10,6 +10,6 @@ start();
loadPolyfills()
.then(loadLocale)
.then(main)
.catch(e => {
.catch((e: unknown) => {
console.error(e);
});

View file

@ -0,0 +1,74 @@
import './public-path';
import { createRoot } from 'react-dom/client';
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
import { start } from '../mastodon/common';
import { Status } from '../mastodon/features/standalone/status';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
start();
function loaded() {
const mountNode = document.getElementById('mastodon-status');
if (mountNode) {
const attr = mountNode.getAttribute('data-props');
if (!attr) return;
const props = JSON.parse(attr) as { id: string; locale: string };
const root = createRoot(mountNode);
root.render(<Status {...props} />);
}
}
function main() {
ready(loaded).catch((error: unknown) => {
console.error(error);
});
}
loadPolyfills()
.then(main)
.catch((error: unknown) => {
console.error(error);
});
interface SetHeightMessage {
type: 'setHeight';
id: string;
height: number;
}
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
if (
data &&
typeof data === 'object' &&
'type' in data &&
data.type === 'setHeight'
)
return true;
else return false;
}
window.addEventListener('message', (e) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
const data = e.data;
// We use a timeout to allow for the React page to render before calculating the height
afterInitialRender(() => {
window.parent.postMessage(
{
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0]?.scrollHeight,
},
'*',
);
});
});

View file

@ -2,7 +2,9 @@ import './public-path';
import ready from '../mastodon/ready';
ready(() => {
const image = document.querySelector('img');
const image = document.querySelector<HTMLImageElement>('img');
if (!image) return;
image.addEventListener('mouseenter', () => {
image.src = '/oops.gif';
@ -11,4 +13,6 @@ ready(() => {
image.addEventListener('mouseleave', () => {
image.src = '/oops.png';
});
}).catch((e: unknown) => {
console.error(e);
});

View file

@ -0,0 +1,4 @@
/* Placeholder file to have `inert.scss` compiled by Webpack
This is used by the `wicg-inert` polyfill */
import '../styles/inert.scss';

View file

@ -2,7 +2,7 @@
// to share the same assets regardless of instance configuration.
// See https://webpack.js.org/guides/public-path/#on-the-fly
function removeOuterSlashes(string) {
function removeOuterSlashes(string: string) {
return string.replace(/^\/*/, '').replace(/\/*$/, '');
}
@ -15,7 +15,9 @@ function formatPublicPath(host = '', path = '') {
return `${formattedHost}/${formattedPath}/`;
}
const cdnHost = document.querySelector('meta[name=cdn-host]');
const cdnHost = document.querySelector<HTMLMetaElement>('meta[name=cdn-host]');
// eslint-disable-next-line no-undef
__webpack_public_path__ = formatPublicPath(cdnHost ? cdnHost.content : '', process.env.PUBLIC_OUTPUT_PATH);
__webpack_public_path__ = formatPublicPath(
cdnHost ? cdnHost.content : '',
process.env.PUBLIC_OUTPUT_PATH,
);

View file

@ -0,0 +1,461 @@
import { createRoot } from 'react-dom/client';
import './public-path';
import { IntlMessageFormat } from 'intl-messageformat';
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
import { defineMessages } from 'react-intl';
import Rails from '@rails/ujs';
import axios from 'axios';
import { throttle } from 'lodash';
import { start } from '../mastodon/common';
import { timeAgoString } from '../mastodon/components/relative_timestamp';
import emojify from '../mastodon/features/emoji/emoji';
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
import { loadLocale, getLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
import 'cocoon-js-vanilla';
start();
const messages = defineMessages({
usernameTaken: {
id: 'username.taken',
defaultMessage: 'That username is taken. Try another',
},
passwordExceedsLength: {
id: 'password_confirmation.exceeds_maxlength',
defaultMessage: 'Password confirmation exceeds the maximum password length',
},
passwordDoesNotMatch: {
id: 'password_confirmation.mismatching',
defaultMessage: 'Password confirmation does not match',
},
});
function loaded() {
const { messages: localeData } = getLocale();
const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const dateFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
});
const formatMessage = (
{ id, defaultMessage }: MessageDescriptor,
values?: Record<string, PrimitiveType>,
) => {
let message: string | undefined = undefined;
if (id) message = localeData[id];
if (!message) message = defaultMessage as string;
const messageFormat = new IntlMessageFormat(message, locale);
return messageFormat.format(values) as string;
};
document.querySelectorAll('.emojify').forEach((content) => {
content.innerHTML = emojify(content.innerHTML);
});
document
.querySelectorAll<HTMLTimeElement>('time.formatted')
.forEach((content) => {
const datetime = new Date(content.dateTime);
const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate;
content.textContent = formattedDate;
});
const isToday = (date: Date) => {
const today = new Date();
return (
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()
);
};
const todayFormat = new IntlMessageFormat(
localeData['relative_format.today'] ?? 'Today at {time}',
locale,
);
document
.querySelectorAll<HTMLTimeElement>('time.relative-formatted')
.forEach((content) => {
const datetime = new Date(content.dateTime);
let formattedContent: string;
if (isToday(datetime)) {
const formattedTime = timeFormat.format(datetime);
formattedContent = todayFormat.format({
time: formattedTime,
}) as string;
} else {
formattedContent = dateFormat.format(datetime);
}
content.title = formattedContent;
content.textContent = formattedContent;
});
document
.querySelectorAll<HTMLTimeElement>('time.time-ago')
.forEach((content) => {
const datetime = new Date(content.dateTime);
const now = new Date();
const timeGiven = content.dateTime.includes('T');
content.title = timeGiven
? dateTimeFormat.format(datetime)
: dateFormat.format(datetime);
content.textContent = timeAgoString(
{
formatMessage,
formatDate: (date: Date, options) =>
new Intl.DateTimeFormat(locale, options).format(date),
},
datetime,
now.getTime(),
now.getFullYear(),
timeGiven,
);
});
const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) {
import(
/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container'
)
.then(({ default: MediaContainer }) => {
reactComponents.forEach((component) => {
Array.from(component.children).forEach((child) => {
component.removeChild(child);
});
});
const content = document.createElement('div');
const root = createRoot(content);
root.render(
<MediaContainer locale={locale} components={reactComponents} />,
);
document.body.appendChild(content);
return true;
})
.catch((error: unknown) => {
console.error(error);
});
}
Rails.delegate(
document,
'input#user_account_attributes_username',
'input',
throttle(
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
if (target.value && target.value.length > 0) {
axios
.get('/api/v1/accounts/lookup', { params: { acct: target.value } })
.then(() => {
target.setCustomValidity(formatMessage(messages.usernameTaken));
return true;
})
.catch(() => {
target.setCustomValidity('');
});
} else {
target.setCustomValidity('');
}
},
500,
{ leading: false, trailing: true },
),
);
Rails.delegate(
document,
'#user_password,#user_password_confirmation',
'input',
() => {
const password = document.querySelector<HTMLInputElement>(
'input#user_password',
);
const confirmation = document.querySelector<HTMLInputElement>(
'input#user_password_confirmation',
);
if (!confirmation || !password) return;
if (
confirmation.value &&
confirmation.value.length > password.maxLength
) {
confirmation.setCustomValidity(
formatMessage(messages.passwordExceedsLength),
);
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(
formatMessage(messages.passwordDoesNotMatch),
);
} else {
confirmation.setCustomValidity('');
}
},
);
Rails.delegate(
document,
'button.status__content__spoiler-link',
'click',
function () {
if (!(this instanceof HTMLButtonElement)) return;
const statusEl = this.parentNode?.parentNode;
if (
!(
statusEl instanceof HTMLDivElement &&
statusEl.classList.contains('.status__content')
)
)
return;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = new IntlMessageFormat(
localeData['status.show_more'] ?? 'Show more',
locale,
).format() as string;
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = new IntlMessageFormat(
localeData['status.show_less'] ?? 'Show less',
locale,
).format() as string;
}
},
);
document
.querySelectorAll<HTMLButtonElement>('button.status__content__spoiler-link')
.forEach((spoilerLink) => {
const statusEl = spoilerLink.parentNode?.parentNode;
if (
!(
statusEl instanceof HTMLDivElement &&
statusEl.classList.contains('.status__content')
)
)
return;
const message =
statusEl.dataset.spoiler === 'expanded'
? (localeData['status.show_less'] ?? 'Show less')
: (localeData['status.show_more'] ?? 'Show more');
spoilerLink.textContent = new IntlMessageFormat(
message,
locale,
).format() as string;
});
}
Rails.delegate(
document,
'#edit_profile input[type=file]',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const avatar = document.querySelector<HTMLImageElement>(
`img#${target.id}-preview`,
);
if (!avatar) return;
let file: File | undefined;
if (target.files) file = target.files[0];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
if (url) avatar.src = url;
},
);
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!(target instanceof HTMLButtonElement)) return;
const input = target.parentNode?.querySelector<HTMLInputElement>(
'.input-copy__wrapper input',
);
if (!input) return;
const oldReadOnly = input.readOnly;
input.readOnly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
const parent = target.parentElement;
if (!parent) return;
parent.classList.add('copied');
setTimeout(() => {
parent.classList.remove('copied');
}, 700);
}
} catch (err) {
console.error(err);
}
input.readOnly = oldReadOnly;
});
const toggleSidebar = () => {
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
const toggleButton = document.querySelector<HTMLAnchorElement>(
'a.sidebar__toggle__icon',
);
if (!sidebar || !toggleButton) return;
if (sidebar.classList.contains('visible')) {
document.body.style.overflow = '';
toggleButton.setAttribute('aria-expanded', 'false');
} else {
document.body.style.overflow = 'hidden';
toggleButton.setAttribute('aria-expanded', 'true');
}
toggleButton.classList.toggle('active');
sidebar.classList.toggle('visible');
};
Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
toggleSidebar();
});
Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleSidebar();
}
});
Rails.delegate(document, 'img.custom-emoji', 'mouseover', ({ target }) => {
if (target instanceof HTMLImageElement && target.dataset.original)
target.src = target.dataset.original;
});
Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => {
if (target instanceof HTMLImageElement && target.dataset.static)
target.src = target.dataset.static;
});
const setInputDisabled = (
input: HTMLInputElement | HTMLSelectElement,
disabled: boolean,
) => {
input.disabled = disabled;
const wrapper = input.closest('.with_label');
if (wrapper) {
wrapper.classList.toggle('disabled', input.disabled);
const hidden =
input.type === 'checkbox' &&
wrapper.querySelector<HTMLInputElement>('input[type=hidden][value="0"]');
if (hidden) {
hidden.disabled = input.disabled;
}
}
};
Rails.delegate(
document,
'#account_statuses_cleanup_policy_enabled',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement) || !target.form) return;
target.form
.querySelectorAll<
HTMLInputElement | HTMLSelectElement
>('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select')
.forEach((input) => {
setInputDisabled(input, !target.checked);
});
},
);
// Empty the honeypot fields in JS in case something like an extension
// automatically filled them.
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
[
'user_website',
'user_confirm_password',
'registration_user_website',
'registration_user_confirm_password',
].forEach((id) => {
const field = document.querySelector<HTMLInputElement>(`input#${id}`);
if (field) {
field.value = '';
}
});
});
function main() {
ready(loaded).catch((error: unknown) => {
console.error(error);
});
}
loadPolyfills()
.then(loadLocale)
.then(main)
.then(loadKeyboardExtensions)
.catch((error: unknown) => {
console.error(error);
});

View file

@ -67,7 +67,9 @@ const fetchInteractionURLFailure = () => {
);
};
const isValidDomain = (value: string) => {
const isValidDomain = (value: unknown) => {
if (typeof value !== 'string') return false;
const url = new URL('https:///path');
url.hostname = value;
return url.hostname === value;
@ -124,6 +126,11 @@ const fromAcct = (acct: string) => {
const domain = segments[1];
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
if (!domain) {
fetchInteractionURLFailure();
return;
}
axios
.get(`https://${domain}/.well-known/webfinger`, {
params: { resource: `acct:${acct}` },

View file

@ -2,7 +2,7 @@ import './public-path';
import { createRoot } from 'react-dom/client';
import { start } from '../mastodon/common';
import ComposeContainer from '../mastodon/containers/compose_container';
import ComposeContainer from '../mastodon/containers/compose_container';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
@ -13,18 +13,24 @@ function loaded() {
if (mountNode) {
const attr = mountNode.getAttribute('data-props');
if(!attr) return;
const props = JSON.parse(attr);
if (!attr) return;
const props = JSON.parse(attr) as object;
const root = createRoot(mountNode);
root.render(<ComposeContainer {...props} />);
}
}
function main() {
ready(loaded);
ready(loaded).catch((error: unknown) => {
console.error(error);
});
}
loadPolyfills().then(main).catch(error => {
console.error(error);
});
loadPolyfills()
.then(main)
.catch((error: unknown) => {
console.error(error);
});

View file

@ -0,0 +1,48 @@
import './public-path';
import axios from 'axios';
import ready from '../mastodon/ready';
async function checkConfirmation() {
const response = await axios.get('/api/v1/emails/check_confirmation');
if (response.data) {
window.location.href = '/start';
}
}
ready(() => {
setInterval(() => {
void checkConfirmation();
}, 5000);
document
.querySelectorAll<HTMLButtonElement>('button.timer-button')
.forEach((button) => {
let counter = 30;
const container = document.createElement('span');
const updateCounter = () => {
container.innerText = ` (${counter})`;
};
updateCounter();
const countdown = setInterval(() => {
counter--;
if (counter === 0) {
button.disabled = false;
button.removeChild(container);
clearInterval(countdown);
} else {
updateCounter();
}
}, 1000);
button.appendChild(container);
});
}).catch((e: unknown) => {
throw e;
});

View file

@ -0,0 +1,197 @@
import * as WebAuthnJSON from '@github/webauthn-json';
import axios, { AxiosError } from 'axios';
import ready from '../mastodon/ready';
import 'regenerator-runtime/runtime';
type PublicKeyCredentialCreationOptionsJSON =
WebAuthnJSON.CredentialCreationOptionsJSON['publicKey'];
function exceptionHasAxiosError(
error: unknown,
): error is AxiosError<{ error: unknown }> {
return (
error instanceof AxiosError &&
typeof error.response?.data === 'object' &&
'error' in error.response.data
);
}
function logAxiosResponseError(error: unknown) {
if (exceptionHasAxiosError(error)) console.error(error);
}
function getCSRFToken() {
return document
.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')
?.getAttribute('content');
}
function hideFlashMessages() {
document.querySelectorAll('.flash-message').forEach((flashMessage) => {
flashMessage.classList.add('hidden');
});
}
async function callback(
url: string,
body:
| {
credential: WebAuthnJSON.PublicKeyCredentialWithAttestationJSON;
nickname: string;
}
| {
user: { credential: WebAuthnJSON.PublicKeyCredentialWithAssertionJSON };
},
) {
try {
const response = await axios.post<{ redirect_path: string }>(
url,
JSON.stringify(body),
{
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-Token': getCSRFToken(),
},
},
);
window.location.replace(response.data.redirect_path);
} catch (error) {
if (error instanceof AxiosError && error.response?.status === 422) {
const errorMessage = document.getElementById(
'security-key-error-message',
);
errorMessage?.classList.remove('hidden');
logAxiosResponseError(error);
} else {
console.error(error);
}
}
}
async function handleWebauthnCredentialRegistration(nickname: string) {
try {
const response = await axios.get<PublicKeyCredentialCreationOptionsJSON>(
'/settings/security_keys/options',
);
const credentialOptions = response.data;
try {
const credential = await WebAuthnJSON.create({
publicKey: credentialOptions,
});
const params = {
credential: credential,
nickname: nickname,
};
await callback('/settings/security_keys', params);
} catch (error) {
const errorMessage = document.getElementById(
'security-key-error-message',
);
errorMessage?.classList.remove('hidden');
console.error(error);
}
} catch (error) {
logAxiosResponseError(error);
}
}
async function handleWebauthnCredentialAuthentication() {
try {
const response = await axios.get<PublicKeyCredentialCreationOptionsJSON>(
'sessions/security_key_options',
);
const credentialOptions = response.data;
try {
const credential = await WebAuthnJSON.get({
publicKey: credentialOptions,
});
const params = { user: { credential: credential } };
void callback('sign_in', params);
} catch (error) {
const errorMessage = document.getElementById(
'security-key-error-message',
);
errorMessage?.classList.remove('hidden');
console.error(error);
}
} catch (error) {
logAxiosResponseError(error);
}
}
ready(() => {
if (!WebAuthnJSON.supported()) {
const unsupported_browser_message = document.getElementById(
'unsupported-browser-message',
);
if (unsupported_browser_message) {
unsupported_browser_message.classList.remove('hidden');
const button = document.querySelector<HTMLButtonElement>(
'button.btn.js-webauthn',
);
if (button) button.disabled = true;
}
}
const webAuthnCredentialRegistrationForm =
document.querySelector<HTMLFormElement>('form#new_webauthn_credential');
if (webAuthnCredentialRegistrationForm) {
webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => {
event.preventDefault();
if (!(event.target instanceof HTMLFormElement)) return;
const nickname = event.target.querySelector<HTMLInputElement>(
'input[name="new_webauthn_credential[nickname]"]',
);
if (nickname?.value) {
void handleWebauthnCredentialRegistration(nickname.value);
} else {
nickname?.focus();
}
});
}
const webAuthnCredentialAuthenticationForm =
document.getElementById('webauthn-form');
if (webAuthnCredentialAuthenticationForm) {
webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => {
event.preventDefault();
void handleWebauthnCredentialAuthentication();
});
const otpAuthenticationForm = document.getElementById(
'otp-authentication-form',
);
const linkToOtp = document.getElementById('link-to-otp');
linkToOtp?.addEventListener('click', () => {
webAuthnCredentialAuthenticationForm.classList.add('hidden');
otpAuthenticationForm?.classList.remove('hidden');
hideFlashMessages();
});
const linkToWebAuthn = document.getElementById('link-to-webauthn');
linkToWebAuthn?.addEventListener('click', () => {
otpAuthenticationForm?.classList.add('hidden');
webAuthnCredentialAuthenticationForm.classList.remove('hidden');
hideFlashMessages();
});
}
}).catch((e: unknown) => {
throw e;
});

View file

@ -0,0 +1,61 @@
import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/store';
const isMentionClick = (element: HTMLAnchorElement) =>
element.classList.contains('mention');
const isHashtagClick = (element: HTMLAnchorElement) =>
element.textContent?.[0] === '#' ||
element.previousSibling?.textContent?.endsWith('#');
export const useLinks = () => {
const history = useHistory();
const dispatch = useAppDispatch();
const handleHashtagClick = useCallback(
(element: HTMLAnchorElement) => {
const { textContent } = element;
if (!textContent) return;
history.push(`/tags/${textContent.replace(/^#/, '')}`);
},
[history],
);
const handleMentionClick = useCallback(
(element: HTMLAnchorElement) => {
dispatch(
openURL(element.href, history, () => {
window.location.href = element.href;
}),
);
},
[dispatch, history],
);
const handleClick = useCallback(
(e: React.MouseEvent) => {
const target = (e.target as HTMLElement).closest('a');
if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
return;
}
if (isMentionClick(target)) {
e.preventDefault();
handleMentionClick(target);
} else if (isHashtagClick(target)) {
e.preventDefault();
handleHashtagClick(target);
}
},
[handleMentionClick, handleHashtagClick],
);
return handleClick;
};

View file

@ -0,0 +1,32 @@
// This hook allows a component to signal that it's done rendering in a way that
// can be used by e.g. our embed code to determine correct iframe height
let renderSignalReceived = false;
type Callback = () => void;
let onInitialRender: Callback;
export const afterInitialRender = (callback: Callback) => {
if (renderSignalReceived) {
callback();
} else {
onInitialRender = callback;
}
};
export const useRenderSignal = () => {
return () => {
if (renderSignalReceived) {
return;
}
renderSignalReceived = true;
if (typeof onInitialRender !== 'undefined') {
window.requestAnimationFrame(() => {
onInitialRender();
});
}
};
};

View file

@ -0,0 +1,31 @@
import { useMemo, useCallback } from 'react';
import { useLocation, useHistory } from 'react-router';
export function useSearchParams() {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
}
export function useSearchParam(name: string, defaultValue?: string) {
const searchParams = useSearchParams();
const history = useHistory();
const value = searchParams.get(name) ?? defaultValue;
const setValue = useCallback(
(value: string | null) => {
if (value === null) {
searchParams.delete(name);
} else {
searchParams.set(name, value);
}
history.push({ search: searchParams.toString() });
},
[history, name, searchParams],
);
return [value, setValue] as const;
}

View file

@ -0,0 +1,44 @@
import { useRef, useCallback, useEffect } from 'react';
export const useTimeout = () => {
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const callbackRef = useRef<() => void>();
const set = useCallback((callback: () => void, delay: number) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
callbackRef.current = callback;
timeoutRef.current = setTimeout(callback, delay);
}, []);
const delay = useCallback((delay: number) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (!callbackRef.current) {
return;
}
timeoutRef.current = setTimeout(callbackRef.current, delay);
}, []);
const cancel = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = undefined;
callbackRef.current = undefined;
}
}, []);
useEffect(
() => () => {
cancel();
},
[cancel],
);
return [set, cancel, delay] as const;
};

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" />
</svg>

After

Width:  |  Height:  |  Size: 293 B

View file

@ -0,0 +1,24 @@
<svg width="5" height="80" viewBox="0 0 5 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_954_5239)">
<rect width="5" height="80" transform="matrix(1 0 0 -1 0 80)" fill="#2F0C7A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 84C6.46234 83.9609 6.57112 83.9208 6.68172 83.8795C6.79039 83.839 7.16116 83.7 7.21197 83.681C10.0919 82.6043 11.8941 82.2 15 82.2C18.0813 82.2 19.6086 82.5665 22.3886 83.6785C22.6753 83.7932 22.9494 83.9003 23.2136 84H25.0812C24.3279 83.7808 23.5395 83.4927 22.6114 83.1215C19.7664 81.9835 18.1687 81.6 15 81.6C11.8122 81.6 9.94344 82.0192 7.00185 83.119C6.95066 83.1381 6.58016 83.277 6.47198 83.3174C5.72954 83.5944 5.0738 83.8198 4.45483 84H6.35524ZM6.35524 78C3.97726 78.8676 2.42302 79.2 0 79.2L0 78.6C1.72318 78.6 2.98542 78.4277 4.45483 78H6.35524ZM23.2136 78C25.5716 78.8899 27.1507 79.2 30 79.2V78.6C27.9473 78.6 26.5843 78.4373 25.0812 78L23.2136 78ZM0 82.2C2.53215 82.2 4.1155 81.837 6.68172 80.8795C6.79039 80.839 7.16116 80.7 7.21197 80.681C10.0919 79.6043 11.8941 79.2 15 79.2C18.0813 79.2 19.6086 79.5665 22.3886 80.6785C25.2336 81.8165 26.8313 82.2 30 82.2V81.6C26.9187 81.6 25.3914 81.2335 22.6114 80.1215C19.7664 78.9835 18.1687 78.6 15 78.6C11.8122 78.6 9.94344 79.0192 7.00185 80.119C6.95066 80.1381 6.58016 80.277 6.47198 80.3174C3.96706 81.2519 2.44905 81.6 0 81.6L0 82.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 78C6.46234 77.9609 6.57112 77.9208 6.68172 77.8795C6.79039 77.839 7.16116 77.7 7.21197 77.681C10.0919 76.6043 11.8941 76.2 15 76.2C18.0813 76.2 19.6086 76.5665 22.3886 77.6785C22.6753 77.7932 22.9494 77.9003 23.2136 78H25.0812C24.3279 77.7808 23.5395 77.4927 22.6114 77.1215C19.7664 75.9835 18.1687 75.6 15 75.6C11.8122 75.6 9.94344 76.0192 7.00185 77.119C6.95066 77.1381 6.58016 77.277 6.47198 77.3174C5.72954 77.5944 5.0738 77.8198 4.45483 78H6.35524ZM6.35524 72C3.97726 72.8676 2.42302 73.2 0 73.2L0 72.6C1.72318 72.6 2.98542 72.4277 4.45483 72H6.35524ZM23.2136 72C25.5716 72.8899 27.1507 73.2 30 73.2V72.6C27.9473 72.6 26.5843 72.4373 25.0812 72L23.2136 72ZM0 76.2C2.53215 76.2 4.1155 75.837 6.68172 74.8795C6.79039 74.839 7.16116 74.7 7.21197 74.681C10.0919 73.6043 11.8941 73.2 15 73.2C18.0813 73.2 19.6086 73.5665 22.3886 74.6785C25.2336 75.8165 26.8313 76.2 30 76.2V75.6C26.9187 75.6 25.3914 75.2335 22.6114 74.1215C19.7664 72.9835 18.1687 72.6 15 72.6C11.8122 72.6 9.94344 73.0192 7.00185 74.119C6.95066 74.1381 6.58016 74.277 6.47198 74.3174C3.96706 75.2519 2.44905 75.6 0 75.6L0 76.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 72C6.46234 71.9609 6.57112 71.9208 6.68172 71.8795C6.79039 71.839 7.16116 71.7 7.21197 71.681C10.0919 70.6043 11.8941 70.2 15 70.2C18.0813 70.2 19.6086 70.5665 22.3886 71.6785C22.6753 71.7932 22.9494 71.9003 23.2136 72H25.0812C24.3279 71.7808 23.5395 71.4927 22.6114 71.1215C19.7664 69.9835 18.1687 69.6 15 69.6C11.8122 69.6 9.94344 70.0192 7.00185 71.119C6.95066 71.1381 6.58016 71.277 6.47198 71.3174C5.72954 71.5944 5.0738 71.8198 4.45483 72H6.35524ZM6.35524 66C3.97726 66.8676 2.42302 67.2 0 67.2L0 66.6C1.72318 66.6 2.98542 66.4277 4.45483 66H6.35524ZM23.2136 66C25.5716 66.8899 27.1507 67.2 30 67.2V66.6C27.9473 66.6 26.5843 66.4373 25.0812 66L23.2136 66ZM0 70.2C2.53215 70.2 4.1155 69.837 6.68172 68.8795C6.79039 68.839 7.16116 68.7 7.21197 68.681C10.0919 67.6043 11.8941 67.2 15 67.2C18.0813 67.2 19.6086 67.5665 22.3886 68.6785C25.2336 69.8165 26.8313 70.2 30 70.2V69.6C26.9187 69.6 25.3914 69.2335 22.6114 68.1215C19.7664 66.9835 18.1687 66.6 15 66.6C11.8122 66.6 9.94344 67.0192 7.00185 68.119C6.95066 68.1381 6.58016 68.277 6.47198 68.3174C3.96706 69.2519 2.44905 69.6 0 69.6L0 70.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 66C6.46234 65.9609 6.57112 65.9208 6.68172 65.8795C6.79039 65.839 7.16116 65.7 7.21197 65.681C10.0919 64.6043 11.8941 64.2 15 64.2C18.0813 64.2 19.6086 64.5665 22.3886 65.6785C22.6753 65.7932 22.9494 65.9003 23.2136 66H25.0812C24.3279 65.7808 23.5395 65.4927 22.6114 65.1215C19.7664 63.9835 18.1687 63.6 15 63.6C11.8122 63.6 9.94344 64.0192 7.00185 65.119C6.95066 65.1381 6.58016 65.277 6.47198 65.3174C5.72954 65.5944 5.0738 65.8198 4.45483 66H6.35524ZM6.35524 60C3.97726 60.8676 2.42302 61.2 0 61.2L0 60.6C1.72318 60.6 2.98542 60.4277 4.45483 60H6.35524ZM23.2136 60C25.5716 60.8899 27.1507 61.2 30 61.2V60.6C27.9473 60.6 26.5843 60.4373 25.0812 60L23.2136 60ZM0 64.2C2.53215 64.2 4.1155 63.837 6.68172 62.8795C6.79039 62.839 7.16116 62.7 7.21197 62.681C10.0919 61.6043 11.8941 61.2 15 61.2C18.0813 61.2 19.6086 61.5665 22.3886 62.6785C25.2336 63.8165 26.8313 64.2 30 64.2V63.6C26.9187 63.6 25.3914 63.2335 22.6114 62.1215C19.7664 60.9835 18.1687 60.6 15 60.6C11.8122 60.6 9.94344 61.0192 7.00185 62.119C6.95066 62.1381 6.58016 62.277 6.47198 62.3174C3.96706 63.2519 2.44905 63.6 0 63.6L0 64.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 60C6.46234 59.9609 6.57112 59.9208 6.68172 59.8795C6.79039 59.839 7.16116 59.7 7.21197 59.681C10.0919 58.6043 11.8941 58.2 15 58.2C18.0813 58.2 19.6086 58.5665 22.3886 59.6785C22.6753 59.7932 22.9494 59.9003 23.2136 60H25.0812C24.3279 59.7808 23.5395 59.4927 22.6114 59.1215C19.7664 57.9835 18.1687 57.6 15 57.6C11.8122 57.6 9.94344 58.0192 7.00185 59.119C6.95066 59.1381 6.58016 59.277 6.47198 59.3174C5.72954 59.5944 5.0738 59.8198 4.45483 60H6.35524ZM6.35524 54C3.97726 54.8676 2.42302 55.2 0 55.2L0 54.6C1.72318 54.6 2.98542 54.4277 4.45483 54H6.35524ZM23.2136 54C25.5716 54.8899 27.1507 55.2 30 55.2V54.6C27.9473 54.6 26.5843 54.4373 25.0812 54L23.2136 54ZM0 58.2C2.53215 58.2 4.1155 57.837 6.68172 56.8795C6.79039 56.839 7.16116 56.7 7.21197 56.681C10.0919 55.6043 11.8941 55.2 15 55.2C18.0813 55.2 19.6086 55.5665 22.3886 56.6785C25.2336 57.8165 26.8313 58.2 30 58.2V57.6C26.9187 57.6 25.3914 57.2335 22.6114 56.1215C19.7664 54.9835 18.1687 54.6 15 54.6C11.8122 54.6 9.94344 55.0192 7.00185 56.119C6.95066 56.1381 6.58016 56.277 6.47198 56.3174C3.96706 57.2519 2.44905 57.6 0 57.6L0 58.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 54C6.46234 53.9609 6.57112 53.9208 6.68172 53.8795C6.79039 53.839 7.16116 53.7 7.21197 53.681C10.0919 52.6043 11.8941 52.2 15 52.2C18.0813 52.2 19.6086 52.5665 22.3886 53.6785C22.6753 53.7932 22.9494 53.9003 23.2136 54H25.0812C24.3279 53.7808 23.5395 53.4927 22.6114 53.1215C19.7664 51.9835 18.1687 51.6 15 51.6C11.8122 51.6 9.94344 52.0192 7.00185 53.119C6.95066 53.1381 6.58016 53.277 6.47198 53.3174C5.72954 53.5944 5.0738 53.8198 4.45483 54H6.35524ZM6.35524 48C3.97726 48.8676 2.42302 49.2 0 49.2L0 48.6C1.72318 48.6 2.98542 48.4277 4.45483 48H6.35524ZM23.2136 48C25.5716 48.8899 27.1507 49.2 30 49.2V48.6C27.9473 48.6 26.5843 48.4373 25.0812 48L23.2136 48ZM0 52.2C2.53215 52.2 4.1155 51.837 6.68172 50.8795C6.79039 50.839 7.16116 50.7 7.21197 50.681C10.0919 49.6043 11.8941 49.2 15 49.2C18.0813 49.2 19.6086 49.5665 22.3886 50.6785C25.2336 51.8165 26.8313 52.2 30 52.2V51.6C26.9187 51.6 25.3914 51.2335 22.6114 50.1215C19.7664 48.9835 18.1687 48.6 15 48.6C11.8122 48.6 9.94344 49.0192 7.00185 50.119C6.95066 50.1381 6.58016 50.277 6.47198 50.3174C3.96706 51.2519 2.44905 51.6 0 51.6L0 52.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 48C6.46234 47.9609 6.57112 47.9208 6.68172 47.8795C6.79039 47.839 7.16116 47.7 7.21197 47.681C10.0919 46.6043 11.8941 46.2 15 46.2C18.0813 46.2 19.6086 46.5665 22.3886 47.6785C22.6753 47.7932 22.9494 47.9003 23.2136 48H25.0812C24.3279 47.7808 23.5395 47.4927 22.6114 47.1215C19.7664 45.9835 18.1687 45.6 15 45.6C11.8122 45.6 9.94344 46.0192 7.00185 47.119C6.95066 47.1381 6.58016 47.277 6.47198 47.3174C5.72954 47.5944 5.0738 47.8198 4.45483 48H6.35524ZM6.35524 42C3.97726 42.8676 2.42302 43.2 0 43.2L0 42.6C1.72318 42.6 2.98542 42.4277 4.45483 42H6.35524ZM23.2136 42C25.5716 42.8899 27.1507 43.2 30 43.2V42.6C27.9473 42.6 26.5843 42.4373 25.0812 42L23.2136 42ZM0 46.2C2.53215 46.2 4.1155 45.837 6.68172 44.8795C6.79039 44.839 7.16116 44.7 7.21197 44.681C10.0919 43.6043 11.8941 43.2 15 43.2C18.0813 43.2 19.6086 43.5665 22.3886 44.6785C25.2336 45.8165 26.8313 46.2 30 46.2V45.6C26.9187 45.6 25.3914 45.2335 22.6114 44.1215C19.7664 42.9835 18.1687 42.6 15 42.6C11.8122 42.6 9.94344 43.0192 7.00185 44.119C6.95066 44.1381 6.58016 44.277 6.47198 44.3174C3.96706 45.2519 2.44905 45.6 0 45.6L0 46.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 42C6.46234 41.9609 6.57112 41.9208 6.68172 41.8795C6.79039 41.839 7.16116 41.7 7.21197 41.681C10.0919 40.6043 11.8941 40.2 15 40.2C18.0813 40.2 19.6086 40.5665 22.3886 41.6785C22.6753 41.7932 22.9494 41.9003 23.2136 42H25.0812C24.3279 41.7808 23.5395 41.4927 22.6114 41.1215C19.7664 39.9835 18.1687 39.6 15 39.6C11.8122 39.6 9.94344 40.0192 7.00185 41.119C6.95066 41.1381 6.58016 41.277 6.47198 41.3174C5.72954 41.5944 5.0738 41.8198 4.45483 42H6.35524ZM6.35524 36C3.97726 36.8676 2.42302 37.2 0 37.2L0 36.6C1.72318 36.6 2.98542 36.4277 4.45483 36H6.35524ZM23.2136 36C25.5716 36.8899 27.1507 37.2 30 37.2V36.6C27.9473 36.6 26.5843 36.4373 25.0812 36L23.2136 36ZM0 40.2C2.53215 40.2 4.1155 39.837 6.68172 38.8795C6.79039 38.839 7.16116 38.7 7.21197 38.681C10.0919 37.6043 11.8941 37.2 15 37.2C18.0813 37.2 19.6086 37.5665 22.3886 38.6785C25.2336 39.8165 26.8313 40.2 30 40.2V39.6C26.9187 39.6 25.3914 39.2335 22.6114 38.1215C19.7664 36.9835 18.1687 36.6 15 36.6C11.8122 36.6 9.94344 37.0192 7.00185 38.119C6.95066 38.1381 6.58016 38.277 6.47198 38.3174C3.96706 39.2519 2.44905 39.6 0 39.6L0 40.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 36C6.46234 35.9609 6.57112 35.9208 6.68172 35.8795C6.79039 35.839 7.16116 35.7 7.21197 35.681C10.0919 34.6043 11.8941 34.2 15 34.2C18.0813 34.2 19.6086 34.5665 22.3886 35.6785C22.6753 35.7932 22.9494 35.9003 23.2136 36H25.0812C24.3279 35.7808 23.5395 35.4927 22.6114 35.1215C19.7664 33.9835 18.1687 33.6 15 33.6C11.8122 33.6 9.94344 34.0192 7.00185 35.119C6.95066 35.1381 6.58016 35.277 6.47198 35.3174C5.72954 35.5944 5.0738 35.8198 4.45483 36H6.35524ZM6.35524 30C3.97726 30.8676 2.42302 31.2 0 31.2L0 30.6C1.72318 30.6 2.98542 30.4277 4.45483 30H6.35524ZM23.2136 30C25.5716 30.8899 27.1507 31.2 30 31.2V30.6C27.9473 30.6 26.5843 30.4373 25.0812 30L23.2136 30ZM0 34.2C2.53215 34.2 4.1155 33.837 6.68172 32.8795C6.79039 32.839 7.16116 32.7 7.21197 32.681C10.0919 31.6043 11.8941 31.2 15 31.2C18.0813 31.2 19.6086 31.5665 22.3886 32.6785C25.2336 33.8165 26.8313 34.2 30 34.2V33.6C26.9187 33.6 25.3914 33.2335 22.6114 32.1215C19.7664 30.9835 18.1687 30.6 15 30.6C11.8122 30.6 9.94344 31.0192 7.00185 32.119C6.95066 32.1381 6.58016 32.277 6.47198 32.3174C3.96706 33.2519 2.44905 33.6 0 33.6L0 34.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 30C6.46234 29.9609 6.57112 29.9208 6.68172 29.8795C6.79039 29.839 7.16116 29.7 7.21197 29.681C10.0919 28.6043 11.8941 28.2 15 28.2C18.0813 28.2 19.6086 28.5665 22.3886 29.6785C22.6753 29.7932 22.9494 29.9003 23.2136 30H25.0812C24.3279 29.7808 23.5395 29.4927 22.6114 29.1215C19.7664 27.9835 18.1687 27.6 15 27.6C11.8122 27.6 9.94344 28.0192 7.00185 29.119C6.95066 29.1381 6.58016 29.277 6.47198 29.3174C5.72954 29.5944 5.0738 29.8198 4.45483 30H6.35524ZM6.35524 24C3.97726 24.8676 2.42302 25.2 0 25.2L0 24.6C1.72318 24.6 2.98542 24.4277 4.45483 24H6.35524ZM23.2136 24C25.5716 24.8899 27.1507 25.2 30 25.2V24.6C27.9473 24.6 26.5843 24.4373 25.0812 24L23.2136 24ZM0 28.2C2.53215 28.2 4.1155 27.837 6.68172 26.8795C6.79039 26.839 7.16116 26.7 7.21197 26.681C10.0919 25.6043 11.8941 25.2 15 25.2C18.0813 25.2 19.6086 25.5665 22.3886 26.6785C25.2336 27.8165 26.8313 28.2 30 28.2V27.6C26.9187 27.6 25.3914 27.2335 22.6114 26.1215C19.7664 24.9835 18.1687 24.6 15 24.6C11.8122 24.6 9.94344 25.0192 7.00185 26.119C6.95066 26.1381 6.58016 26.277 6.47198 26.3174C3.96706 27.2519 2.44905 27.6 0 27.6L0 28.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 24C6.46234 23.9609 6.57112 23.9208 6.68172 23.8795C6.79039 23.839 7.16116 23.7 7.21197 23.681C10.0919 22.6043 11.8941 22.2 15 22.2C18.0813 22.2 19.6086 22.5665 22.3886 23.6785C22.6753 23.7932 22.9494 23.9003 23.2136 24H25.0812C24.3279 23.7808 23.5395 23.4927 22.6114 23.1215C19.7664 21.9835 18.1687 21.6 15 21.6C11.8122 21.6 9.94344 22.0192 7.00185 23.119C6.95066 23.1381 6.58016 23.277 6.47198 23.3174C5.72954 23.5944 5.0738 23.8198 4.45483 24H6.35524ZM6.35524 18C3.97726 18.8676 2.42302 19.2 0 19.2L0 18.6C1.72318 18.6 2.98542 18.4277 4.45483 18H6.35524ZM23.2136 18C25.5716 18.8899 27.1507 19.2 30 19.2V18.6C27.9473 18.6 26.5843 18.4373 25.0812 18L23.2136 18ZM0 22.2C2.53215 22.2 4.1155 21.837 6.68172 20.8795C6.79039 20.839 7.16116 20.7 7.21197 20.681C10.0919 19.6043 11.8941 19.2 15 19.2C18.0813 19.2 19.6086 19.5665 22.3886 20.6785C25.2336 21.8165 26.8313 22.2 30 22.2V21.6C26.9187 21.6 25.3914 21.2335 22.6114 20.1215C19.7664 18.9835 18.1687 18.6 15 18.6C11.8122 18.6 9.94344 19.0192 7.00185 20.119C6.95066 20.1381 6.58016 20.277 6.47198 20.3174C3.96706 21.2519 2.44905 21.6 0 21.6L0 22.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 18C6.46234 17.9609 6.57112 17.9208 6.68172 17.8795C6.79039 17.839 7.16116 17.7 7.21197 17.681C10.0919 16.6043 11.8941 16.2 15 16.2C18.0813 16.2 19.6086 16.5665 22.3886 17.6785C22.6753 17.7932 22.9494 17.9003 23.2136 18H25.0812C24.3279 17.7808 23.5395 17.4927 22.6114 17.1215C19.7664 15.9835 18.1687 15.6 15 15.6C11.8122 15.6 9.94344 16.0192 7.00185 17.119C6.95066 17.1381 6.58016 17.277 6.47198 17.3174C5.72954 17.5944 5.0738 17.8198 4.45483 18H6.35524ZM6.35524 12C3.97726 12.8676 2.42302 13.2 0 13.2L0 12.6C1.72318 12.6 2.98542 12.4277 4.45483 12H6.35524ZM23.2136 12C25.5716 12.8899 27.1507 13.2 30 13.2V12.6C27.9473 12.6 26.5843 12.4373 25.0812 12L23.2136 12ZM0 16.2C2.53215 16.2 4.1155 15.837 6.68172 14.8795C6.79039 14.839 7.16116 14.7 7.21197 14.681C10.0919 13.6043 11.8941 13.2 15 13.2C18.0813 13.2 19.6086 13.5665 22.3886 14.6785C25.2336 15.8165 26.8313 16.2 30 16.2V15.6C26.9187 15.6 25.3914 15.2335 22.6114 14.1215C19.7664 12.9835 18.1687 12.6 15 12.6C11.8122 12.6 9.94344 13.0192 7.00185 14.119C6.95066 14.1381 6.58016 14.277 6.47198 14.3174C3.96706 15.2519 2.44905 15.6 0 15.6L0 16.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 12C6.46234 11.9609 6.57112 11.9208 6.68172 11.8795C6.79039 11.839 7.16116 11.7 7.21197 11.681C10.0919 10.6043 11.8941 10.2 15 10.2C18.0813 10.2 19.6086 10.5665 22.3886 11.6785C22.6753 11.7932 22.9494 11.9003 23.2136 12H25.0812C24.3279 11.7808 23.5395 11.4927 22.6114 11.1215C19.7664 9.98347 18.1687 9.6 15 9.6C11.8122 9.6 9.94344 10.0192 7.00185 11.119C6.95066 11.1381 6.58016 11.277 6.47198 11.3174C5.72954 11.5944 5.0738 11.8198 4.45483 12H6.35524ZM6.35524 6C3.97726 6.86758 2.42302 7.2 0 7.2L0 6.6C1.72318 6.6 2.98542 6.42769 4.45483 6H6.35524ZM23.2136 6C25.5716 6.88993 27.1507 7.2 30 7.2V6.6C27.9473 6.6 26.5843 6.43734 25.0812 6L23.2136 6ZM0 10.2C2.53215 10.2 4.1155 9.83697 6.68172 8.8795C6.79039 8.83895 7.16116 8.7 7.21197 8.681C10.0919 7.60427 11.8941 7.2 15 7.2C18.0813 7.2 19.6086 7.56653 22.3886 8.67854C25.2336 9.81653 26.8313 10.2 30 10.2V9.6C26.9187 9.6 25.3914 9.23347 22.6114 8.12146C19.7664 6.98347 18.1687 6.6 15 6.6C11.8122 6.6 9.94344 7.01922 7.00185 8.119C6.95066 8.13814 6.58016 8.27699 6.47198 8.31735C3.96706 9.25195 2.44905 9.6 0 9.6L0 10.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 6C6.46234 5.96092 6.57112 5.92076 6.68172 5.8795C6.79039 5.83895 7.16116 5.7 7.21197 5.681C10.0919 4.60427 11.8941 4.2 15 4.2C18.0813 4.2 19.6086 4.56653 22.3886 5.67854C22.6753 5.79323 22.9494 5.90026 23.2136 6H25.0812C24.3279 5.78083 23.5395 5.49269 22.6114 5.12146C19.7664 3.98347 18.1687 3.6 15 3.6C11.8122 3.6 9.94344 4.01922 7.00185 5.119C6.95066 5.13814 6.58016 5.27699 6.47198 5.31735C5.72954 5.59436 5.0738 5.81984 4.45483 6H6.35524ZM6.35524 0C3.97726 0.867585 2.42302 1.2 0 1.2L0 0.6C1.72318 0.6 2.98542 0.427692 4.45483 0L6.35524 0ZM23.2136 0C25.5716 0.88993 27.1507 1.2 30 1.2V0.6C27.9473 0.6 26.5843 0.437343 25.0812 0L23.2136 0ZM0 4.2C2.53215 4.2 4.1155 3.83697 6.68172 2.8795C6.79039 2.83895 7.16116 2.7 7.21197 2.681C10.0919 1.60427 11.8941 1.2 15 1.2C18.0813 1.2 19.6086 1.56653 22.3886 2.67854C25.2336 3.81653 26.8313 4.2 30 4.2V3.6C26.9187 3.6 25.3914 3.23347 22.6114 2.12146C19.7664 0.983469 18.1687 0.6 15 0.6C11.8122 0.6 9.94344 1.01922 7.00185 2.119C6.95066 2.13814 6.58016 2.27699 6.47198 2.31735C3.96706 3.25195 2.44905 3.6 0 3.6L0 4.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
</g>
<defs>
<clipPath id="clip0_954_5239">
<rect width="5" height="80" fill="white" transform="matrix(1 0 0 -1 0 80)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2024 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1 @@
Images in this folder are based on [Tabler.io icons](https://tabler.io/icons).

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2024 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1 @@
Images in this folder are based on [Tabler.io icons](https://tabler.io/icons).

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,25 @@
<svg width="5" height="80" viewBox="0 0 5 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_253_1286)">
<rect width="5" height="80" fill="url(#paint0_linear_253_1286)"/>
<line x1="-0.860365" y1="6.80136" x2="10.6078" y2="-1.22871" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="14.8314" x2="10.6078" y2="6.80132" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="22.8615" x2="10.6078" y2="14.8314" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="30.8916" x2="10.6078" y2="22.8615" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="38.9216" x2="10.6078" y2="30.8915" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="46.9517" x2="10.6078" y2="38.9216" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="54.9818" x2="10.6078" y2="46.9517" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="63.0118" x2="10.6078" y2="54.9817" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="71.0419" x2="10.6078" y2="63.0118" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="79.072" x2="10.6078" y2="71.0419" stroke="black" stroke-width="3"/>
<line x1="-0.860365" y1="87.102" x2="10.6078" y2="79.072" stroke="black" stroke-width="3"/>
</g>
<defs>
<linearGradient id="paint0_linear_253_1286" x1="2.5" y1="0" x2="2.5" y2="80" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEC84B"/>
<stop offset="1" stop-color="#F79009"/>
</linearGradient>
<clipPath id="clip0_253_1286">
<rect width="5" height="80" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,18 +1,9 @@
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
import { apiSubmitAccountNote } from 'mastodon/api/accounts';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import api from '../api';
export const submitAccountNote = createAppAsyncThunk(
export const submitAccountNote = createDataLoadingThunk(
'account_note/submit',
async (args: { id: string; value: string }, { getState }) => {
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
const response = await api(getState).post<unknown>(
`/api/v1/accounts/${args.id}/note`,
{
comment: args.value,
},
);
return { relationship: response.data };
},
({ accountId, note }: { accountId: string; note: string }) =>
apiSubmitAccountNote(accountId, note),
(relationship) => ({ relationship }),
);

View file

@ -1,5 +1,18 @@
import { browserHistory } from 'mastodon/components/router';
import { debounceWithDispatchAndArguments } from 'mastodon/utils/debounce';
import api, { getLinks } from '../api';
import {
followAccountSuccess, unfollowAccountSuccess,
authorizeFollowRequestSuccess, rejectFollowRequestSuccess,
followAccountRequest, followAccountFail,
unfollowAccountRequest, unfollowAccountFail,
muteAccountSuccess, unmuteAccountSuccess,
blockAccountSuccess, unblockAccountSuccess,
pinAccountSuccess, unpinAccountSuccess,
fetchRelationshipsSuccess,
} from './accounts_typed';
import { importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
@ -10,36 +23,22 @@ export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS';
export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
@ -59,7 +58,6 @@ export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
@ -71,21 +69,21 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
export * from './accounts_typed';
export function fetchAccount(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchRelationships([id]));
dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
api().get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data));
dispatch(fetchAccountSuccess());
}).catch(error => {
@ -94,10 +92,10 @@ export function fetchAccount(id) {
};
}
export const lookupAccount = acct => (dispatch, getState) => {
export const lookupAccount = acct => (dispatch) => {
dispatch(lookupAccountRequest(acct));
api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
api().get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
dispatch(lookupAccountSuccess());
@ -149,12 +147,12 @@ export function followAccount(id, options = { reblogs: true }) {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
const locked = getState().getIn(['accounts', id, 'locked'], false);
dispatch(followAccountRequest(id, locked));
dispatch(followAccountRequest({ id, locked }));
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
api().post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing}));
}).catch(error => {
dispatch(followAccountFail(error, locked));
dispatch(followAccountFail({ id, error, locked }));
});
};
}
@ -163,87 +161,35 @@ export function unfollowAccount(id) {
return (dispatch, getState) => {
dispatch(unfollowAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
api().post(`/api/v1/accounts/${id}/unfollow`).then(response => {
dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')}));
}).catch(error => {
dispatch(unfollowAccountFail(error));
dispatch(unfollowAccountFail({ id, error }));
});
};
}
export function followAccountRequest(id, locked) {
return {
type: ACCOUNT_FOLLOW_REQUEST,
id,
locked,
skipLoading: true,
};
}
export function followAccountSuccess(relationship, alreadyFollowing) {
return {
type: ACCOUNT_FOLLOW_SUCCESS,
relationship,
alreadyFollowing,
skipLoading: true,
};
}
export function followAccountFail(error, locked) {
return {
type: ACCOUNT_FOLLOW_FAIL,
error,
locked,
skipLoading: true,
};
}
export function unfollowAccountRequest(id) {
return {
type: ACCOUNT_UNFOLLOW_REQUEST,
id,
skipLoading: true,
};
}
export function unfollowAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_UNFOLLOW_SUCCESS,
relationship,
statuses,
skipLoading: true,
};
}
export function unfollowAccountFail(error) {
return {
type: ACCOUNT_UNFOLLOW_FAIL,
error,
skipLoading: true,
};
}
export function blockAccount(id) {
return (dispatch, getState) => {
dispatch(blockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
api().post(`/api/v1/accounts/${id}/block`).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(blockAccountSuccess(response.data, getState().get('statuses')));
dispatch(blockAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
}).catch(error => {
dispatch(blockAccountFail(id, error));
dispatch(blockAccountFail({ id, error }));
});
};
}
export function unblockAccount(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(unblockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
dispatch(unblockAccountSuccess(response.data));
api().post(`/api/v1/accounts/${id}/unblock`).then(response => {
dispatch(unblockAccountSuccess({ relationship: response.data }));
}).catch(error => {
dispatch(unblockAccountFail(id, error));
dispatch(unblockAccountFail({ id, error }));
});
};
}
@ -254,15 +200,6 @@ export function blockAccountRequest(id) {
id,
};
}
export function blockAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_BLOCK_SUCCESS,
relationship,
statuses,
};
}
export function blockAccountFail(error) {
return {
type: ACCOUNT_BLOCK_FAIL,
@ -277,13 +214,6 @@ export function unblockAccountRequest(id) {
};
}
export function unblockAccountSuccess(relationship) {
return {
type: ACCOUNT_UNBLOCK_SUCCESS,
relationship,
};
}
export function unblockAccountFail(error) {
return {
type: ACCOUNT_UNBLOCK_FAIL,
@ -296,23 +226,23 @@ export function muteAccount(id, notifications, duration=0) {
return (dispatch, getState) => {
dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
api().post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
dispatch(muteAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
}).catch(error => {
dispatch(muteAccountFail(id, error));
dispatch(muteAccountFail({ id, error }));
});
};
}
export function unmuteAccount(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(unmuteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
dispatch(unmuteAccountSuccess(response.data));
api().post(`/api/v1/accounts/${id}/unmute`).then(response => {
dispatch(unmuteAccountSuccess({ relationship: response.data }));
}).catch(error => {
dispatch(unmuteAccountFail(id, error));
dispatch(unmuteAccountFail({ id, error }));
});
};
}
@ -324,14 +254,6 @@ export function muteAccountRequest(id) {
};
}
export function muteAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_MUTE_SUCCESS,
relationship,
statuses,
};
}
export function muteAccountFail(error) {
return {
type: ACCOUNT_MUTE_FAIL,
@ -346,13 +268,6 @@ export function unmuteAccountRequest(id) {
};
}
export function unmuteAccountSuccess(relationship) {
return {
type: ACCOUNT_UNMUTE_SUCCESS,
relationship,
};
}
export function unmuteAccountFail(error) {
return {
type: ACCOUNT_UNMUTE_FAIL,
@ -362,10 +277,10 @@ export function unmuteAccountFail(error) {
export function fetchFollowers(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchFollowersRequest(id));
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
api().get(`/api/v1/accounts/${id}/followers`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -412,7 +327,7 @@ export function expandFollowers(id) {
dispatch(expandFollowersRequest(id));
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -449,10 +364,10 @@ export function expandFollowersFail(id, error) {
}
export function fetchFollowing(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchFollowingRequest(id));
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
api().get(`/api/v1/accounts/${id}/following`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -499,7 +414,7 @@ export function expandFollowing(id) {
dispatch(expandFollowingRequest(id));
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -535,6 +450,20 @@ export function expandFollowingFail(id, error) {
};
}
const debouncedFetchRelationships = debounceWithDispatchAndArguments((dispatch, ...newAccountIds) => {
if (newAccountIds.length === 0) {
return;
}
dispatch(fetchRelationshipsRequest(newAccountIds));
api().get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
}, { delay: 500 });
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
const state = getState();
@ -546,13 +475,7 @@ export function fetchRelationships(accountIds) {
return;
}
dispatch(fetchRelationshipsRequest(newAccountIds));
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess(response.data));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
debouncedFetchRelationships(dispatch, ...newAccountIds);
};
}
@ -564,14 +487,6 @@ export function fetchRelationshipsRequest(ids) {
};
}
export function fetchRelationshipsSuccess(relationships) {
return {
type: RELATIONSHIPS_FETCH_SUCCESS,
relationships,
skipLoading: true,
};
}
export function fetchRelationshipsFail(error) {
return {
type: RELATIONSHIPS_FETCH_FAIL,
@ -582,10 +497,10 @@ export function fetchRelationshipsFail(error) {
}
export function fetchFollowRequests() {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchFollowRequestsRequest());
api(getState).get('/api/v1/follow_requests').then(response => {
api().get('/api/v1/follow_requests').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
@ -624,7 +539,7 @@ export function expandFollowRequests() {
dispatch(expandFollowRequestsRequest());
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
@ -654,12 +569,12 @@ export function expandFollowRequestsFail(error) {
}
export function authorizeFollowRequest(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(authorizeFollowRequestRequest(id));
api(getState)
api()
.post(`/api/v1/follow_requests/${id}/authorize`)
.then(() => dispatch(authorizeFollowRequestSuccess(id)))
.then(() => dispatch(authorizeFollowRequestSuccess({ id })))
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
};
}
@ -671,13 +586,6 @@ export function authorizeFollowRequestRequest(id) {
};
}
export function authorizeFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
id,
};
}
export function authorizeFollowRequestFail(id, error) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
@ -688,12 +596,12 @@ export function authorizeFollowRequestFail(id, error) {
export function rejectFollowRequest(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(rejectFollowRequestRequest(id));
api(getState)
api()
.post(`/api/v1/follow_requests/${id}/reject`)
.then(() => dispatch(rejectFollowRequestSuccess(id)))
.then(() => dispatch(rejectFollowRequestSuccess({ id })))
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
};
}
@ -705,13 +613,6 @@ export function rejectFollowRequestRequest(id) {
};
}
export function rejectFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_REJECT_SUCCESS,
id,
};
}
export function rejectFollowRequestFail(id, error) {
return {
type: FOLLOW_REQUEST_REJECT_FAIL,
@ -721,11 +622,11 @@ export function rejectFollowRequestFail(id, error) {
}
export function pinAccount(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(pinAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
dispatch(pinAccountSuccess(response.data));
api().post(`/api/v1/accounts/${id}/pin`).then(response => {
dispatch(pinAccountSuccess({ relationship: response.data }));
}).catch(error => {
dispatch(pinAccountFail(error));
});
@ -733,11 +634,11 @@ export function pinAccount(id) {
}
export function unpinAccount(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(unpinAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
dispatch(unpinAccountSuccess(response.data));
api().post(`/api/v1/accounts/${id}/unpin`).then(response => {
dispatch(unpinAccountSuccess({ relationship: response.data }));
}).catch(error => {
dispatch(unpinAccountFail(error));
});
@ -751,13 +652,6 @@ export function pinAccountRequest(id) {
};
}
export function pinAccountSuccess(relationship) {
return {
type: ACCOUNT_PIN_SUCCESS,
relationship,
};
}
export function pinAccountFail(error) {
return {
type: ACCOUNT_PIN_FAIL,
@ -772,13 +666,6 @@ export function unpinAccountRequest(id) {
};
}
export function unpinAccountSuccess(relationship) {
return {
type: ACCOUNT_UNPIN_SUCCESS,
relationship,
};
}
export function unpinAccountFail(error) {
return {
type: ACCOUNT_UNPIN_FAIL,
@ -786,7 +673,27 @@ export function unpinAccountFail(error) {
};
}
export const revealAccount = id => ({
type: ACCOUNT_REVEAL,
id,
});
export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch) => {
const data = new FormData();
data.append('display_name', displayName);
data.append('note', note);
if (avatar) data.append('avatar', avatar);
if (header) data.append('header', header);
data.append('discoverable', discoverable);
data.append('indexable', indexable);
return api().patch('/api/v1/accounts/update_credentials', data).then(response => {
dispatch(importFetchedAccount(response.data));
});
};
export const navigateToProfile = (accountId) => {
return (_dispatch, getState) => {
const acct = getState().accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}`);
}
};
};

View file

@ -0,0 +1,97 @@
import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
export const revealAccount = createAction<{
id: string;
}>('accounts/revealAccount');
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
'accounts/importAccounts',
);
function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
return {
payload: {
...args,
skipLoading: true,
},
};
}
export const followAccountSuccess = createAction(
'accounts/followAccount/SUCCESS',
actionWithSkipLoadingTrue<{
relationship: ApiRelationshipJSON;
alreadyFollowing: boolean;
}>,
);
export const unfollowAccountSuccess = createAction(
'accounts/unfollowAccount/SUCCESS',
actionWithSkipLoadingTrue<{
relationship: ApiRelationshipJSON;
statuses: unknown;
alreadyFollowing?: boolean;
}>,
);
export const authorizeFollowRequestSuccess = createAction<{ id: string }>(
'accounts/followRequestAuthorize/SUCCESS',
);
export const rejectFollowRequestSuccess = createAction<{ id: string }>(
'accounts/followRequestReject/SUCCESS',
);
export const followAccountRequest = createAction(
'accounts/follow/REQUEST',
actionWithSkipLoadingTrue<{ id: string; locked: boolean }>,
);
export const followAccountFail = createAction(
'accounts/follow/FAIL',
actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>,
);
export const unfollowAccountRequest = createAction(
'accounts/unfollow/REQUEST',
actionWithSkipLoadingTrue<{ id: string }>,
);
export const unfollowAccountFail = createAction(
'accounts/unfollow/FAIL',
actionWithSkipLoadingTrue<{ id: string; error: string }>,
);
export const blockAccountSuccess = createAction<{
relationship: ApiRelationshipJSON;
statuses: unknown;
}>('accounts/block/SUCCESS');
export const unblockAccountSuccess = createAction<{
relationship: ApiRelationshipJSON;
}>('accounts/unblock/SUCCESS');
export const muteAccountSuccess = createAction<{
relationship: ApiRelationshipJSON;
statuses: unknown;
}>('accounts/mute/SUCCESS');
export const unmuteAccountSuccess = createAction<{
relationship: ApiRelationshipJSON;
}>('accounts/unmute/SUCCESS');
export const pinAccountSuccess = createAction<{
relationship: ApiRelationshipJSON;
}>('accounts/pin/SUCCESS');
export const unpinAccountSuccess = createAction<{
relationship: ApiRelationshipJSON;
}>('accounts/unpin/SUCCESS');
export const fetchRelationshipsSuccess = createAction(
'relationships/fetch/SUCCESS',
actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>,
);

View file

@ -1,5 +1,7 @@
import { defineMessages } from 'react-intl';
import { AxiosError } from 'axios';
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
@ -50,10 +52,15 @@ export const showAlertForError = (error, skipNotFound = false) => {
});
}
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
}
};

View file

@ -26,10 +26,10 @@ export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW';
const noOp = () => {};
export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => {
export const fetchAnnouncements = (done = noOp) => (dispatch) => {
dispatch(fetchAnnouncementsRequest());
api(getState).get('/api/v1/announcements').then(response => {
api().get('/api/v1/announcements').then(response => {
dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x))));
}).catch(error => {
dispatch(fetchAnnouncementsFail(error));
@ -61,10 +61,10 @@ export const updateAnnouncements = announcement => ({
announcement: normalizeAnnouncement(announcement),
});
export const dismissAnnouncement = announcementId => (dispatch, getState) => {
export const dismissAnnouncement = announcementId => (dispatch) => {
dispatch(dismissAnnouncementRequest(announcementId));
api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
api().post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
dispatch(dismissAnnouncementSuccess(announcementId));
}).catch(error => {
dispatch(dismissAnnouncementFail(announcementId, error));
@ -103,7 +103,7 @@ export const addReaction = (announcementId, name) => (dispatch, getState) => {
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
}
api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
api().put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
}).catch(err => {
if (!alreadyAdded) {
@ -134,10 +134,10 @@ export const addReactionFail = (announcementId, name, error) => ({
skipLoading: true,
});
export const removeReaction = (announcementId, name) => (dispatch, getState) => {
export const removeReaction = (announcementId, name) => (dispatch) => {
dispatch(removeReactionRequest(announcementId, name));
api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
api().delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
dispatch(removeReactionSuccess(announcementId, name));
}).catch(err => {
dispatch(removeReactionFail(announcementId, name, err));

View file

@ -12,13 +12,11 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
export function fetchBlocks() {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchBlocksRequest());
api(getState).get('/api/v1/blocks').then(response => {
api().get('/api/v1/blocks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
@ -58,7 +56,7 @@ export function expandBlocks() {
dispatch(expandBlocksRequest());
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
export function initBlockModal(account) {
return dispatch => {
dispatch({
type: BLOCKS_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'BLOCK' }));
dispatch(openModal({
modalType: 'BLOCK',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

View file

@ -18,7 +18,7 @@ export function fetchBookmarkedStatuses() {
dispatch(fetchBookmarkedStatusesRequest());
api(getState).get('/api/v1/bookmarks').then(response => {
api().get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
@ -59,7 +59,7 @@ export function expandBookmarkedStatuses() {
dispatch(expandBookmarkedStatusesRequest());
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));

View file

@ -1,32 +0,0 @@
import { openModal } from './modal';
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
export function initBoostModal(props) {
return (dispatch, getState) => {
const default_privacy = getState().getIn(['compose', 'default_privacy']);
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
dispatch({
type: BOOSTS_INIT_MODAL,
privacy,
});
dispatch(openModal({
modalType: 'BOOST',
modalProps: props,
}));
};
}
export function changeBoostPrivacy(privacy) {
return dispatch => {
dispatch({
type: BOOSTS_CHANGE_PRIVACY,
privacy,
});
};
}

View file

@ -4,6 +4,7 @@ import axios from 'axios';
import { throttle } from 'lodash';
import api from 'mastodon/api';
import { browserHistory } from 'mastodon/components/router';
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'mastodon/settings';
@ -75,6 +76,7 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
@ -87,9 +89,9 @@ const messages = defineMessages({
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
export const ensureComposeIsVisible = (getState) => {
if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/publish');
browserHistory.push('/publish');
}
};
@ -109,14 +111,26 @@ export function changeCompose(text) {
};
}
export function replyCompose(status, routerHistory) {
export function replyCompose(status) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_REPLY,
status: status,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function replyComposeById(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (status) {
const account = state.accounts.get(status.get('account'));
dispatch(replyCompose(status.set('account', account)));
}
};
}
@ -132,38 +146,44 @@ export function resetCompose() {
};
}
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
export const focusCompose = (defaultText) => (dispatch, getState) => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
export function mentionCompose(account, routerHistory) {
export function mentionCompose(account) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_MENTION,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function directCompose(account, routerHistory) {
export function mentionComposeById(accountId) {
return (dispatch, getState) => {
dispatch(mentionCompose(getState().accounts.get(accountId)));
};
}
export function directCompose(account) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_DIRECT,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function submitCompose(routerHistory) {
export function submitCompose() {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@ -195,7 +215,7 @@ export function submitCompose(routerHistory) {
});
}
api(getState).request({
api().request({
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
method: statusId === null ? 'post' : 'put',
data: {
@ -213,8 +233,8 @@ export function submitCompose(routerHistory) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
if (routerHistory && (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new') && window.history.state) {
routerHistory.goBack();
if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new') && window.history.state) {
browserHistory.goBack();
}
dispatch(insertIntoTagHistory(response.data.tags, status));
@ -248,7 +268,7 @@ export function submitCompose(routerHistory) {
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}).catch(function (error) {
dispatch(submitComposeFail(error));
@ -278,7 +298,7 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
return function (dispatch, getState) {
const uploadLimit = 4;
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0);
@ -298,12 +318,12 @@ export function uploadCompose(files) {
dispatch(uploadComposeRequest());
for (const [i, file] of Array.from(files).entries()) {
if (media.size + i > 3) break;
if (media.size + i > (uploadLimit - 1)) break;
const data = new FormData();
data.append('file', file);
api(getState).post('/api/v2/media', data, {
api().post('/api/v2/media', data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
@ -320,7 +340,7 @@ export function uploadCompose(files) {
let tryCount = 1;
const poll = () => {
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
api().get(`/api/v1/media/${data.id}`).then(response => {
if (response.status === 200) {
dispatch(uploadComposeSuccess(response.data, file));
} else if (response.status === 206) {
@ -342,7 +362,7 @@ export const uploadComposeProcessing = () => ({
type: COMPOSE_UPLOAD_PROCESSING,
});
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
export const uploadThumbnail = (id, file) => (dispatch) => {
dispatch(uploadThumbnailRequest());
const total = file.size;
@ -350,7 +370,7 @@ export const uploadThumbnail = (id, file) => (dispatch, getState) => {
data.append('thumbnail', file);
api(getState).put(`/api/v1/media/${id}`, data, {
api().put(`/api/v1/media/${id}`, data, {
onUploadProgress: ({ loaded }) => {
dispatch(uploadThumbnailProgress(loaded, total));
},
@ -433,7 +453,7 @@ export function changeUploadCompose(id, params) {
dispatch(changeUploadComposeSuccess(data, true));
} else {
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
api().put(`/api/v1/media/${id}`, params).then(response => {
dispatch(changeUploadComposeSuccess(response.data, false));
}).catch(error => {
dispatch(changeUploadComposeFail(id, error));
@ -521,7 +541,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
fetchComposeSuggestionsAccountsController = new AbortController();
api(getState).get('/api/v1/accounts/search', {
api().get('/api/v1/accounts/search', {
signal: fetchComposeSuggestionsAccountsController.signal,
params: {
@ -555,7 +575,7 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
fetchComposeSuggestionsTagsController = new AbortController();
api(getState).get('/api/v2/search', {
api().get('/api/v2/search', {
signal: fetchComposeSuggestionsTagsController.signal,
params: {
@ -786,11 +806,12 @@ export function addPollOption(title) {
};
}
export function changePollOption(index, title) {
export function changePollOption(index, title, maxOptions) {
return {
type: COMPOSE_POLL_OPTION_CHANGE,
index,
title,
maxOptions,
};
}
@ -808,3 +829,9 @@ export function changePollSettings(expiresIn, isMultiple) {
isMultiple,
};
}
export const changeMediaOrder = (a, b) => ({
type: COMPOSE_CHANGE_MEDIA_ORDER,
a,
b,
});

View file

@ -28,13 +28,13 @@ export const unmountConversations = () => ({
type: CONVERSATIONS_UNMOUNT,
});
export const markConversationRead = conversationId => (dispatch, getState) => {
export const markConversationRead = conversationId => (dispatch) => {
dispatch({
type: CONVERSATIONS_READ,
id: conversationId,
});
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
api().post(`/api/v1/conversations/${conversationId}/read`);
};
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
@ -48,7 +48,7 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
const isLoadingRecent = !!params.since_id;
api(getState).get('/api/v1/conversations', { params })
api().get('/api/v1/conversations', { params })
.then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
@ -88,10 +88,10 @@ export const updateConversations = conversation => dispatch => {
});
};
export const deleteConversation = conversationId => (dispatch, getState) => {
export const deleteConversation = conversationId => (dispatch) => {
dispatch(deleteConversationRequest(conversationId));
api(getState).delete(`/api/v1/conversations/${conversationId}`)
api().delete(`/api/v1/conversations/${conversationId}`)
.then(() => dispatch(deleteConversationSuccess(conversationId)))
.catch(error => dispatch(deleteConversationFail(conversationId, error)));
};

View file

@ -5,10 +5,10 @@ export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
export function fetchCustomEmojis() {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchCustomEmojisRequest());
api(getState).get('/api/v1/custom_emojis').then(response => {
api().get('/api/v1/custom_emojis').then(response => {
dispatch(fetchCustomEmojisSuccess(response.data));
}).catch(error => {
dispatch(fetchCustomEmojisFail(error));

View file

@ -1,62 +0,0 @@
import api from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
export const fetchDirectory = params => (dispatch, getState) => {
dispatch(fetchDirectoryRequest());
api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(fetchDirectoryFail(error)));
};
export const fetchDirectoryRequest = () => ({
type: DIRECTORY_FETCH_REQUEST,
});
export const fetchDirectorySuccess = accounts => ({
type: DIRECTORY_FETCH_SUCCESS,
accounts,
});
export const fetchDirectoryFail = error => ({
type: DIRECTORY_FETCH_FAIL,
error,
});
export const expandDirectory = params => (dispatch, getState) => {
dispatch(expandDirectoryRequest());
const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(expandDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(expandDirectoryFail(error)));
};
export const expandDirectoryRequest = () => ({
type: DIRECTORY_EXPAND_REQUEST,
});
export const expandDirectorySuccess = accounts => ({
type: DIRECTORY_EXPAND_SUCCESS,
accounts,
});
export const expandDirectoryFail = error => ({
type: DIRECTORY_EXPAND_FAIL,
error,
});

View file

@ -0,0 +1,37 @@
import type { List as ImmutableList } from 'immutable';
import { apiGetDirectory } from 'mastodon/api/directory';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
export const fetchDirectory = createDataLoadingThunk(
'directory/fetch',
async (params: Parameters<typeof apiGetDirectory>[0]) =>
apiGetDirectory(params),
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchRelationships(data.map((x) => x.id)));
return { accounts: data };
},
);
export const expandDirectory = createDataLoadingThunk(
'directory/expand',
async (params: Parameters<typeof apiGetDirectory>[0], { getState }) => {
const loadedItems = getState().user_lists.getIn([
'directory',
'items',
]) as ImmutableList<unknown>;
return apiGetDirectory({ ...params, offset: loadedItems.size }, 20);
},
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchRelationships(data.map((x) => x.id)));
return { accounts: data };
},
);

View file

@ -1,11 +1,15 @@
import api, { getLinks } from '../api';
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
import { openModal } from './modal';
export * from "./domain_blocks_typed";
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS';
export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
@ -20,11 +24,11 @@ export function blockDomain(domain) {
return (dispatch, getState) => {
dispatch(blockDomainRequest(domain));
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
api().post('/api/v1/domain_blocks', { domain }).then(() => {
const at_domain = '@' + domain;
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
dispatch(blockDomainSuccess(domain, accounts));
dispatch(blockDomainSuccess({ domain, accounts }));
}).catch(err => {
dispatch(blockDomainFail(domain, err));
});
@ -38,14 +42,6 @@ export function blockDomainRequest(domain) {
};
}
export function blockDomainSuccess(domain, accounts) {
return {
type: DOMAIN_BLOCK_SUCCESS,
domain,
accounts,
};
}
export function blockDomainFail(domain, error) {
return {
type: DOMAIN_BLOCK_FAIL,
@ -58,10 +54,10 @@ export function unblockDomain(domain) {
return (dispatch, getState) => {
dispatch(unblockDomainRequest(domain));
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
api().delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
const at_domain = '@' + domain;
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
dispatch(unblockDomainSuccess(domain, accounts));
dispatch(unblockDomainSuccess({ domain, accounts }));
}).catch(err => {
dispatch(unblockDomainFail(domain, err));
});
@ -75,14 +71,6 @@ export function unblockDomainRequest(domain) {
};
}
export function unblockDomainSuccess(domain, accounts) {
return {
type: DOMAIN_UNBLOCK_SUCCESS,
domain,
accounts,
};
}
export function unblockDomainFail(domain, error) {
return {
type: DOMAIN_UNBLOCK_FAIL,
@ -92,10 +80,10 @@ export function unblockDomainFail(domain, error) {
}
export function fetchDomainBlocks() {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchDomainBlocksRequest());
api(getState).get('/api/v1/domain_blocks').then(response => {
api().get('/api/v1/domain_blocks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => {
@ -135,7 +123,7 @@ export function expandDomainBlocks() {
dispatch(expandDomainBlocksRequest());
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => {
@ -164,3 +152,12 @@ export function expandDomainBlocksFail(error) {
error,
};
}
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
modalType: 'DOMAIN_BLOCK',
modalProps: {
domain: account.get('acct').split('@')[1],
acct: account.get('acct'),
accountId: account.get('id'),
},
}));

View file

@ -0,0 +1,13 @@
import { createAction } from '@reduxjs/toolkit';
import type { Account } from 'mastodon/models/account';
export const blockDomainSuccess = createAction<{
domain: string;
accounts: Account[];
}>('domain_blocks/block/SUCCESS');
export const unblockDomainSuccess = createAction<{
domain: string;
accounts: Account[];
}>('domain_blocks/unblock/SUCCESS');

View file

@ -1,10 +0,0 @@
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
export function openDropdownMenu(id, keyboard, scroll_key) {
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
}
export function closeDropdownMenu(id) {
return { type: DROPDOWN_MENU_CLOSE, id };
}

View file

@ -0,0 +1,11 @@
import { createAction } from '@reduxjs/toolkit';
export const openDropdownMenu = createAction<{
id: string;
keyboard: boolean;
scrollKey: string;
}>('dropdownMenu/open');
export const closeDropdownMenu = createAction<{ id: string }>(
'dropdownMenu/close',
);

View file

@ -18,7 +18,7 @@ export function fetchFavouritedStatuses() {
dispatch(fetchFavouritedStatusesRequest());
api(getState).get('/api/v1/favourites').then(response => {
api().get('/api/v1/favourites').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
@ -62,7 +62,7 @@ export function expandFavouritedStatuses() {
dispatch(expandFavouritedStatusesRequest());
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));

View file

@ -11,7 +11,7 @@ export const fetchFeaturedTags = (id) => (dispatch, getState) => {
dispatch(fetchFeaturedTagsRequest(id));
api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
api().get(`/api/v1/accounts/${id}/featured_tags`)
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
};

View file

@ -23,13 +23,13 @@ export const initAddFilter = (status, { contextType }) => dispatch =>
},
}));
export const fetchFilters = () => (dispatch, getState) => {
export const fetchFilters = () => (dispatch) => {
dispatch({
type: FILTERS_FETCH_REQUEST,
skipLoading: true,
});
api(getState)
api()
.get('/api/v2/filters')
.then(({ data }) => dispatch({
type: FILTERS_FETCH_SUCCESS,
@ -44,10 +44,10 @@ export const fetchFilters = () => (dispatch, getState) => {
}));
};
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch) => {
dispatch(createFilterStatusRequest());
api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
api().post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
dispatch(createFilterStatusSuccess(response.data));
if (onSuccess) onSuccess();
}).catch(error => {
@ -70,10 +70,10 @@ export const createFilterStatusFail = error => ({
error,
});
export const createFilter = (params, onSuccess, onFail) => (dispatch, getState) => {
export const createFilter = (params, onSuccess, onFail) => (dispatch) => {
dispatch(createFilterRequest());
api(getState).post('/api/v2/filters', params).then(response => {
api().post('/api/v2/filters', params).then(response => {
dispatch(createFilterSuccess(response.data));
if (onSuccess) onSuccess(response.data);
}).catch(error => {

View file

@ -15,7 +15,7 @@ export const fetchHistory = statusId => (dispatch, getState) => {
dispatch(fetchHistoryRequest(statusId));
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
api().get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
dispatch(importFetchedAccounts(data.map(x => x.account)));
dispatch(fetchHistorySuccess(statusId, data));
}).catch(error => dispatch(fetchHistoryFail(error)));

View file

@ -1,7 +1,7 @@
import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer';
import { importAccounts } from '../accounts_typed';
import { normalizeStatus, normalizePoll } from './normalizer';
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';
@ -13,14 +13,6 @@ function pushUnique(array, object) {
}
}
export function importAccount(account) {
return { type: ACCOUNT_IMPORT, account };
}
export function importAccounts(accounts) {
return { type: ACCOUNTS_IMPORT, accounts };
}
export function importStatus(status) {
return { type: STATUS_IMPORT, status };
}
@ -45,7 +37,7 @@ export function importFetchedAccounts(accounts) {
const normalAccounts = [];
function processAccount(account) {
pushUnique(normalAccounts, normalizeAccount(account));
pushUnique(normalAccounts, account);
if (account.moved) {
processAccount(account.moved);
@ -54,7 +46,7 @@ export function importFetchedAccounts(accounts) {
accounts.forEach(processAccount);
return importAccounts(normalAccounts);
return importAccounts({ accounts: normalAccounts });
}
export function importFetchedStatus(status) {
@ -76,13 +68,17 @@ export function importFetchedStatuses(statuses) {
status.filtered.forEach(result => pushUnique(filters, result.filter));
}
if (status.reblog && status.reblog.id) {
if (status.reblog?.id) {
processStatus(status.reblog);
}
if (status.poll && status.poll.id) {
if (status.poll?.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
}
if (status.card) {
status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
}
}
statuses.forEach(processStatus);

View file

@ -2,7 +2,6 @@ import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji';
import { expandSpoilers } from '../../initial_state';
import { unescapeHTML } from '../../utils/html';
const domParser = new DOMParser();
@ -17,32 +16,6 @@ export function searchTextFromRawStatus (status) {
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
}
export function normalizeAccount(account) {
account = { ...account };
const emojiMap = makeEmojiMap(account.emojis);
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
account.note_emojified = emojify(account.note, emojiMap);
account.note_plain = unescapeHTML(account.note);
if (account.fields) {
account.fields = account.fields.map(pair => ({
...pair,
name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap),
value_emojified: emojify(pair.value, emojiMap),
value_plain: unescapeHTML(pair.value),
}));
}
if (account.moved) {
account.moved = account.moved.id;
}
return account;
}
export function normalizeFilterResult(result) {
const normalResult = { ...result };
@ -63,6 +36,17 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.poll = status.poll.id;
}
if (status.card) {
normalStatus.card = {
...status.card,
authors: status.card.authors.map(author => ({
...author,
accountId: author.account?.id,
account: undefined,
})),
};
}
if (status.filtered) {
normalStatus.filtered = status.filtered.map(normalizeFilterResult);
}
@ -104,7 +88,7 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.media_attachments.forEach(item => {
const oldItem = list.find(i => i.get('id') === item.id);
if (oldItem && oldItem.get('description') === item.description) {
item.translation = oldItem.get('translation')
item.translation = oldItem.get('translation');
}
});
}
@ -137,13 +121,13 @@ export function normalizePoll(poll, normalOldPoll) {
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}
};
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
}
return normalOption
return normalOption;
});
return normalPoll;

View file

@ -1,11 +1,11 @@
import { boostModal } from 'mastodon/initial_state';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatus } from './importer';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
export const REBLOG_FAIL = 'REBLOG_FAIL';
import { unreblog, reblog } from './interactions_typed';
import { openModal } from './modal';
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
@ -15,10 +15,6 @@ export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
export const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
@ -51,89 +47,13 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
export function reblog(status, visibility) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
// The reblog API method returns a new status wrapped around the original. In this case we are only
// interested in how the original is modified, hence passing it skipping the wrapper
dispatch(importFetchedStatus(response.data.reblog));
dispatch(reblogSuccess(status));
}).catch(function (error) {
dispatch(reblogFail(status, error));
});
};
}
export function unreblog(status) {
return (dispatch, getState) => {
dispatch(unreblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unreblogSuccess(status));
}).catch(error => {
dispatch(unreblogFail(status, error));
});
};
}
export function reblogRequest(status) {
return {
type: REBLOG_REQUEST,
status: status,
skipLoading: true,
};
}
export function reblogSuccess(status) {
return {
type: REBLOG_SUCCESS,
status: status,
skipLoading: true,
};
}
export function reblogFail(status, error) {
return {
type: REBLOG_FAIL,
status: status,
error: error,
skipLoading: true,
};
}
export function unreblogRequest(status) {
return {
type: UNREBLOG_REQUEST,
status: status,
skipLoading: true,
};
}
export function unreblogSuccess(status) {
return {
type: UNREBLOG_SUCCESS,
status: status,
skipLoading: true,
};
}
export function unreblogFail(status, error) {
return {
type: UNREBLOG_FAIL,
status: status,
error: error,
skipLoading: true,
};
}
export * from "./interactions_typed";
export function favourite(status) {
return function (dispatch, getState) {
return function (dispatch) {
dispatch(favouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
api().post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
dispatch(importFetchedStatus(response.data));
dispatch(favouriteSuccess(status));
}).catch(function (error) {
@ -143,10 +63,10 @@ export function favourite(status) {
}
export function unfavourite(status) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(unfavouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
api().post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unfavouriteSuccess(status));
}).catch(error => {
@ -206,10 +126,10 @@ export function unfavouriteFail(status, error) {
}
export function bookmark(status) {
return function (dispatch, getState) {
return function (dispatch) {
dispatch(bookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
api().post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status, response.data));
}).catch(function (error) {
@ -219,10 +139,10 @@ export function bookmark(status) {
}
export function unbookmark(status) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(unbookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
api().post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status, response.data));
}).catch(error => {
@ -278,10 +198,10 @@ export function unbookmarkFail(status, error) {
}
export function fetchReblogs(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchReblogsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
api().get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
@ -325,7 +245,7 @@ export function expandReblogs(id) {
dispatch(expandReblogsRequest(id));
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -360,10 +280,10 @@ export function expandReblogsFail(id, error) {
}
export function fetchFavourites(id) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
api().get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
@ -407,7 +327,7 @@ export function expandFavourites(id) {
dispatch(expandFavouritesRequest(id));
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -442,10 +362,10 @@ export function expandFavouritesFail(id, error) {
}
export function pin(status) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(pinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(pinSuccess(status));
}).catch(error => {
@ -480,10 +400,10 @@ export function pinFail(status, error) {
}
export function unpin (status) {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(unpinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unpinSuccess(status));
}).catch(error => {
@ -516,3 +436,49 @@ export function unpinFail(status, error) {
skipLoading: true,
};
}
function toggleReblogWithoutConfirmation(status, visibility) {
return (dispatch) => {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
dispatch(reblog({ statusId: status.get('id'), visibility }));
}
};
}
export function toggleReblog(statusId, skipModal = false) {
return (dispatch, getState) => {
const state = getState();
let status = state.statuses.get(statusId);
if (!status)
return;
// The reblog modal expects a pre-filled account in status
// TODO: fix this by having the reblog modal get a statusId and do the work itself
status = status.set('account', state.accounts.get(status.get('account')));
if (boostModal && !skipModal) {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)) } }));
} else {
dispatch(toggleReblogWithoutConfirmation(status));
}
};
}
export function toggleFavourite(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (!status)
return;
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
};
}

View file

@ -0,0 +1,35 @@
import { apiReblog, apiUnreblog } from 'mastodon/api/interactions';
import type { StatusVisibility } from 'mastodon/models/status';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedStatus } from './importer';
export const reblog = createDataLoadingThunk(
'status/reblog',
({
statusId,
visibility,
}: {
statusId: string;
visibility: StatusVisibility;
}) => apiReblog(statusId, visibility),
(data, { dispatch, discardLoadData }) => {
// The reblog API method returns a new status wrapped around the original. In this case we are only
// interested in how the original is modified, hence passing it skipping the wrapper
dispatch(importFetchedStatus(data.reblog));
// The payload is not used in any actions
return discardLoadData;
},
);
export const unreblog = createDataLoadingThunk(
'status/unreblog',
({ statusId }: { statusId: string }) => apiUnreblog(statusId),
(data, { dispatch, discardLoadData }) => {
dispatch(importFetchedStatus(data));
// The payload is not used in any actions
return discardLoadData;
},
);

View file

@ -1,12 +0,0 @@
import { saveSettings } from './settings';
export const LANGUAGE_USE = 'LANGUAGE_USE';
export const useLanguage = language => dispatch => {
dispatch({
type: LANGUAGE_USE,
language,
});
dispatch(saveSettings());
};

View file

@ -57,7 +57,7 @@ export const fetchList = id => (dispatch, getState) => {
dispatch(fetchListRequest(id));
api(getState).get(`/api/v1/lists/${id}`)
api().get(`/api/v1/lists/${id}`)
.then(({ data }) => dispatch(fetchListSuccess(data)))
.catch(err => dispatch(fetchListFail(id, err)));
};
@ -78,10 +78,10 @@ export const fetchListFail = (id, error) => ({
error,
});
export const fetchLists = () => (dispatch, getState) => {
export const fetchLists = () => (dispatch) => {
dispatch(fetchListsRequest());
api(getState).get('/api/v1/lists')
api().get('/api/v1/lists')
.then(({ data }) => dispatch(fetchListsSuccess(data)))
.catch(err => dispatch(fetchListsFail(err)));
};
@ -125,10 +125,10 @@ export const changeListEditorTitle = value => ({
value,
});
export const createList = (title, shouldReset) => (dispatch, getState) => {
export const createList = (title, shouldReset) => (dispatch) => {
dispatch(createListRequest());
api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
api().post('/api/v1/lists', { title }).then(({ data }) => {
dispatch(createListSuccess(data));
if (shouldReset) {
@ -151,10 +151,10 @@ export const createListFail = error => ({
error,
});
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
api().put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {
@ -183,10 +183,10 @@ export const resetListEditor = () => ({
type: LIST_EDITOR_RESET,
});
export const deleteList = id => (dispatch, getState) => {
export const deleteList = id => (dispatch) => {
dispatch(deleteListRequest(id));
api(getState).delete(`/api/v1/lists/${id}`)
api().delete(`/api/v1/lists/${id}`)
.then(() => dispatch(deleteListSuccess(id)))
.catch(err => dispatch(deleteListFail(id, err)));
};
@ -207,10 +207,10 @@ export const deleteListFail = (id, error) => ({
error,
});
export const fetchListAccounts = listId => (dispatch, getState) => {
export const fetchListAccounts = listId => (dispatch) => {
dispatch(fetchListAccountsRequest(listId));
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
api().get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchListAccountsSuccess(listId, data));
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
@ -234,7 +234,7 @@ export const fetchListAccountsFail = (id, error) => ({
error,
});
export const fetchListSuggestions = q => (dispatch, getState) => {
export const fetchListSuggestions = q => (dispatch) => {
const params = {
q,
resolve: false,
@ -242,7 +242,7 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
following: true,
};
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
api().get('/api/v1/accounts/search', { params }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchListSuggestionsReady(q, data));
}).catch(error => dispatch(showAlertForError(error)));
@ -267,10 +267,10 @@ export const addToListEditor = accountId => (dispatch, getState) => {
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const addToList = (listId, accountId) => (dispatch, getState) => {
export const addToList = (listId, accountId) => (dispatch) => {
dispatch(addToListRequest(listId, accountId));
api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
api().post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
.then(() => dispatch(addToListSuccess(listId, accountId)))
.catch(err => dispatch(addToListFail(listId, accountId, err)));
};
@ -298,10 +298,10 @@ export const removeFromListEditor = accountId => (dispatch, getState) => {
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const removeFromList = (listId, accountId) => (dispatch, getState) => {
export const removeFromList = (listId, accountId) => (dispatch) => {
dispatch(removeFromListRequest(listId, accountId));
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
api().delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
};
@ -338,10 +338,10 @@ export const setupListAdder = accountId => (dispatch, getState) => {
dispatch(fetchAccountLists(accountId));
};
export const fetchAccountLists = accountId => (dispatch, getState) => {
export const fetchAccountLists = accountId => (dispatch) => {
dispatch(fetchAccountListsRequest(accountId));
api(getState).get(`/api/v1/accounts/${accountId}/lists`)
api().get(`/api/v1/accounts/${accountId}/lists`)
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
};
@ -370,4 +370,3 @@ export const addToListAdder = listId => (dispatch, getState) => {
export const removeFromListAdder = listId => (dispatch, getState) => {
dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
};

View file

@ -1,152 +0,0 @@
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import api from '../api';
import { compareId } from '../compare_id';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if (window.fetch && 'keepalive' in new Request('')) {
fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
} else if (navigator && navigator.sendBeacon) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
};
const _buildParams = (state) => {
const params = {};
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
api(getState).post('/api/v1/markers', params).then(() => {
dispatch(submitMarkersSuccess(params));
}).catch(() => {});
}, 300000, { leading: true, trailing: true });
export function submitMarkersSuccess({ home, notifications }) {
return {
type: MARKERS_SUBMIT_SUCCESS,
home: (home || {}).last_read_id,
notifications: (notifications || {}).last_read_id,
};
}
export function submitMarkers(params = {}) {
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
if (params.immediate === true) {
debouncedSubmitMarkers.flush();
}
return result;
}
export const fetchMarkers = () => (dispatch, getState) => {
const params = { timeline: ['notifications'] };
dispatch(fetchMarkersRequest());
api(getState).get('/api/v1/markers', { params }).then(response => {
dispatch(fetchMarkersSuccess(response.data));
}).catch(error => {
dispatch(fetchMarkersFail(error));
});
};
export function fetchMarkersRequest() {
return {
type: MARKERS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchMarkersSuccess(markers) {
return {
type: MARKERS_FETCH_SUCCESS,
markers,
skipLoading: true,
};
}
export function fetchMarkersFail(error) {
return {
type: MARKERS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
}

View file

@ -0,0 +1,146 @@
import { debounce } from 'lodash';
import type { MarkerJSON } from 'mastodon/api_types/markers';
import { getAccessToken } from 'mastodon/initial_state';
import type { AppDispatch, RootState } from 'mastodon/store';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
import api from '../api';
import { compareId } from '../compare_id';
export const synchronouslySubmitMarkers = createAppAsyncThunk(
'markers/submit',
async (_args, { getState }) => {
const accessToken = getAccessToken();
const params = buildPostMarkersParams(getState());
if (
Object.keys(params).length === 0 ||
!accessToken ||
accessToken === ''
) {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if ('fetch' in window && 'keepalive' in new Request('')) {
await fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if ('navigator' && 'sendBeacon' in navigator) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
if (value.last_read_id)
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch {
// Do not make the BeforeUnload handler error out
}
},
);
interface MarkerParam {
last_read_id?: string;
}
function getLastNotificationId(state: RootState): string | undefined {
return state.notificationGroups.lastReadId;
}
const buildPostMarkersParams = (state: RootState) => {
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
const lastNotificationId = getLastNotificationId(state);
if (
lastNotificationId &&
compareId(lastNotificationId, state.markers.notifications) > 0
) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
export const submitMarkersAction = createAppAsyncThunk<{
home: string | undefined;
notifications: string | undefined;
}>('markers/submitAction', async (_args, { getState }) => {
const accessToken = getAccessToken();
const params = buildPostMarkersParams(getState());
if (Object.keys(params).length === 0 || !accessToken || accessToken === '') {
return { home: undefined, notifications: undefined };
}
await api().post<MarkerJSON>('/api/v1/markers', params);
return {
home: params.home?.last_read_id,
notifications: params.notifications?.last_read_id,
};
});
const debouncedSubmitMarkers = debounce(
(dispatch: AppDispatch) => {
void dispatch(submitMarkersAction());
},
300000,
{
leading: true,
trailing: true,
},
);
export const submitMarkers = createAppAsyncThunk(
'markers/submit',
(params: { immediate?: boolean }, { dispatch }) => {
debouncedSubmitMarkers(dispatch);
if (params.immediate) {
debouncedSubmitMarkers.flush();
}
},
);
export const fetchMarkers = createAppAsyncThunk('markers/fetch', async () => {
const response = await api().get<Record<string, MarkerJSON>>(
`/api/v1/markers`,
{ params: { timeline: ['notifications'] } },
);
return { markers: response.data };
});

View file

@ -1,12 +1,14 @@
import { createAction } from '@reduxjs/toolkit';
import type { ModalProps } from 'mastodon/reducers/modal';
import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root';
export type ModalType = keyof typeof MODAL_COMPONENTS;
interface OpenModalPayload {
modalType: ModalType;
modalProps: unknown;
modalProps: ModalProps;
}
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');

View file

@ -12,15 +12,11 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
export function fetchMutes() {
return (dispatch, getState) => {
return (dispatch) => {
dispatch(fetchMutesRequest());
api(getState).get('/api/v1/mutes').then(response => {
api().get('/api/v1/mutes').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
@ -60,7 +56,7 @@ export function expandMutes() {
dispatch(expandMutesRequest());
api(getState).get(url).then(response => {
api().get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
@ -92,26 +88,12 @@ export function expandMutesFail(error) {
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'MUTE' }));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
dispatch(openModal({
modalType: 'MUTE',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

View file

@ -0,0 +1,239 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiClearNotifications,
apiFetchNotificationGroups,
} from 'mastodon/api/notifications';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'mastodon/api_types/notifications';
import { allNotificationTypes } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
import { usePendingItems } from 'mastodon/initial_state';
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
import {
selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsShows,
} from 'mastodon/selectors/settings';
import type { AppDispatch, RootState } from 'mastodon/store';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'mastodon/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';
function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter);
}
function getExcludedTypes(state: RootState) {
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
return activeFilter === 'all'
? selectSettingsNotificationsExcludedTypes(state)
: excludeAllTypesExcept(activeFilter);
}
function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}
if (notification.type === 'moderation_warning') {
fetchedAccounts.push(notification.moderation_warning.target_account);
}
if ('status' in notification && notification.status) {
fetchedStatuses.push(notification.status);
}
});
if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) =>
apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
exclude_types: getExcludedTypes(getState()),
}),
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
notifications;
// TODO: might be worth not using gaps for that…
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
if (notifications.length > 1)
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });
return payload;
// dispatch(submitMarkers());
},
);
export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()),
}),
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
max_id: undefined,
exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
since_id: usePendingItems
? getState().notificationGroups.groups.find(
(group) => group.type !== 'gap',
)?.page_max_id
: undefined,
});
},
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew',
(notification: ApiNotificationJSON, { dispatch, getState }) => {
const state = getState();
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
const notificationShows = selectSettingsNotificationsShows(state);
const showInColumn =
activeFilter === 'all'
? notificationShows[notification.type]
: activeFilter === notification.type;
if (!showInColumn) return;
if (
(notification.type === 'mention' || notification.type === 'update') &&
notification.status?.filtered
) {
const filters = notification.status.filtered.filter((result) =>
result.filter.context.includes('notifications'),
);
if (filters.some((result) => result.filter.filter_action === 'hide')) {
return;
}
}
dispatchAssociatedRecords(dispatch, [notification]);
return notification;
},
);
export const loadPending = createAction('notificationGroups/loadPending');
export const updateScrollPosition = createAppAsyncThunk(
'notificationGroups/updateScrollPosition',
({ top }: { top: boolean }, { dispatch, getState }) => {
if (
top &&
getState().notificationGroups.mergedNotifications === 'needs-reload'
) {
void dispatch(fetchNotifications());
}
return { top };
},
);
export const setNotificationsFilter = createAppAsyncThunk(
'notifications/filter/set',
({ filterType }: { filterType: string }, { dispatch }) => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
void dispatch(fetchNotifications());
dispatch(saveSettings());
},
);
export const clearNotifications = createDataLoadingThunk(
'notifications/clear',
() => apiClearNotifications(),
);
export const markNotificationsAsRead = createAction(
'notificationGroups/markAsRead',
);
export const mountNotifications = createAppAsyncThunk(
'notificationGroups/mount',
(_, { dispatch, getState }) => {
const state = getState();
if (
state.notificationGroups.mounted === 0 &&
state.notificationGroups.mergedNotifications === 'needs-reload'
) {
void dispatch(fetchNotifications());
}
},
);
export const unmountNotifications = createAction('notificationGroups/unmount');
export const refreshStaleNotificationGroups = createAppAsyncThunk<{
deferredRefresh: boolean;
}>('notificationGroups/refreshStale', (_, { dispatch, getState }) => {
const state = getState();
if (
state.notificationGroups.scrolledToTop ||
!state.notificationGroups.mounted
) {
void dispatch(fetchNotifications());
return { deferredRefresh: false };
}
return { deferredRefresh: true };
});

Some files were not shown because too many files have changed in this diff Show more