esifawafwu
This commit is contained in:
+77
-11
@@ -369,6 +369,55 @@
|
||||
return finishData;
|
||||
}
|
||||
|
||||
// ── Add a passkey to the current anonymous identity ───────────────────
|
||||
|
||||
async addPasskey() {
|
||||
if (!this.supported) throw new Error('WebAuthn / Passkeys are not supported in this browser.');
|
||||
|
||||
const csrfToken = (window.f0ckSession && window.f0ckSession.csrf_token)
|
||||
|| document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
const beginRes = await fetch('/api/v2/anon/passkey/add/begin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
const beginData = await beginRes.json();
|
||||
if (!beginData.success) throw new Error(beginData.msg || 'Server error');
|
||||
|
||||
const rawChallenge = beginData.options.challenge;
|
||||
const opts = beginData.options;
|
||||
|
||||
const cred = await navigator.credentials.create({ publicKey: {
|
||||
rp: opts.rp,
|
||||
user: {
|
||||
id: b64urlToArr(opts.user.id),
|
||||
name: opts.user.name,
|
||||
displayName: opts.user.displayName
|
||||
},
|
||||
challenge: b64urlToArr(rawChallenge),
|
||||
pubKeyCredParams: opts.pubKeyCredParams,
|
||||
timeout: opts.timeout || 60000,
|
||||
excludeCredentials: (opts.excludeCredentials || []).map(c => ({ type: c.type, id: b64urlToArr(c.id) })),
|
||||
authenticatorSelection: opts.authenticatorSelection,
|
||||
attestation: opts.attestation || 'none'
|
||||
}});
|
||||
|
||||
const finishRes = await fetch('/api/v2/anon/passkey/add/finish', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken },
|
||||
body: JSON.stringify({
|
||||
challenge: rawChallenge,
|
||||
credentialId: arrToB64url(cred.rawId),
|
||||
clientDataJSON: arrToB64url(cred.response.clientDataJSON),
|
||||
attestationObject: arrToB64url(cred.response.attestationObject),
|
||||
hw_fingerprint: await getHardwareFingerprint()
|
||||
})
|
||||
});
|
||||
const finishData = await finishRes.json();
|
||||
if (!finishData.success) throw new Error(finishData.msg || 'Registration failed');
|
||||
return finishData;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
_hasLocalPasskey() {
|
||||
@@ -490,12 +539,26 @@
|
||||
const res = await fetch('/api/v2/anon/identity');
|
||||
const data = await res.json();
|
||||
const fpEl = document.getElementById('anon-pk-fp-display');
|
||||
const credEl = document.getElementById('anon-pk-cred-display');
|
||||
if (fpEl) fpEl.textContent = data.fingerprint ? data.fingerprint.slice(7, 15) : 'none';
|
||||
if (credEl) credEl.textContent = data.credential_id || '—';
|
||||
this._renderPasskeyCount(data.passkey_count, data.passkey_max);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_renderPasskeyCount(count, max) {
|
||||
const countEl = document.getElementById('anon-pk-count');
|
||||
const maxEl = document.getElementById('anon-pk-max');
|
||||
const addBtn = document.getElementById('anon-pk-add-btn');
|
||||
if (countEl && typeof count === 'number') countEl.textContent = count;
|
||||
if (maxEl && typeof max === 'number') maxEl.textContent = max;
|
||||
if (addBtn && typeof count === 'number' && typeof max === 'number') {
|
||||
const full = count >= max;
|
||||
addBtn.disabled = full;
|
||||
addBtn.innerHTML = full
|
||||
? '<i class="fa-solid fa-lock"></i> Limit reached'
|
||||
: '<i class="fa-solid fa-plus"></i> Add passkey';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async init() {
|
||||
@@ -568,24 +631,27 @@
|
||||
const modalOverlay = document.getElementById('anon-passkey-modal');
|
||||
if (modalOverlay) modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) this.closeModal(); });
|
||||
|
||||
// Add-passkey button inside modal (for anonymous users to add a second passkey)
|
||||
// Add-passkey button inside modal — attaches a new passkey to this identity (max enforced server-side)
|
||||
const addBtn = document.getElementById('anon-pk-add-btn');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', async () => {
|
||||
const errEl = document.getElementById('anon-pk-error');
|
||||
if (errEl) errEl.style.display = 'none';
|
||||
addBtn.disabled = true;
|
||||
try {
|
||||
await this.register();
|
||||
if (typeof window.showToastNotification === 'function') window.showToastNotification('New passkey registered!');
|
||||
this._refreshModalContent();
|
||||
const data = await this.addPasskey();
|
||||
if (typeof window.showToastNotification === 'function') window.showToastNotification('Passkey added');
|
||||
this._renderPasskeyCount(data.passkey_count, data.passkey_max);
|
||||
} catch (err) {
|
||||
console.warn('[ANON_PASSKEY] Modal add passkey error:', err);
|
||||
if (err && err.name === 'NotAllowedError') {
|
||||
alert('Passkey creation was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it.');
|
||||
} else {
|
||||
alert('Failed to add passkey: ' + ((err && err.message) || 'Unknown error'));
|
||||
if (errEl) {
|
||||
errEl.style.display = '';
|
||||
errEl.textContent = (err && err.name === 'NotAllowedError')
|
||||
? 'Cancelled or blocked. Unlock your password manager and try again.'
|
||||
: ((err && err.message) || 'Failed to add passkey.');
|
||||
}
|
||||
} finally {
|
||||
addBtn.disabled = false;
|
||||
this._refreshModalContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+39
-14
@@ -1,3 +1,12 @@
|
||||
// Pages whose content depends on the rating/mime filter and must reload when it changes.
|
||||
// Profile pages count even when both previews are empty (then no .posts grid is rendered).
|
||||
// Elements whose horizontal drags belong to them (sliders, sideways-scrolling rows): swipe gestures ignore touches starting here.
|
||||
window.F0CK_NO_SWIPE_SELECTOR = 'input[type="range"], .f0ck-tuner-subtabs-nav, .f0ck-tuner-mode-bar, [data-no-swipe]';
|
||||
|
||||
window.isFilterReloadableView = function () {
|
||||
return !!document.querySelector('.posts, .tags-grid') || /^\/user\/[^/]+\/?$/.test(window.location.pathname);
|
||||
};
|
||||
|
||||
|
||||
// Normalize percent-encoded characters in the URL bar that are safe to show decoded.
|
||||
// Runs immediately so the address bar is clean before any other JS runs.
|
||||
@@ -1437,7 +1446,7 @@ window.cancelAnimFrame = (function () {
|
||||
window.flashMessage('ALL MODE ACTIVATED');
|
||||
gridCacheMap.clear();
|
||||
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
|
||||
const isGridView = document.querySelector('.posts, .tags-grid');
|
||||
const isGridView = window.isFilterReloadableView();
|
||||
const isItemView = document.getElementById('prev') || document.getElementById('next') || (typeof isItemPath === 'function' ? isItemPath(window.location.pathname) : (!window.location.pathname.startsWith('/4/') && /^\/\d+(?:[?#]|$)/.test(window.location.pathname)));
|
||||
let reloadPromise = null;
|
||||
if (isOnaraOpen) {
|
||||
@@ -1501,7 +1510,7 @@ window.cancelAnimFrame = (function () {
|
||||
window.flashMessage(label);
|
||||
gridCacheMap.clear();
|
||||
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
|
||||
const isGridView = document.querySelector('.posts, .tags-grid');
|
||||
const isGridView = window.isFilterReloadableView();
|
||||
const isItemView = document.getElementById('prev') || document.getElementById('next') || (typeof isItemPath === 'function' ? isItemPath(window.location.pathname) : (!window.location.pathname.startsWith('/4/') && /^\/\d+(?:[?#]|$)/.test(window.location.pathname)));
|
||||
let reloadPromise = null;
|
||||
if (isOnaraOpen) {
|
||||
@@ -13173,7 +13182,7 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
// Refresh content
|
||||
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
|
||||
const isGridView = document.querySelector('.posts, .tags-grid');
|
||||
const isGridView = window.isFilterReloadableView();
|
||||
const isItemView = document.getElementById('prev') || document.getElementById('next');
|
||||
|
||||
let reloadPromise = null;
|
||||
@@ -13789,6 +13798,7 @@ window.cancelAnimFrame = (function () {
|
||||
}, 50);
|
||||
|
||||
const updateDomAfterSave = (resData) => {
|
||||
const itemPath = (resData && resData.item_path) || itemId;
|
||||
try {
|
||||
const finalRating = resData.rating || selectedRating || 'untagged';
|
||||
|
||||
@@ -13838,11 +13848,11 @@ window.cancelAnimFrame = (function () {
|
||||
sep.textContent = '→';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = `/${itemId}`;
|
||||
a.href = `/${itemPath}`;
|
||||
a.className = 'location-link is-rehosted';
|
||||
a.title = `View rehosted post #${itemId} on f0ckm`;
|
||||
a.title = `View rehosted post /${itemPath} on f0ckm`;
|
||||
a.style.cssText = 'color: #4ade80; font-weight: 700;';
|
||||
a.innerHTML = `<i class="fa-solid fa-check"></i> #${itemId}`;
|
||||
a.innerHTML = `<i class="fa-solid fa-check"></i> /${itemPath}`;
|
||||
|
||||
locEl.appendChild(sep);
|
||||
locEl.appendChild(a);
|
||||
@@ -13860,8 +13870,8 @@ window.cancelAnimFrame = (function () {
|
||||
if (rehostBtn) {
|
||||
rehostBtn.outerHTML = `
|
||||
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
|
||||
<a href="/${itemId}" class="chan-rehost-action-btn rehosted" title="View Post #${itemId}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
|
||||
<i class="fa-solid fa-check"></i> #${itemId}
|
||||
<a href="/${itemPath}" class="chan-rehost-action-btn rehosted" title="View /${itemPath}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
|
||||
<i class="fa-solid fa-check"></i> /${itemPath}
|
||||
</a>
|
||||
<button type="button" class="chan-rehost-edit-btn" data-item-id="${itemId}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
@@ -13985,8 +13995,8 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
btn.outerHTML = `
|
||||
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
|
||||
<a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
|
||||
<i class="fa-solid fa-check"></i> #${data.item_id}
|
||||
<a href="/${data.item_path || data.item_id}" class="chan-rehost-action-btn rehosted" title="View /${data.item_path || data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
|
||||
<i class="fa-solid fa-check"></i> /${data.item_path || data.item_id}
|
||||
</a>
|
||||
<button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
@@ -13997,7 +14007,7 @@ window.cancelAnimFrame = (function () {
|
||||
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
|
||||
}
|
||||
if (typeof window.openChanRehostModal === 'function') {
|
||||
const newAnchor = document.querySelector(`.chan-rehost-edit-btn[data-item-id="${data.item_id}"]`) || document.querySelector(`.chan-rehost-action-btn[href="/${data.item_id}"]`);
|
||||
const newAnchor = document.querySelector(`.chan-rehost-edit-btn[data-item-id="${data.item_id}"]`) || document.querySelector(`.chan-rehost-action-btn[href="/${data.item_path || data.item_id}"]`);
|
||||
window.openChanRehostModal({ itemId: data.item_id, anchorEl: newAnchor });
|
||||
}
|
||||
} else {
|
||||
@@ -16561,6 +16571,19 @@ window.cancelAnimFrame = (function () {
|
||||
document.addEventListener('touchstart', e => {
|
||||
if (window.f0ckSession?.disable_swiping) return;
|
||||
if (document.body.classList.contains('modal-open')) return;
|
||||
|
||||
// Sliders and horizontally scrolling rows own their horizontal drags: never start a
|
||||
// sidebar-drag or item-swipe from them (the swipe's preventDefault would freeze the slider)
|
||||
if (e.target?.closest?.(window.F0CK_NO_SWIPE_SELECTOR)) {
|
||||
swipeRT.xDown = null;
|
||||
swipeRT.yDown = null;
|
||||
swipeRT.xDiff = 0;
|
||||
swipeRT.yDiff = 0;
|
||||
swipeRT.isSidebarCandidate = false;
|
||||
swipeRT.isDraggingSidebar = false;
|
||||
swipeRT.startedInSidebarZone = true; // also blocks prev/next navigation on touchend
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = e.touches[0];
|
||||
|
||||
@@ -17474,13 +17497,15 @@ window.cancelAnimFrame = (function () {
|
||||
if (document.body.classList.contains('modal-open')) return;
|
||||
if (!isLSActive()) return;
|
||||
|
||||
swipeRT.isDraggingLeftSidebar = false;
|
||||
swipeRT.isLeftSidebarCandidate = false;
|
||||
if (e.target?.closest?.(window.F0CK_NO_SWIPE_SELECTOR)) return;
|
||||
|
||||
const { sb, layout } = getLS();
|
||||
if (!sb || !layout) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const isHidden = layout.classList.contains('sidebar-hidden');
|
||||
swipeRT.isDraggingLeftSidebar = false;
|
||||
swipeRT.isLeftSidebarCandidate = false;
|
||||
|
||||
if (isHidden) {
|
||||
if (touch.clientX < 60) {
|
||||
@@ -20145,7 +20170,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// Trigger reload context-aware
|
||||
const fromModal = !!menu.closest('#excluded-tags-overlay');
|
||||
const postsEl = document.querySelector('.posts, .tags-grid');
|
||||
const postsEl = window.isFilterReloadableView();
|
||||
let reloadPromise = null;
|
||||
|
||||
// Set flag so hideAllModals (called inside loadPageAjax) skips the filter overlay
|
||||
|
||||
@@ -1455,7 +1455,7 @@
|
||||
${item.is_external && item.external_board && item.external_tid
|
||||
? `<a class="scroll-id-link" href="https://boards.4chan.org/${item.external_board}/thread/${item.external_tid}#p${item.external_id}" target="_blank">/${item.external_board}/thread/${item.external_tid}</a>`
|
||||
: (item.local_id
|
||||
? `<a class="scroll-id-link" href="/${item.local_id}" target="_blank">#${item.local_id}</a>`
|
||||
? `<a class="scroll-id-link" href="/${item.local_path || item.local_id}" target="_blank">/${item.local_path || item.local_id}</a>`
|
||||
: `<a class="scroll-id-link" href="/abyss/${item.id}">#${item.id}</a>`)}
|
||||
</div>`;
|
||||
slide.appendChild(meta);
|
||||
@@ -1486,7 +1486,7 @@
|
||||
</button>
|
||||
${item.is_external ? (
|
||||
item.local_id
|
||||
? `<a class="scroll-btn rehost-btn success" href="/${item.local_id}" target="_blank" title="${_i.already_added || 'Already added'}">
|
||||
? `<a class="scroll-btn rehost-btn success" href="/${item.local_path || item.local_id}" target="_blank" title="${_i.already_added || 'Already added'}">
|
||||
<div class="scroll-btn-icon"><i class="fa-solid fa-check"></i></div>
|
||||
<span class="scroll-btn-label">${_i.view_label || 'View'}</span>
|
||||
</a>`
|
||||
@@ -1761,6 +1761,7 @@
|
||||
|
||||
const external_media_url = `https://i.4cdn.org/${data.board}/${p.tim}${ext}`;
|
||||
const local_id = allRehosts[external_media_url] || null;
|
||||
const local_path = (local_id && data.rehost_paths && data.rehost_paths[local_id]) || local_id;
|
||||
|
||||
return {
|
||||
id: `${data.board}/${p.no}`,
|
||||
@@ -1770,6 +1771,7 @@
|
||||
external_source: '4chan',
|
||||
is_external: true,
|
||||
local_id,
|
||||
local_path,
|
||||
external_media_url,
|
||||
mime: isVideo ? 'video/unknown' : (isImage ? 'image/unknown' : 'application/octet-stream'),
|
||||
dest: `/api/v2/scroller/external/4chan/${data.board}/media/${p.tim}${ext}`,
|
||||
@@ -1972,7 +1974,7 @@
|
||||
// Update button to link to the new site-internal post
|
||||
setTimeout(() => {
|
||||
btn.outerHTML = `
|
||||
<a href="/${data.item_id}" target="_blank" class="scroll-btn rehost-btn success" style="text-decoration:none;">
|
||||
<a href="/${data.item_path || data.item_id}" target="_blank" class="scroll-btn rehost-btn success" style="text-decoration:none;">
|
||||
<div class="scroll-btn-icon"><i class="fa-solid fa-arrow-up-right-from-square"></i></div>
|
||||
<span class="scroll-btn-label">View</span>
|
||||
</a>
|
||||
@@ -1983,8 +1985,8 @@
|
||||
if (slide) {
|
||||
const idLink = slide.querySelector('.scroll-id-link');
|
||||
if (idLink) {
|
||||
idLink.href = `/${data.item_id}`;
|
||||
idLink.textContent = `#${data.item_id}`;
|
||||
idLink.href = `/${data.item_path || data.item_id}`;
|
||||
idLink.textContent = `/${data.item_path || data.item_id}`;
|
||||
}
|
||||
// Reflect rehoster's username and local timestamp
|
||||
if (window.scrollerUsername) {
|
||||
|
||||
@@ -3401,7 +3401,7 @@ window.initUploadForm = (selector) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRedirectToItem) {
|
||||
if (shouldRedirectToItem && !lastData?.manual_approval) {
|
||||
const isMultiNonAlbumShitpost = isShitpost && !isAlbum && (uploadedResults.length > 1 || successCount > 1 || totalFilesToUpload > 1);
|
||||
let targetUrl;
|
||||
if (isMultiNonAlbumShitpost) {
|
||||
@@ -3416,7 +3416,7 @@ window.initUploadForm = (selector) => {
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
}
|
||||
// else: stay on current page
|
||||
// else: stay on current page (including manual_approval pending)
|
||||
}
|
||||
} else {
|
||||
restoreBtn();
|
||||
|
||||
+5
-2
@@ -60,6 +60,9 @@
|
||||
(window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase())
|
||||
);
|
||||
|
||||
// Anonymous uploaders may change the rating of their own item without full manage rights
|
||||
const canRate = canManage || !!document.querySelector('#tags[data-can-rate="true"]');
|
||||
|
||||
// Only remove existing dynamically generated tags
|
||||
[...inner.querySelectorAll(".badge")].forEach(tag => {
|
||||
// Don't remove the one containing the add/toggle buttons, and don't remove the autocomplete input itself
|
||||
@@ -100,7 +103,7 @@
|
||||
span.classList.add('rating-tag', `is-${tag.normalized}`);
|
||||
span.dataset.rating = tag.normalized;
|
||||
if (activePostId) span.dataset.itemId = activePostId;
|
||||
if (canManage) {
|
||||
if (canRate) {
|
||||
span.classList.add('can-cycle');
|
||||
}
|
||||
} else {
|
||||
@@ -144,7 +147,7 @@
|
||||
const hasRating = !!lastRatingTag;
|
||||
if (!hasRating) {
|
||||
const untaggedSpan = document.createElement("span");
|
||||
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManage ? ' can-cycle' : ''}`;
|
||||
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canRate ? ' can-cycle' : ''}`;
|
||||
untaggedSpan.dataset.rating = 'untagged';
|
||||
if (activePostId) untaggedSpan.dataset.itemId = activePostId;
|
||||
const lbl = document.createElement("span");
|
||||
|
||||
Reference in New Issue
Block a user