mirror of
https://github.com/funamitech/mastodon
synced 2024-11-27 14:29:03 +09:00
Get rid of app/javascript/core
Have all flavors implement everything they need instead.
This commit is contained in:
parent
113c931cda
commit
62bc36416f
@ -4,7 +4,6 @@ module ThemingConcern
|
|||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def use_pack(pack_name)
|
def use_pack(pack_name)
|
||||||
@core = resolve_pack_with_common(Themes.instance.core, pack_name)
|
|
||||||
@theme = resolve_pack_with_common(Themes.instance.flavour(current_flavour), pack_name, current_skin)
|
@theme = resolve_pack_with_common(Themes.instance.flavour(current_flavour), pack_name, current_skin)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class StatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
use_pack 'embed'
|
use_pack 'public'
|
||||||
return not_found if @status.hidden? || @status.reblog?
|
return not_found if @status.hidden? || @status.reblog?
|
||||||
|
|
||||||
expires_in 180, public: true
|
expires_in 180, public: true
|
||||||
|
@ -1,340 +0,0 @@
|
|||||||
// This file will be loaded on admin pages, regardless of theme.
|
|
||||||
|
|
||||||
import 'packs/public-path';
|
|
||||||
|
|
||||||
import Rails from '@rails/ujs';
|
|
||||||
|
|
||||||
import ready from '../mastodon/ready';
|
|
||||||
|
|
||||||
const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
|
|
||||||
const valid = target.value && target.validity.valid;
|
|
||||||
const element = document.querySelector<HTMLInputElement>(
|
|
||||||
'input[type="datetime-local"]#announcement_ends_at',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
element.classList.remove('optional');
|
|
||||||
element.required = true;
|
|
||||||
element.min = target.value;
|
|
||||||
} else {
|
|
||||||
element.classList.add('optional');
|
|
||||||
element.removeAttribute('required');
|
|
||||||
element.removeAttribute('min');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Rails.delegate(
|
|
||||||
document,
|
|
||||||
'input[type="datetime-local"]#announcement_starts_at',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (target instanceof HTMLInputElement)
|
|
||||||
setAnnouncementEndsAttributes(target);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
|
||||||
|
|
||||||
const showSelectAll = () => {
|
|
||||||
const selectAllMatchingElement = document.querySelector(
|
|
||||||
'.batch-table__select-all',
|
|
||||||
);
|
|
||||||
selectAllMatchingElement?.classList.add('active');
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideSelectAll = () => {
|
|
||||||
const selectAllMatchingElement = document.querySelector(
|
|
||||||
'.batch-table__select-all',
|
|
||||||
);
|
|
||||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
|
||||||
'input#select_all_matching',
|
|
||||||
);
|
|
||||||
const selectedMsg = document.querySelector(
|
|
||||||
'.batch-table__select-all .selected',
|
|
||||||
);
|
|
||||||
const notSelectedMsg = document.querySelector(
|
|
||||||
'.batch-table__select-all .not-selected',
|
|
||||||
);
|
|
||||||
|
|
||||||
selectAllMatchingElement?.classList.remove('active');
|
|
||||||
selectedMsg?.classList.remove('active');
|
|
||||||
notSelectedMsg?.classList.add('active');
|
|
||||||
if (hiddenField) hiddenField.value = '0';
|
|
||||||
};
|
|
||||||
|
|
||||||
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
|
||||||
if (!(target instanceof HTMLInputElement)) return;
|
|
||||||
|
|
||||||
const selectAllMatchingElement = document.querySelector(
|
|
||||||
'.batch-table__select-all',
|
|
||||||
);
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
|
|
||||||
.forEach((content) => {
|
|
||||||
content.checked = target.checked;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectAllMatchingElement) {
|
|
||||||
if (target.checked) {
|
|
||||||
showSelectAll();
|
|
||||||
} else {
|
|
||||||
hideSelectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
|
|
||||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
|
||||||
'#select_all_matching',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hiddenField) return;
|
|
||||||
|
|
||||||
const active = hiddenField.value === '1';
|
|
||||||
const selectedMsg = document.querySelector(
|
|
||||||
'.batch-table__select-all .selected',
|
|
||||||
);
|
|
||||||
const notSelectedMsg = document.querySelector(
|
|
||||||
'.batch-table__select-all .not-selected',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedMsg || !notSelectedMsg) return;
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
hiddenField.value = '0';
|
|
||||||
selectedMsg.classList.remove('active');
|
|
||||||
notSelectedMsg.classList.add('active');
|
|
||||||
} else {
|
|
||||||
hiddenField.value = '1';
|
|
||||||
notSelectedMsg.classList.remove('active');
|
|
||||||
selectedMsg.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
|
|
||||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
|
||||||
'input#batch_checkbox_all',
|
|
||||||
);
|
|
||||||
const selectAllMatchingElement = document.querySelector(
|
|
||||||
'.batch-table__select-all',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (checkAllElement) {
|
|
||||||
const allCheckboxes = Array.from(
|
|
||||||
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
|
||||||
);
|
|
||||||
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
|
|
||||||
checkAllElement.indeterminate =
|
|
||||||
!checkAllElement.checked &&
|
|
||||||
allCheckboxes.some((content) => content.checked);
|
|
||||||
|
|
||||||
if (selectAllMatchingElement) {
|
|
||||||
if (checkAllElement.checked) {
|
|
||||||
showSelectAll();
|
|
||||||
} else {
|
|
||||||
hideSelectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Rails.delegate(
|
|
||||||
document,
|
|
||||||
'.filter-subset--with-select select',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (target instanceof HTMLSelectElement) target.form?.submit();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
|
||||||
const rejectMediaDiv = document.querySelector(
|
|
||||||
'.input.with_label.domain_block_reject_media',
|
|
||||||
);
|
|
||||||
const rejectReportsDiv = document.querySelector(
|
|
||||||
'.input.with_label.domain_block_reject_reports',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) {
|
|
||||||
rejectMediaDiv.style.display =
|
|
||||||
target.value === 'suspend' ? 'none' : 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) {
|
|
||||||
rejectReportsDiv.style.display =
|
|
||||||
target.value === 'suspend' ? 'none' : 'block';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
|
||||||
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
|
||||||
});
|
|
||||||
|
|
||||||
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
|
|
||||||
const bootstrapTimelineAccountsField =
|
|
||||||
document.querySelector<HTMLInputElement>(
|
|
||||||
'#form_admin_settings_bootstrap_timeline_accounts',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (bootstrapTimelineAccountsField) {
|
|
||||||
bootstrapTimelineAccountsField.disabled = !target.checked;
|
|
||||||
if (target.checked) {
|
|
||||||
bootstrapTimelineAccountsField.parentElement?.classList.remove(
|
|
||||||
'disabled',
|
|
||||||
);
|
|
||||||
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove(
|
|
||||||
'disabled',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
bootstrapTimelineAccountsField.parentElement?.classList.add('disabled');
|
|
||||||
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add(
|
|
||||||
'disabled',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Rails.delegate(
|
|
||||||
document,
|
|
||||||
'#form_admin_settings_enable_bootstrap_timeline_accounts',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (target instanceof HTMLInputElement)
|
|
||||||
onEnableBootstrapTimelineAccountsChange(target);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeRegistrationMode = (target: HTMLSelectElement) => {
|
|
||||||
const enabled = target.value === 'approved';
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll<HTMLElement>(
|
|
||||||
'.form_admin_settings_registrations_mode .warning-hint',
|
|
||||||
)
|
|
||||||
.forEach((warning_hint) => {
|
|
||||||
warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll<HTMLInputElement>(
|
|
||||||
'input#form_admin_settings_require_invite_text',
|
|
||||||
)
|
|
||||||
.forEach((input) => {
|
|
||||||
input.disabled = !enabled;
|
|
||||||
if (enabled) {
|
|
||||||
let element: HTMLElement | null = input;
|
|
||||||
do {
|
|
||||||
element.classList.remove('disabled');
|
|
||||||
element = element.parentElement;
|
|
||||||
} while (element && !element.classList.contains('fields-group'));
|
|
||||||
} else {
|
|
||||||
let element: HTMLElement | null = input;
|
|
||||||
do {
|
|
||||||
element.classList.add('disabled');
|
|
||||||
element = element.parentElement;
|
|
||||||
} while (element && !element.classList.contains('fields-group'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertUTCDateTimeToLocal = (value: string) => {
|
|
||||||
const date = new Date(value + 'Z');
|
|
||||||
const twoChars = (x: number) => x.toString().padStart(2, '0');
|
|
||||||
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function convertLocalDatetimeToUTC(value: string) {
|
|
||||||
const date = new Date(value);
|
|
||||||
const fullISO8601 = date.toISOString();
|
|
||||||
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
Rails.delegate(
|
|
||||||
document,
|
|
||||||
'#form_admin_settings_registrations_mode',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ready(() => {
|
|
||||||
const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
|
|
||||||
'select#domain_block_severity',
|
|
||||||
);
|
|
||||||
if (domainBlockSeveritySelect)
|
|
||||||
onDomainBlockSeverityChange(domainBlockSeveritySelect);
|
|
||||||
|
|
||||||
const enableBootstrapTimelineAccounts =
|
|
||||||
document.querySelector<HTMLInputElement>(
|
|
||||||
'input#form_admin_settings_enable_bootstrap_timeline_accounts',
|
|
||||||
);
|
|
||||||
if (enableBootstrapTimelineAccounts)
|
|
||||||
onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
|
|
||||||
|
|
||||||
const registrationMode = document.querySelector<HTMLSelectElement>(
|
|
||||||
'select#form_admin_settings_registrations_mode',
|
|
||||||
);
|
|
||||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
|
||||||
|
|
||||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
|
||||||
'input#batch_checkbox_all',
|
|
||||||
);
|
|
||||||
if (checkAllElement) {
|
|
||||||
const allCheckboxes = Array.from(
|
|
||||||
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
|
||||||
);
|
|
||||||
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
|
|
||||||
checkAllElement.indeterminate =
|
|
||||||
!checkAllElement.checked &&
|
|
||||||
allCheckboxes.some((content) => content.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelector('a#add-instance-button')
|
|
||||||
?.addEventListener('click', (e) => {
|
|
||||||
const domain = document.querySelector<HTMLInputElement>(
|
|
||||||
'input[type="text"]#by_domain',
|
|
||||||
)?.value;
|
|
||||||
|
|
||||||
if (domain && e.target instanceof HTMLAnchorElement) {
|
|
||||||
const url = new URL(e.target.href);
|
|
||||||
url.searchParams.set('_domain', domain);
|
|
||||||
e.target.href = url.toString();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
|
||||||
.forEach((element) => {
|
|
||||||
if (element.value) {
|
|
||||||
element.value = convertUTCDateTimeToLocal(element.value);
|
|
||||||
}
|
|
||||||
if (element.placeholder) {
|
|
||||||
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Rails.delegate(document, 'form', 'submit', ({ target }) => {
|
|
||||||
if (target instanceof HTMLFormElement)
|
|
||||||
target
|
|
||||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
|
||||||
.forEach((element) => {
|
|
||||||
if (element.value && element.validity.valid) {
|
|
||||||
element.value = convertLocalDatetimeToUTC(element.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const announcementStartsAt = document.querySelector<HTMLInputElement>(
|
|
||||||
'input[type="datetime-local"]#announcement_starts_at',
|
|
||||||
);
|
|
||||||
if (announcementStartsAt) {
|
|
||||||
setAnnouncementEndsAttributes(announcementStartsAt);
|
|
||||||
}
|
|
||||||
}).catch((reason) => {
|
|
||||||
throw reason;
|
|
||||||
});
|
|
@ -1,3 +0,0 @@
|
|||||||
import 'packs/public-path';
|
|
||||||
import './settings';
|
|
||||||
import './two_factor_authentication';
|
|
@ -1,6 +0,0 @@
|
|||||||
// This file will be loaded on all pages, regardless of theme.
|
|
||||||
|
|
||||||
import 'packs/public-path';
|
|
||||||
import 'font-awesome/css/font-awesome.css';
|
|
||||||
|
|
||||||
require.context('../images/', true);
|
|
@ -1,41 +0,0 @@
|
|||||||
// This file will be loaded on embed pages, regardless of theme.
|
|
||||||
|
|
||||||
import 'packs/public-path';
|
|
||||||
import ready from '../mastodon/ready';
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
console.error('Error in setHeightMessage postMessage', e);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,70 +0,0 @@
|
|||||||
// This file will be loaded on settings pages, regardless of theme.
|
|
||||||
|
|
||||||
import 'packs/public-path';
|
|
||||||
import Rails from '@rails/ujs';
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
@ -1,24 +0,0 @@
|
|||||||
# These packs will be loaded on every appropriate page, regardless of
|
|
||||||
# theme.
|
|
||||||
pack:
|
|
||||||
about:
|
|
||||||
admin: admin.ts
|
|
||||||
auth: auth.js
|
|
||||||
common:
|
|
||||||
filename: common.js
|
|
||||||
stylesheet: true
|
|
||||||
embed: embed.ts
|
|
||||||
error:
|
|
||||||
home:
|
|
||||||
inert:
|
|
||||||
filename: inert.js
|
|
||||||
stylesheet: true
|
|
||||||
mailer:
|
|
||||||
filename: mailer.js
|
|
||||||
stylesheet: true
|
|
||||||
modal:
|
|
||||||
public:
|
|
||||||
settings: settings.ts
|
|
||||||
sign_up:
|
|
||||||
share:
|
|
||||||
remote_interaction_helper: remote_interaction_helper.ts
|
|
@ -1,8 +1,265 @@
|
|||||||
import 'packs/public-path';
|
import 'packs/public-path';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import Rails from '@rails/ujs';
|
||||||
|
|
||||||
import ready from 'flavours/glitch/ready';
|
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) {
|
async function mountReactComponent(element: Element) {
|
||||||
const componentName = element.getAttribute('data-admin-component');
|
const componentName = element.getAttribute('data-admin-component');
|
||||||
const stringProps = element.getAttribute('data-props');
|
const stringProps = element.getAttribute('data-props');
|
||||||
@ -29,6 +286,80 @@ async function mountReactComponent(element: Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ready(() => {
|
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) => {
|
document.querySelectorAll('[data-admin-component]').forEach((element) => {
|
||||||
void mountReactComponent(element);
|
void mountReactComponent(element);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'packs/public-path';
|
import 'packs/public-path';
|
||||||
|
import 'font-awesome/css/font-awesome.css';
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
import 'flavours/glitch/styles/index.scss';
|
import 'flavours/glitch/styles/index.scss';
|
||||||
|
|
||||||
|
4
app/javascript/flavours/glitch/packs/inert.js
Normal file
4
app/javascript/flavours/glitch/packs/inert.js
Normal 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';
|
3
app/javascript/flavours/glitch/packs/mailer.js
Normal file
3
app/javascript/flavours/glitch/packs/mailer.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import '@/styles/mailer.scss';
|
||||||
|
|
||||||
|
require.context('@/icons');
|
@ -34,6 +34,43 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
console.error('Error in setHeightMessage postMessage', e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function loaded() {
|
function loaded() {
|
||||||
const { messages: localeData } = getLocale();
|
const { messages: localeData } = getLocale();
|
||||||
|
|
||||||
@ -285,6 +322,72 @@ function loaded() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 toggleSidebar = () => {
|
||||||
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
|
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
|
||||||
const toggleButton = document.querySelector<HTMLAnchorElement>(
|
const toggleButton = document.querySelector<HTMLAnchorElement>(
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
import * as WebAuthnJSON from '@github/webauthn-json';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import ready from 'flavours/glitch/ready';
|
||||||
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
|
function getCSRFToken() {
|
||||||
|
var CSRFSelector = document.querySelector('meta[name="csrf-token"]');
|
||||||
|
if (CSRFSelector) {
|
||||||
|
return CSRFSelector.getAttribute('content');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideFlashMessages() {
|
||||||
|
Array.from(document.getElementsByClassName('flash-message')).forEach(function(flashMessage) {
|
||||||
|
flashMessage.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function callback(url, body) {
|
||||||
|
axios.post(url, JSON.stringify(body), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CSRF-Token': getCSRFToken(),
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
}).then(function(response) {
|
||||||
|
window.location.replace(response.data.redirect_path);
|
||||||
|
}).catch(function(error) {
|
||||||
|
if (error.response.status === 422) {
|
||||||
|
const errorMessage = document.getElementById('security-key-error-message');
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
console.error(error.response.data.error);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
if (!WebAuthnJSON.supported()) {
|
||||||
|
const unsupported_browser_message = document.getElementById('unsupported-browser-message');
|
||||||
|
if (unsupported_browser_message) {
|
||||||
|
unsupported_browser_message.classList.remove('hidden');
|
||||||
|
document.querySelector('.btn.js-webauthn').disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const webAuthnCredentialRegistrationForm = document.getElementById('new_webauthn_credential');
|
||||||
|
if (webAuthnCredentialRegistrationForm) {
|
||||||
|
webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var nickname = event.target.querySelector('input[name="new_webauthn_credential[nickname]"]');
|
||||||
|
if (nickname.value) {
|
||||||
|
axios.get('/settings/security_keys/options')
|
||||||
|
.then((response) => {
|
||||||
|
const credentialOptions = response.data;
|
||||||
|
|
||||||
|
WebAuthnJSON.create({ 'publicKey': credentialOptions }).then((credential) => {
|
||||||
|
var params = { 'credential': credential, 'nickname': nickname.value };
|
||||||
|
callback('/settings/security_keys', params);
|
||||||
|
}).catch((error) => {
|
||||||
|
const errorMessage = document.getElementById('security-key-error-message');
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error.response.data.error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nickname.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const webAuthnCredentialAuthenticationForm = document.getElementById('webauthn-form');
|
||||||
|
if (webAuthnCredentialAuthenticationForm) {
|
||||||
|
webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
axios.get('sessions/security_key_options')
|
||||||
|
.then((response) => {
|
||||||
|
const credentialOptions = response.data;
|
||||||
|
|
||||||
|
WebAuthnJSON.get({ 'publicKey': credentialOptions }).then((credential) => {
|
||||||
|
var params = { 'user': { 'credential': credential } };
|
||||||
|
callback('sign_in', params);
|
||||||
|
}).catch((error) => {
|
||||||
|
const errorMessage = document.getElementById('security-key-error-message');
|
||||||
|
errorMessage.classList.remove('hidden');
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error.response.data.error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -1,26 +1,32 @@
|
|||||||
# (REQUIRED) The location of the pack files.
|
# (REQUIRED) The location of the pack files.
|
||||||
pack:
|
pack:
|
||||||
admin:
|
admin:
|
||||||
- packs/admin.tsx
|
- admin.tsx
|
||||||
- packs/public.tsx
|
- public.tsx
|
||||||
auth: packs/public.tsx
|
auth:
|
||||||
|
- public.tsx
|
||||||
|
- two_factor_authentication.js
|
||||||
common:
|
common:
|
||||||
filename: packs/common.js
|
filename: common.js
|
||||||
stylesheet: true
|
stylesheet: true
|
||||||
embed: packs/public.tsx
|
error: error.js
|
||||||
error: packs/error.js
|
|
||||||
home:
|
home:
|
||||||
filename: packs/home.js
|
filename: home.js
|
||||||
preload:
|
preload:
|
||||||
- flavours/glitch/async/compose
|
- flavours/glitch/async/compose
|
||||||
- flavours/glitch/async/home_timeline
|
- flavours/glitch/async/home_timeline
|
||||||
- flavours/glitch/async/notifications
|
- flavours/glitch/async/notifications
|
||||||
|
inert:
|
||||||
|
filename: inert.js
|
||||||
|
stylesheet: true
|
||||||
mailer:
|
mailer:
|
||||||
modal:
|
filename: mailer.js
|
||||||
public: packs/public.tsx
|
stylesheet: true
|
||||||
settings: packs/public.tsx
|
public: public.tsx
|
||||||
sign_up: packs/sign_up.js
|
settings: public.tsx
|
||||||
share: packs/share.jsx
|
sign_up: sign_up.js
|
||||||
|
share: share.jsx
|
||||||
|
remote_interaction_helper: remote_interaction_helper.ts
|
||||||
|
|
||||||
# (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
|
||||||
@ -35,6 +41,12 @@ 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/glitch-preview.png
|
screenshot: flavours/glitch/images/glitch-preview.png
|
||||||
|
|
||||||
|
# (OPTIONAL) The directory which contains the pack files.
|
||||||
|
# Defaults to this directory (`app/javascript/flavour/[flavour]`),
|
||||||
|
# but in the case of the vanilla Mastodon flavour the pack files are
|
||||||
|
# somewhere else.
|
||||||
|
pack_directory: app/javascript/flavours/glitch/packs
|
||||||
|
|
||||||
# (OPTIONAL) The directory which contains the pack files.
|
# (OPTIONAL) The directory which contains the pack files.
|
||||||
# Defaults to the theme directory (`app/javascript/themes/[theme]`),
|
# Defaults to the theme directory (`app/javascript/themes/[theme]`),
|
||||||
# which should be sufficient for like 99% of use-cases lol.
|
# which should be sufficient for like 99% of use-cases lol.
|
||||||
|
@ -3,11 +3,12 @@ pack:
|
|||||||
admin:
|
admin:
|
||||||
- admin.tsx
|
- admin.tsx
|
||||||
- public.tsx
|
- public.tsx
|
||||||
auth: public.tsx
|
auth:
|
||||||
|
- public.tsx
|
||||||
|
- two_factor_authentication.js
|
||||||
common:
|
common:
|
||||||
filename: common.js
|
filename: common.js
|
||||||
stylesheet: true
|
stylesheet: true
|
||||||
embed: public.tsx
|
|
||||||
error: error.js
|
error: error.js
|
||||||
home:
|
home:
|
||||||
filename: application.js
|
filename: application.js
|
||||||
@ -15,12 +16,17 @@ pack:
|
|||||||
- features/compose
|
- features/compose
|
||||||
- features/home_timeline
|
- features/home_timeline
|
||||||
- features/notifications
|
- features/notifications
|
||||||
|
inert:
|
||||||
|
filename: inert.js
|
||||||
|
stylesheet: true
|
||||||
mailer:
|
mailer:
|
||||||
modal:
|
filename: mailer.js
|
||||||
|
stylesheet: true
|
||||||
public: public.tsx
|
public: public.tsx
|
||||||
settings: public.tsx
|
settings: public.tsx
|
||||||
sign_up: sign_up.js
|
sign_up: sign_up.js
|
||||||
share: share.jsx
|
share: share.jsx
|
||||||
|
remote_interaction_helper: remote_interaction_helper.ts
|
||||||
|
|
||||||
# (OPTIONAL) The directory which contains localization files for
|
# (OPTIONAL) The directory which contains localization files for
|
||||||
# the flavour, relative to this directory.
|
# the flavour, relative to this directory.
|
||||||
|
@ -1,8 +1,265 @@
|
|||||||
import './public-path';
|
import './public-path';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import Rails from '@rails/ujs';
|
||||||
|
|
||||||
import ready from '../mastodon/ready';
|
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) {
|
async function mountReactComponent(element: Element) {
|
||||||
const componentName = element.getAttribute('data-admin-component');
|
const componentName = element.getAttribute('data-admin-component');
|
||||||
const stringProps = element.getAttribute('data-props');
|
const stringProps = element.getAttribute('data-props');
|
||||||
@ -29,6 +286,80 @@ async function mountReactComponent(element: Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ready(() => {
|
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) => {
|
document.querySelectorAll('[data-admin-component]').forEach((element) => {
|
||||||
void mountReactComponent(element);
|
void mountReactComponent(element);
|
||||||
});
|
});
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
import './public-path';
|
import './public-path';
|
||||||
|
import 'font-awesome/css/font-awesome.css';
|
||||||
import 'styles/application.scss';
|
import 'styles/application.scss';
|
||||||
|
|
||||||
|
require.context('../images/', true);
|
||||||
|
@ -37,6 +37,43 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
console.error('Error in setHeightMessage postMessage', e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function loaded() {
|
function loaded() {
|
||||||
const { messages: localeData } = getLocale();
|
const { messages: localeData } = getLocale();
|
||||||
|
|
||||||
@ -288,6 +325,72 @@ function loaded() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 toggleSidebar = () => {
|
||||||
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
|
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
|
||||||
const toggleButton = document.querySelector<HTMLAnchorElement>(
|
const toggleButton = document.querySelector<HTMLAnchorElement>(
|
||||||
|
174
app/javascript/packs/remote_interaction_helper.ts
Normal file
174
app/javascript/packs/remote_interaction_helper.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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 './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: string) => {
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
@ -1,5 +1,3 @@
|
|||||||
import 'packs/public-path';
|
|
||||||
|
|
||||||
import * as WebAuthnJSON from '@github/webauthn-json';
|
import * as WebAuthnJSON from '@github/webauthn-json';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
@ -7,9 +7,6 @@ class Themes
|
|||||||
include Singleton
|
include Singleton
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
core = YAML.load_file(Rails.root.join('app', 'javascript', 'core', 'theme.yml'))
|
|
||||||
core['pack'] = {} unless core['pack']
|
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
Rails.root.glob('app/javascript/flavours/*/theme.yml') do |pathname|
|
Rails.root.glob('app/javascript/flavours/*/theme.yml') do |pathname|
|
||||||
data = YAML.load_file(pathname)
|
data = YAML.load_file(pathname)
|
||||||
@ -61,12 +58,9 @@ class Themes
|
|||||||
result[name]['skin'][skin] = pack if skin != 'default'
|
result[name]['skin'][skin] = pack if skin != 'default'
|
||||||
end
|
end
|
||||||
|
|
||||||
@core = core
|
|
||||||
@conf = result
|
@conf = result
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :core
|
|
||||||
|
|
||||||
def flavour(name)
|
def flavour(name)
|
||||||
@conf[name]
|
@conf[name]
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
- if theme
|
- if theme
|
||||||
= render partial: 'layouts/theme', object: theme[:common] if theme[:pack] != 'common' && theme[:common]
|
= render partial: 'layouts/theme', object: theme[:common] if theme[:pack] != 'common' && theme[:common]
|
||||||
- if theme[:pack]
|
- if theme[:pack]
|
||||||
- pack_path = theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}"
|
- pack_path = "flavours/#{theme[:flavour]}/#{theme[:pack]}"
|
||||||
= javascript_pack_tag pack_path, crossorigin: 'anonymous'
|
= javascript_pack_tag pack_path, crossorigin: 'anonymous'
|
||||||
- if theme[:skin]
|
- if theme[:skin]
|
||||||
- if !theme[:flavour] || theme[:skin] == 'default'
|
- if !theme[:flavour] || theme[:skin] == 'default'
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
||||||
|
|
||||||
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
||||||
= stylesheet_pack_tag 'core/inert', media: 'all', id: 'inert-style'
|
= stylesheet_pack_tag 'flavours/vanilla/inert', media: 'all', id: 'inert-style'
|
||||||
|
|
||||||
- if @theme
|
- if @theme
|
||||||
- if @theme[:supported_locales].include? I18n.locale.to_s
|
- if @theme[:supported_locales].include? I18n.locale.to_s
|
||||||
@ -42,7 +42,6 @@
|
|||||||
= yield :header_tags
|
= yield :header_tags
|
||||||
|
|
||||||
-# These must come after :header_tags to ensure our initial state has been defined.
|
-# These must come after :header_tags to ensure our initial state has been defined.
|
||||||
= render partial: 'layouts/theme', object: @core
|
|
||||||
= render partial: 'layouts/theme', object: @theme
|
= render partial: 'layouts/theme', object: @theme
|
||||||
|
|
||||||
= stylesheet_link_tag custom_css_path, skip_pipeline: true, host: root_url, media: 'all'
|
= stylesheet_link_tag custom_css_path, skip_pipeline: true, host: root_url, media: 'all'
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
= preload_pack_asset "locales/#{@theme[:flavour]}/#{I18n.locale}-json.js"
|
= preload_pack_asset "locales/#{@theme[:flavour]}/#{I18n.locale}-json.js"
|
||||||
- elsif @theme[:supported_locales].include? 'en'
|
- elsif @theme[:supported_locales].include? 'en'
|
||||||
= preload_pack_asset "locales/#{@theme[:flavour]}/en-json.js"
|
= preload_pack_asset "locales/#{@theme[:flavour]}/en-json.js"
|
||||||
= render partial: 'layouts/theme', object: @core
|
|
||||||
= render partial: 'layouts/theme', object: @theme
|
= render partial: 'layouts/theme', object: @theme
|
||||||
|
|
||||||
%body.embed
|
%body.embed
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
||||||
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
||||||
= render partial: 'layouts/theme', object: @core || { pack: 'common' }
|
|
||||||
= render partial: 'layouts/theme', object: @theme || { pack: 'error', flavour: 'glitch', common: { pack: 'common', flavour: 'glitch', skin: 'default' } }
|
= render partial: 'layouts/theme', object: @theme || { pack: 'error', flavour: 'glitch', common: { pack: 'common', flavour: 'glitch', skin: 'default' } }
|
||||||
%body.error
|
%body.error
|
||||||
.dialog
|
.dialog
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
</o:OfficeDocumentSettings>
|
</o:OfficeDocumentSettings>
|
||||||
</xml>
|
</xml>
|
||||||
= stylesheet_pack_tag 'core/mailer'
|
= stylesheet_pack_tag 'flavours/vanilla/mailer'
|
||||||
%body
|
%body
|
||||||
.email{ dir: locale_direction }
|
.email{ dir: locale_direction }
|
||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
= preload_pack_asset "locales/#{@theme[:flavour]}/#{I18n.locale}-json.js"
|
= preload_pack_asset "locales/#{@theme[:flavour]}/#{I18n.locale}-json.js"
|
||||||
- elsif @theme[:supported_locales].include? 'en'
|
- elsif @theme[:supported_locales].include? 'en'
|
||||||
= preload_pack_asset "locales/#{@theme[:flavour]}/en-json.js"
|
= preload_pack_asset "locales/#{@theme[:flavour]}/en-json.js"
|
||||||
= render partial: 'layouts/theme', object: @core
|
|
||||||
= render partial: 'layouts/theme', object: @theme
|
= render partial: 'layouts/theme', object: @theme
|
||||||
|
|
||||||
:ruby
|
:ruby
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
%meta{ name: 'robots', content: 'noindex' }/
|
%meta{ name: 'robots', content: 'noindex' }/
|
||||||
|
|
||||||
= javascript_pack_tag 'core/remote_interaction_helper', crossorigin: 'anonymous'
|
= javascript_pack_tag 'flavours/vanilla/remote_interaction_helper', crossorigin: 'anonymous'
|
||||||
|
@ -13,15 +13,6 @@ const flavourFiles = glob.sync('app/javascript/flavours/*/theme.yml');
|
|||||||
const skinFiles = glob.sync('app/javascript/skins/*/*');
|
const skinFiles = glob.sync('app/javascript/skins/*/*');
|
||||||
const flavours = {};
|
const flavours = {};
|
||||||
|
|
||||||
const core = function () {
|
|
||||||
const coreFile = resolve('app', 'javascript', 'core', 'theme.yml');
|
|
||||||
const data = load(readFileSync(coreFile), 'utf8');
|
|
||||||
if (!data.pack_directory) {
|
|
||||||
data.pack_directory = dirname(coreFile);
|
|
||||||
}
|
|
||||||
return data.pack ? data : {};
|
|
||||||
}();
|
|
||||||
|
|
||||||
flavourFiles.forEach((flavourFile) => {
|
flavourFiles.forEach((flavourFile) => {
|
||||||
const data = load(readFileSync(flavourFile), 'utf8');
|
const data = load(readFileSync(flavourFile), 'utf8');
|
||||||
data.name = basename(dirname(flavourFile));
|
data.name = basename(dirname(flavourFile));
|
||||||
@ -62,7 +53,6 @@ const output = {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
settings,
|
settings,
|
||||||
core,
|
|
||||||
flavours,
|
flavours,
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: env.NODE_ENV,
|
NODE_ENV: env.NODE_ENV,
|
||||||
|
@ -7,7 +7,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const AssetsManifestPlugin = require('webpack-assets-manifest');
|
const AssetsManifestPlugin = require('webpack-assets-manifest');
|
||||||
|
|
||||||
const { env, settings, core, flavours, output } = require('./configuration');
|
const { env, settings, flavours, output } = require('./configuration');
|
||||||
const rules = require('./rules');
|
const rules = require('./rules');
|
||||||
|
|
||||||
function reducePacks (data, into = {}) {
|
function reducePacks (data, into = {}) {
|
||||||
@ -26,7 +26,7 @@ function reducePacks (data, into = {}) {
|
|||||||
packFiles = [pack.filename];
|
packFiles = [pack.filename];
|
||||||
|
|
||||||
if (packFiles) {
|
if (packFiles) {
|
||||||
into[data.name ? `flavours/${data.name}/${entry}` : `core/${entry}`] = packFiles.map(packFile => resolve(data.pack_directory, packFile));
|
into[`flavours/${data.name}/${entry}`] = packFiles.map(packFile => resolve(data.pack_directory, packFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,6 @@ function reducePacks (data, into = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entries = Object.assign(
|
const entries = Object.assign(
|
||||||
reducePacks(core),
|
|
||||||
Object.values(flavours).reduce((map, data) => reducePacks(data, map), {}),
|
Object.values(flavours).reduce((map, data) => reducePacks(data, map), {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user