Security updates
This commit is contained in:
parent
e470787f85
commit
5de70bf238
82
.gitignore.orig
Normal file
82
.gitignore.orig
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||||
|
#
|
||||||
|
# If you find yourself ignoring temporary files generated by your text editor
|
||||||
|
# or operating system, you probably want to add a global ignore instead:
|
||||||
|
# git config --global core.excludesfile '~/.gitignore_global'
|
||||||
|
|
||||||
|
# Ignore bundler config and downloaded libraries.
|
||||||
|
/.bundle
|
||||||
|
/vendor/bundle
|
||||||
|
|
||||||
|
# Ignore the default SQLite database.
|
||||||
|
/db/*.sqlite3
|
||||||
|
/db/*.sqlite3-journal
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
.eslintcache
|
||||||
|
/log/*
|
||||||
|
!/log/.keep
|
||||||
|
/tmp
|
||||||
|
/coverage
|
||||||
|
/public/system
|
||||||
|
/public/assets
|
||||||
|
/public/packs
|
||||||
|
/public/packs-test
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
/node_modules/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Ignore Vagrant files
|
||||||
|
.vagrant/
|
||||||
|
|
||||||
|
# Ignore IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
|
||||||
|
/postgres
|
||||||
|
/postgres14
|
||||||
|
/redis
|
||||||
|
/elasticsearch
|
||||||
|
|
||||||
|
# Ignore Apple files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Ignore vim files
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Ignore npm debug log
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# Ignore yarn log files
|
||||||
|
yarn-error.log
|
||||||
|
yarn-debug.log
|
||||||
|
|
||||||
|
# From https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Ignore vagrant log files
|
||||||
|
*-cloudimg-console.log
|
||||||
|
|
||||||
|
# Ignore Docker option files
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
# Ignore dotenv .local files
|
||||||
|
.env*.local
|
||||||
|
=======
|
||||||
|
/public/packs
|
||||||
|
/public/packs-test
|
||||||
|
/node_modules
|
||||||
|
/yarn-error.log
|
||||||
|
yarn-debug.log*
|
||||||
|
.yarn-integrity
|
||||||
|
>>>>>>> e94c6b5887 (Wobbl customizations on Glitch-SOC)
|
@ -1,4 +1,4 @@
|
|||||||
import 'packs/public-path';
|
import '@/entrypoints/public-path';
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
import 'flavours/glitch/styles/modern.scss';
|
import 'flavours/glitch/styles/modern.scss';
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import 'packs/public-path';
|
||||||
|
import { start } from '@rails/ujs';
|
||||||
|
import 'flavours/glitch/styles/modern.scss';
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
// This ensures that webpack compiles our images.
|
||||||
|
require.context('../images', true);
|
368
app/javascript/flavours/glitch/modern-entrypoints/admin.tsx
Normal file
368
app/javascript/flavours/glitch/modern-entrypoints/admin.tsx
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
import '@/entrypoints/public-path';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import Rails from '@rails/ujs';
|
||||||
|
|
||||||
|
import ready from 'flavours/glitch/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(
|
||||||
|
'@/flavours/glitch/containers/admin_component'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { default: Component } = (await import(
|
||||||
|
`@/flavours/glitch/components/admin/${componentName}`
|
||||||
|
)) as { default: React.ComponentType };
|
||||||
|
|
||||||
|
const root = createRoot(element);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<AdminComponent>
|
||||||
|
<Component {...componentProps} />
|
||||||
|
</AdminComponent>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
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;
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
import '@/entrypoints/public-path';
|
||||||
|
|
||||||
|
import { start } from 'flavours/glitch/common';
|
||||||
|
import { loadLocale } from 'flavours/glitch/locales';
|
||||||
|
import main from 'flavours/glitch/main';
|
||||||
|
import { loadPolyfills } from 'flavours/glitch/polyfills';
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
loadPolyfills()
|
||||||
|
.then(loadLocale)
|
||||||
|
.then(main)
|
||||||
|
.catch((e: unknown) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
@ -0,0 +1,8 @@
|
|||||||
|
import 'packs/public-path';
|
||||||
|
import { start } from '@rails/ujs';
|
||||||
|
import 'flavours/glitch/styles/modern.scss';
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
// This ensures that webpack compiles our images.
|
||||||
|
require.context('../images', true);
|
@ -0,0 +1,8 @@
|
|||||||
|
import '@/modern-entrypoints/public-path';
|
||||||
|
import Rails from '@rails/ujs';
|
||||||
|
import 'flavours/glitch/styles/modern.scss';
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
// This ensures that webpack compiles our images.
|
||||||
|
require.context('../images', true);
|
18
app/javascript/flavours/glitch/modern-entrypoints/error.ts
Normal file
18
app/javascript/flavours/glitch/modern-entrypoints/error.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import '@/entrypoints/public-path';
|
||||||
|
import ready from 'flavours/glitch/ready';
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
const image = document.querySelector<HTMLImageElement>('img');
|
||||||
|
|
||||||
|
if (!image) return;
|
||||||
|
|
||||||
|
image.addEventListener('mouseenter', () => {
|
||||||
|
image.src = '/oops.gif';
|
||||||
|
});
|
||||||
|
|
||||||
|
image.addEventListener('mouseleave', () => {
|
||||||
|
image.src = '/oops.png';
|
||||||
|
});
|
||||||
|
}).catch((e: unknown) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
@ -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';
|
@ -0,0 +1,3 @@
|
|||||||
|
import '@/styles/mailer.scss';
|
||||||
|
|
||||||
|
require.context('@/icons');
|
462
app/javascript/flavours/glitch/modern-entrypoints/public.tsx
Normal file
462
app/javascript/flavours/glitch/modern-entrypoints/public.tsx
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import '@/entrypoints/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 'flavours/glitch/common';
|
||||||
|
import { timeAgoString } from 'flavours/glitch/components/relative_timestamp';
|
||||||
|
import emojify from 'flavours/glitch/features/emoji/emoji';
|
||||||
|
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
|
||||||
|
import { loadLocale, getLocale } from 'flavours/glitch/locales';
|
||||||
|
import { loadPolyfills } from 'flavours/glitch/polyfills';
|
||||||
|
import ready from 'flavours/glitch/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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: 'setHeight',
|
||||||
|
id: data.id,
|
||||||
|
height: document.getElementsByTagName('html')[0]?.scrollHeight,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}).catch((e: unknown) => {
|
||||||
|
console.error('Error in setHeightMessage postMessage', e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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" */ 'flavours/glitch/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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
This script is meant to to be used in an `iframe` with the sole purpose of doing webfinger queries
|
||||||
|
client-side without being restricted by a strict `connect-src` Content-Security-Policy directive.
|
||||||
|
|
||||||
|
It communicates with the parent window through message events that are authenticated by origin,
|
||||||
|
and performs no other task.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '@/entrypoints/public-path';
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface JRDLink {
|
||||||
|
rel: string;
|
||||||
|
template?: string;
|
||||||
|
href?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isJRDLink = (link: unknown): link is JRDLink =>
|
||||||
|
typeof link === 'object' &&
|
||||||
|
link !== null &&
|
||||||
|
'rel' in link &&
|
||||||
|
typeof link.rel === 'string' &&
|
||||||
|
(!('template' in link) || typeof link.template === 'string') &&
|
||||||
|
(!('href' in link) || typeof link.href === 'string');
|
||||||
|
|
||||||
|
const findLink = (rel: string, data: unknown): JRDLink | undefined => {
|
||||||
|
if (
|
||||||
|
typeof data === 'object' &&
|
||||||
|
data !== null &&
|
||||||
|
'links' in data &&
|
||||||
|
data.links instanceof Array
|
||||||
|
) {
|
||||||
|
return data.links.find(
|
||||||
|
(link): link is JRDLink => isJRDLink(link) && link.rel === rel,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findTemplateLink = (data: unknown) =>
|
||||||
|
findLink('http://ostatus.org/schema/1.0/subscribe', data)?.template;
|
||||||
|
|
||||||
|
const fetchInteractionURLSuccess = (
|
||||||
|
uri_or_domain: string,
|
||||||
|
template: string,
|
||||||
|
) => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: 'fetchInteractionURL-success',
|
||||||
|
uri_or_domain,
|
||||||
|
template,
|
||||||
|
},
|
||||||
|
window.origin,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInteractionURLFailure = () => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: 'fetchInteractionURL-failure',
|
||||||
|
},
|
||||||
|
window.origin,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidDomain = (value: unknown) => {
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
|
||||||
|
const url = new URL('https:///path');
|
||||||
|
url.hostname = value;
|
||||||
|
return url.hostname === value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attempt to find a remote interaction URL from a domain
|
||||||
|
const fromDomain = (domain: string) => {
|
||||||
|
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(`https://${domain}/.well-known/webfinger`, {
|
||||||
|
params: { resource: `https://${domain}` },
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
const template = findTemplateLink(data);
|
||||||
|
fetchInteractionURLSuccess(domain, template ?? fallbackTemplate);
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
fetchInteractionURLSuccess(domain, fallbackTemplate);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attempt to find a remote interaction URL from an arbitrary URL
|
||||||
|
const fromURL = (url: string) => {
|
||||||
|
const domain = new URL(url).host;
|
||||||
|
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(`https://${domain}/.well-known/webfinger`, {
|
||||||
|
params: { resource: url },
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
const template = findTemplateLink(data);
|
||||||
|
fetchInteractionURLSuccess(url, template ?? fallbackTemplate);
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
fromDomain(domain);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attempt to find a remote interaction URL from a `user@domain` string
|
||||||
|
const fromAcct = (acct: string) => {
|
||||||
|
acct = acct.replace(/^@/, '');
|
||||||
|
|
||||||
|
const segments = acct.split('@');
|
||||||
|
|
||||||
|
if (segments.length !== 2 || !segments[0] || !isValidDomain(segments[1])) {
|
||||||
|
fetchInteractionURLFailure();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}` },
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
const template = findTemplateLink(data);
|
||||||
|
fetchInteractionURLSuccess(acct, template ?? fallbackTemplate);
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// TODO: handle host-meta?
|
||||||
|
fromDomain(domain);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInteractionURL = (uri_or_domain: string) => {
|
||||||
|
if (uri_or_domain === '') {
|
||||||
|
fetchInteractionURLFailure();
|
||||||
|
} else if (/^https?:\/\//.test(uri_or_domain)) {
|
||||||
|
fromURL(uri_or_domain);
|
||||||
|
} else if (uri_or_domain.includes('@')) {
|
||||||
|
fromAcct(uri_or_domain);
|
||||||
|
} else {
|
||||||
|
fromDomain(uri_or_domain);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', (event: MessageEvent<unknown>) => {
|
||||||
|
// Check message origin
|
||||||
|
if (
|
||||||
|
!window.origin ||
|
||||||
|
window.parent !== event.source ||
|
||||||
|
event.origin !== window.origin
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.data &&
|
||||||
|
typeof event.data === 'object' &&
|
||||||
|
'type' in event.data &&
|
||||||
|
event.data.type === 'fetchInteractionURL' &&
|
||||||
|
'uri_or_domain' in event.data &&
|
||||||
|
typeof event.data.uri_or_domain === 'string'
|
||||||
|
) {
|
||||||
|
fetchInteractionURL(event.data.uri_or_domain);
|
||||||
|
}
|
||||||
|
});
|
36
app/javascript/flavours/glitch/modern-entrypoints/share.tsx
Normal file
36
app/javascript/flavours/glitch/modern-entrypoints/share.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import '@/entrypoints/public-path';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { start } from 'flavours/glitch/common';
|
||||||
|
import ComposeContainer from 'flavours/glitch/containers/compose_container';
|
||||||
|
import { loadPolyfills } from 'flavours/glitch/polyfills';
|
||||||
|
import ready from 'flavours/glitch/ready';
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
function loaded() {
|
||||||
|
const mountNode = document.getElementById('mastodon-compose');
|
||||||
|
|
||||||
|
if (mountNode) {
|
||||||
|
const attr = mountNode.getAttribute('data-props');
|
||||||
|
|
||||||
|
if (!attr) return;
|
||||||
|
|
||||||
|
const props = JSON.parse(attr) as object;
|
||||||
|
const root = createRoot(mountNode);
|
||||||
|
|
||||||
|
root.render(<ComposeContainer {...props} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
ready(loaded).catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPolyfills()
|
||||||
|
.then(main)
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
48
app/javascript/flavours/glitch/modern-entrypoints/sign_up.ts
Normal file
48
app/javascript/flavours/glitch/modern-entrypoints/sign_up.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import '@/entrypoints/public-path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import ready from 'flavours/glitch/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;
|
||||||
|
});
|
@ -0,0 +1,197 @@
|
|||||||
|
import * as WebAuthnJSON from '@github/webauthn-json';
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import ready from 'flavours/glitch/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;
|
||||||
|
});
|
@ -1,27 +1,5 @@
|
|||||||
# (REQUIRED) The location of the pack files.
|
# (REQUIRED) The location of the pack files.
|
||||||
pack:
|
pack_directory: app/javascript/flavours/glitch/modern-entrypoints
|
||||||
admin:
|
|
||||||
- packs/admin.jsx
|
|
||||||
- packs/public.jsx
|
|
||||||
auth: packs/public.jsx
|
|
||||||
common:
|
|
||||||
filename: packs/common-modern.js
|
|
||||||
stylesheet: true
|
|
||||||
embed: packs/public.jsx
|
|
||||||
error: packs/error.js
|
|
||||||
home:
|
|
||||||
filename: packs/home.js
|
|
||||||
preload:
|
|
||||||
- flavours/glitch/async/compose
|
|
||||||
- flavours/glitch/async/getting_started
|
|
||||||
- flavours/glitch/async/home_timeline
|
|
||||||
- flavours/glitch/async/notifications
|
|
||||||
mailer:
|
|
||||||
modal:
|
|
||||||
public: packs/public.jsx
|
|
||||||
settings: packs/settings.js
|
|
||||||
sign_up: packs/sign_up.js
|
|
||||||
share: packs/share.jsx
|
|
||||||
|
|
||||||
# (OPTIONAL) The directory which contains localization files for
|
# (OPTIONAL) The directory which contains localization files for
|
||||||
# the flavour, relative to this directory. The contents of this
|
# the flavour, relative to this directory. The contents of this
|
||||||
@ -36,12 +14,6 @@ inherit_locales: vanilla
|
|||||||
# or an array thereof. These are the full path from `app/javascript/`.
|
# or an array thereof. These are the full path from `app/javascript/`.
|
||||||
screenshot: flavours/glitch/images/modern-preview.jpg
|
screenshot: flavours/glitch/images/modern-preview.jpg
|
||||||
|
|
||||||
# (OPTIONAL) The directory which contains the pack files.
|
|
||||||
# Defaults to the theme directory (`app/javascript/themes/[theme]`),
|
|
||||||
# which should be sufficient for like 99% of use-cases lol.
|
|
||||||
|
|
||||||
pack_directory: app/javascript/flavours/glitch
|
|
||||||
|
|
||||||
# (OPTIONAL) By default the theme will fallback to the default theme
|
# (OPTIONAL) By default the theme will fallback to the default theme
|
||||||
# if a particular pack is not provided. You can specify different
|
# if a particular pack is not provided. You can specify different
|
||||||
# fallbacks here, or disable fallback behaviours altogether by
|
# fallbacks here, or disable fallback behaviours altogether by
|
||||||
|
Loading…
x
Reference in New Issue
Block a user