XT 2.2.0: ability to restore posts from external archives
This commit is contained in:
parent
f5ba6a0941
commit
b047925392
@ -3,8 +3,10 @@
|
||||
4chan XT uses a different user script namespace, so to migrate you need to export settings from 4chan X, and import them
|
||||
in XT.
|
||||
|
||||
### Unreleased
|
||||
### XT v2.2.0 (2023-10-27)
|
||||
|
||||
- Added ability to restore deleted posts from an external archive. This can be found in the drop down menu at the top
|
||||
right. [#8](https://github.com/TuxedoTako/4chan-xt/issues/8)
|
||||
- Also minify css in the minified build.
|
||||
|
||||
### XT v2.1.4 (2023-09-02)
|
||||
|
||||
File diff suppressed because one or more lines are too long
831
builds/4chan-XT-noupdate.user.min.js
vendored
831
builds/4chan-XT-noupdate.user.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "4chan XT",
|
||||
"version": "XT 2.1.4",
|
||||
"version": "XT 2.2.0",
|
||||
"manifest_version": 2,
|
||||
"description": "4chan XT is a script that adds various features to anonymous imageboards.",
|
||||
"icons": {
|
||||
|
||||
4141
builds/crx/script.js
4141
builds/crx/script.js
File diff suppressed because it is too large
Load Diff
176
src/Archive/Parse.ts
Normal file
176
src/Archive/Parse.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import Redirect from './Redirect';
|
||||
import { isEscaped } from '../globals/jsx';
|
||||
import Main from '../main/Main';
|
||||
import ImageHost from '../Images/ImageHost';
|
||||
import Board from '../classes/Board';
|
||||
import Fetcher from '../classes/Fetcher';
|
||||
import Post, { type File } from '../classes/Post';
|
||||
import Thread from '../classes/Thread';
|
||||
import { E, g } from '../globals/globals';
|
||||
import { dict } from '../platform/helpers';
|
||||
import $ from '../platform/$';
|
||||
|
||||
// Got this from just putting a response in a json to ts converter, it might be incomplete.
|
||||
export interface RawArchivePost {
|
||||
doc_id: string;
|
||||
num: string;
|
||||
subnum: string;
|
||||
thread_num: string;
|
||||
op: string;
|
||||
timestamp: number;
|
||||
timestamp_expired: string;
|
||||
capcode: string;
|
||||
email: any;
|
||||
name: string;
|
||||
trip: any;
|
||||
title: any;
|
||||
comment: string;
|
||||
poster_hash: any;
|
||||
poster_country?: string;
|
||||
troll_country_code?: string;
|
||||
sticky: string;
|
||||
locked: string;
|
||||
deleted: string;
|
||||
nreplies: any;
|
||||
nimages: any;
|
||||
fourchan_date: string;
|
||||
comment_sanitized: string;
|
||||
comment_processed: string;
|
||||
formatted: boolean;
|
||||
title_processed: any;
|
||||
name_processed: string;
|
||||
email_processed: any;
|
||||
trip_processed: any;
|
||||
poster_hash_processed: any;
|
||||
poster_country_name: boolean;
|
||||
poster_country_name_processed: string;
|
||||
extra_data: any[];
|
||||
exif?: string;
|
||||
media: {
|
||||
media_id: string;
|
||||
spoiler: string;
|
||||
preview_orig: string;
|
||||
media: string;
|
||||
preview_op: any;
|
||||
preview_reply: string;
|
||||
preview_w: string;
|
||||
preview_h: string;
|
||||
media_filename: string;
|
||||
media_w: string;
|
||||
media_h: string;
|
||||
media_size: string;
|
||||
media_hash: string;
|
||||
media_orig: string;
|
||||
exif: any;
|
||||
total: string;
|
||||
banned: string;
|
||||
media_status: string;
|
||||
safe_media_hash: string;
|
||||
remote_media_link: string;
|
||||
media_link: string;
|
||||
thumb_link: string;
|
||||
media_filename_processed: string;
|
||||
};
|
||||
board: {
|
||||
name: string;
|
||||
shortname: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const parseArchivePost = (data: RawArchivePost) => {
|
||||
// https://github.com/eksopl/asagi/blob/v0.4.0b74/src/main/java/net/easymodo/asagi/YotsubaAbstract.java#L82-L129
|
||||
// https://github.com/FoolCode/FoolFuuka/blob/800bd090835489e7e24371186db6e336f04b85c0/src/Model/Comment.php#L368-L428
|
||||
// https://github.com/bstats/b-stats/blob/6abe7bffaf6e5f523498d760e54b110df5331fbb/inc/classes/Yotsuba.php#L157-L168
|
||||
let comment = (data.comment || '').split(/(\n|\[\/?(?:b|spoiler|code|moot|banned|fortune(?: color="#\w+")?|i|red|green|blue)\])/);
|
||||
comment = comment.map((text, i) => {
|
||||
if ((i % 2) === 1) {
|
||||
var tag = Fetcher.archiveTags[text.replace(/\ .*\]/, ']')];
|
||||
return (typeof tag === 'function') ? tag(text) : tag;
|
||||
} else {
|
||||
var greentext = text[0] === '>';
|
||||
text = text
|
||||
.replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2')
|
||||
.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g)
|
||||
.map((text2, j) => ((j % 2) ? `<span class="deadlink">${E(text2)}</span>` : E(text2)))
|
||||
.join('');
|
||||
return { innerHTML: (greentext ? `<span class="quote">${text}</span>` : text) };
|
||||
}
|
||||
});
|
||||
comment = { innerHTML: E.cat(comment), [isEscaped]: true };
|
||||
|
||||
const o = {
|
||||
ID: data.num,
|
||||
threadID: data.thread_num,
|
||||
boardID: data.board.shortname,
|
||||
isReply: data.num !== data.thread_num,
|
||||
fileDeleted: false,
|
||||
info: {
|
||||
subject: data.title,
|
||||
email: data.email,
|
||||
name: data.name || '',
|
||||
tripcode: data.trip,
|
||||
capcode: (() => {
|
||||
switch (data.capcode) {
|
||||
// https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77
|
||||
case 'M': return 'Mod';
|
||||
case 'A': return 'Admin';
|
||||
case 'D': return 'Developer';
|
||||
case 'V': return 'Verified';
|
||||
case 'F': return 'Founder';
|
||||
case 'G': return 'Manager';
|
||||
}
|
||||
})(),
|
||||
uniqueID: data.poster_hash,
|
||||
flagCode: data.poster_country,
|
||||
flagCodeTroll: data.troll_country_code,
|
||||
flag: data.poster_country_name || data.troll_country_name,
|
||||
dateUTC: data.timestamp,
|
||||
dateText: data.fourchan_date,
|
||||
commentHTML: comment,
|
||||
},
|
||||
file: null as File,
|
||||
extra: null as any,
|
||||
};
|
||||
if (o.info.capcode) { delete o.info.uniqueID; }
|
||||
if (data.media && !!+data.media.banned) {
|
||||
o.fileDeleted = true;
|
||||
} else if (data.media?.media_filename) {
|
||||
let { thumb_link } = data.media;
|
||||
// Fix URLs missing origin
|
||||
if (thumb_link?.[0] === '/') { thumb_link = url.split('/', 3).join('/') + thumb_link; }
|
||||
if (!Redirect.securityCheck(thumb_link)) { thumb_link = ''; }
|
||||
let media_link = Redirect.to('file', { boardID: o.boardID, filename: data.media.media_orig });
|
||||
if (!Redirect.securityCheck(media_link)) { media_link = ''; }
|
||||
o.file = {
|
||||
name: data.media.media_filename,
|
||||
url: media_link ||
|
||||
(o.boardID === 'f' ?
|
||||
`${location.protocol}//${ImageHost.flashHost()}/${o.boardID}/${encodeURIComponent(E(data.media.media_filename))}`
|
||||
:
|
||||
`${location.protocol}//${ImageHost.host()}/${o.boardID}/${data.media.media_orig}`),
|
||||
height: data.media.media_h,
|
||||
width: data.media.media_w,
|
||||
MD5: data.media.media_hash,
|
||||
size: $.bytesToString(data.media.media_size),
|
||||
thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${o.boardID}/${data.media.preview_orig}`,
|
||||
theight: data.media.preview_h,
|
||||
twidth: data.media.preview_w,
|
||||
isSpoiler: data.media.spoiler === '1'
|
||||
};
|
||||
if (!/\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}`; }
|
||||
if ((o.boardID === 'f') && data.media.exif) { o.file.tag = JSON.parse(data.media.exif).Tag; }
|
||||
}
|
||||
o.extra = dict();
|
||||
|
||||
const board = g.boards[o.boardID] ||
|
||||
new Board(o.boardID);
|
||||
const thread = g.threads.get(`${o.boardID}.${o.threadID}`) ||
|
||||
new Thread(o.threadID, board);
|
||||
const post = new Post(g.SITE.Build.post(o), thread, board, { isFetchedQuote: true });
|
||||
post.kill();
|
||||
if (post.file) { post.file.thumbURL = o.file.thumbURL; }
|
||||
Main.callbackNodes('Post', [post]);
|
||||
return post;
|
||||
};
|
||||
export default parseArchivePost;
|
||||
@ -1,4 +1,6 @@
|
||||
import Notice from '../classes/Notice.js';
|
||||
import type { default as Post, File } from '../classes/Post.js';
|
||||
import type Thread from '../classes/Thread.js';
|
||||
import { Conf } from '../globals/globals.js';
|
||||
import $ from '../platform/$.js';
|
||||
import CrossOrigin from '../platform/CrossOrigin.js';
|
||||
@ -12,6 +14,11 @@ import archives from './archives.json';
|
||||
|
||||
var Redirect = {
|
||||
archives,
|
||||
data: null as {
|
||||
thread: Record<any, Thread>,
|
||||
post: Record<any, Post>,
|
||||
file: Record<any, File>,
|
||||
},
|
||||
|
||||
init() {
|
||||
this.selectArchives();
|
||||
@ -128,8 +135,12 @@ var Redirect = {
|
||||
return cb?.();
|
||||
},
|
||||
|
||||
to(dest, data) {
|
||||
const archive = (['search', 'board'].includes(dest) ? Redirect.data.thread : Redirect.data[dest])[data.boardID];
|
||||
to(
|
||||
dest: 'post' | 'thread' | 'threadJSON' | 'file' | 'board' | 'search',
|
||||
data: { boardID: string, threadID?: string | number, postID?: string | number }
|
||||
): string {
|
||||
const archive =
|
||||
(['search', 'board', 'threadJSON'].includes(dest) ? Redirect.data.thread : Redirect.data[dest])[data.boardID];
|
||||
if (!archive) { return ''; }
|
||||
return Redirect[dest](archive, data);
|
||||
},
|
||||
@ -162,6 +173,10 @@ var Redirect = {
|
||||
return `${Redirect.protocol(archive)}${archive.domain}/${path}`;
|
||||
},
|
||||
|
||||
threadJSON(archive, { boardID, threadID }) {
|
||||
return `${Redirect.protocol(archive)}${archive.domain}/_/api/chan/thread/?board=${boardID}&num=${threadID}`;
|
||||
},
|
||||
|
||||
post(archive, {boardID, postID}) {
|
||||
// For fuuka-based archives:
|
||||
// https://github.com/eksopl/fuuka/issues/27
|
||||
|
||||
80
src/Archive/RestoreDeletedFromArchive.ts
Normal file
80
src/Archive/RestoreDeletedFromArchive.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import Redirect from './Redirect';
|
||||
import Notice from '../classes/Notice';
|
||||
import { Conf, g } from '../globals/globals';
|
||||
import CrossOrigin from '../platform/CrossOrigin';
|
||||
import $ from '../platform/$';
|
||||
import Header from '../General/Header';
|
||||
import { type RawArchivePost, parseArchivePost } from './Parse';
|
||||
import QuoteThreading from '../Quotelinks/QuoteThreading';
|
||||
|
||||
const RestoreDeletedFromArchive = {
|
||||
restore() {
|
||||
console.log(g);
|
||||
const url = Redirect.to('threadJSON', { boardID: g.boardID, threadID: g.threadID });
|
||||
console.log(url);
|
||||
if (!url) {
|
||||
new Notice('warning', 'No archive found', 3);
|
||||
return;
|
||||
}
|
||||
const encryptionOK = url.startsWith('https://');
|
||||
if (encryptionOK || Conf['Exempt Archives from Encryption']) {
|
||||
CrossOrigin.cache(url, function (this: XMLHttpRequest) {
|
||||
console.log(this);
|
||||
let nrRestored = 0;
|
||||
const archivePosts = this.response[g.threadID.toString()].posts as Record<string, RawArchivePost>;
|
||||
for (const [postID, raw] of Object.entries(archivePosts)) {
|
||||
const key = `${g.boardID}.${postID}`
|
||||
if (!g.posts.keys.includes(key)) {
|
||||
const postIdNr = +postID;
|
||||
let indexOfNext = g.posts.keys.findIndex(key => +(key.split('.')[1]) > postIdNr);
|
||||
if (indexOfNext === -1) {
|
||||
indexOfNext = g.posts.keys.length;
|
||||
};
|
||||
const newPost = parseArchivePost(raw);
|
||||
newPost.kill()
|
||||
g.posts.push(key, newPost);
|
||||
// move key to right position
|
||||
g.posts.keys.pop();
|
||||
g.posts.keys.splice(indexOfNext, 0, key);
|
||||
|
||||
if (!QuoteThreading.insert(newPost)) {
|
||||
g.posts.get(g.posts.keys[indexOfNext - 1]).root.insertAdjacentElement('afterend', newPost.root);
|
||||
}
|
||||
|
||||
++nrRestored;
|
||||
}
|
||||
}
|
||||
|
||||
let msg: string;
|
||||
if (nrRestored === 0) {
|
||||
msg = 'No removed posts found';
|
||||
} else if (nrRestored === 1) {
|
||||
msg = '1 post restored';
|
||||
} else {
|
||||
msg = `${nrRestored} posts restored`;
|
||||
}
|
||||
|
||||
new Notice('info', msg, 3);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
if (g.VIEW !== 'thread') return;
|
||||
|
||||
const menuEntry = $.el('a', {
|
||||
href: 'javascript:;',
|
||||
textContent: 'Restore from archive',
|
||||
});
|
||||
$.on(menuEntry, 'click', () => {
|
||||
RestoreDeletedFromArchive.restore();
|
||||
Header.menu.close();
|
||||
});
|
||||
Header.menu.addEntry({
|
||||
el: menuEntry,
|
||||
order: 10,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
export default RestoreDeletedFromArchive;
|
||||
@ -12,6 +12,7 @@ import Header from '../General/Header';
|
||||
import { g, Conf, d, doc } from '../globals/globals';
|
||||
import UI from '../General/UI';
|
||||
import { MINUTE, SECOND } from '../platform/helpers';
|
||||
import type Thread from '../classes/Thread';
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
@ -22,8 +23,8 @@ import { MINUTE, SECOND } from '../platform/helpers';
|
||||
*/
|
||||
|
||||
var ThreadUpdater = {
|
||||
init() {
|
||||
let el, name, sc;
|
||||
init(this: typeof ThreadUpdater) {
|
||||
let sc;
|
||||
if ((g.VIEW !== 'thread') || !Conf['Thread Updater']) { return; }
|
||||
this.enabled = true;
|
||||
|
||||
@ -63,9 +64,9 @@ var ThreadUpdater = {
|
||||
$.on(updateLink.firstElementChild, 'click', this.update);
|
||||
|
||||
const subEntries = [];
|
||||
for (name in Config.updater.checkbox) {
|
||||
for (const name in Config.updater.checkbox) {
|
||||
var conf = Config.updater.checkbox[name];
|
||||
el = UI.checkbox(name, name);
|
||||
const el = UI.checkbox(name, name);
|
||||
el.title = conf[1];
|
||||
var input = el.firstElementChild;
|
||||
$.on(input, 'change', $.cb.checked);
|
||||
@ -170,7 +171,7 @@ var ThreadUpdater = {
|
||||
if (e) { return $.cb.value.call(this); }
|
||||
},
|
||||
|
||||
load() {
|
||||
load(this: XMLHttpRequest) {
|
||||
if (this !== ThreadUpdater.req) { return; } // aborted
|
||||
switch (this.status) {
|
||||
case 200:
|
||||
@ -198,13 +199,12 @@ var ThreadUpdater = {
|
||||
confirmed = false;
|
||||
}
|
||||
if (confirmed) {
|
||||
return ThreadUpdater.kill();
|
||||
ThreadUpdater.kill();
|
||||
} else {
|
||||
return ThreadUpdater.error(this);
|
||||
ThreadUpdater.error(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
default:
|
||||
return ThreadUpdater.error(this);
|
||||
}
|
||||
@ -334,11 +334,11 @@ var ThreadUpdater = {
|
||||
return new Notice('info', `The thread is ${change}.`, 30);
|
||||
},
|
||||
|
||||
parse(req) {
|
||||
parse(req: XMLHttpRequest) {
|
||||
let ID, ipCountEl, post;
|
||||
const postObjects = req.response.posts;
|
||||
const OP = postObjects[0];
|
||||
const {thread} = ThreadUpdater;
|
||||
const thread: Thread = ThreadUpdater.thread;
|
||||
const {board} = thread;
|
||||
const lastPost = ThreadUpdater.postIDs[ThreadUpdater.postIDs.length - 1];
|
||||
|
||||
|
||||
@ -161,9 +161,9 @@ var Unread = {
|
||||
},
|
||||
|
||||
addPost() {
|
||||
if (this.isFetchedQuote || this.isClone) { return; }
|
||||
if (this.isFetchedQuote || this.isClone || (this.ID <= Unread.lastReadPost)) return;
|
||||
Unread.order.push(this);
|
||||
if ((this.ID <= Unread.lastReadPost) || this.isHidden || QuoteYou.isYou(this)) { return; }
|
||||
if (this.isHidden || QuoteYou.isYou(this)) return;
|
||||
Unread.posts.add((Unread.posts.last = this.ID));
|
||||
Unread.addPostQuotingYou(this);
|
||||
return Unread.position != null ? Unread.position : (Unread.position = Unread.order[this.ID]);
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import BoardConfig from "../General/BoardConfig";
|
||||
import { d, g } from "../globals/globals";
|
||||
import SimpleDict from "./SimpleDict";
|
||||
import type Post from "./Post";
|
||||
import type Thread from "./Thread";
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
@ -8,6 +10,13 @@ import SimpleDict from "./SimpleDict";
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class Board {
|
||||
declare ID: string;
|
||||
declare boardID: string;
|
||||
declare siteID: string;
|
||||
declare threads: SimpleDict<Thread>;
|
||||
declare posts: SimpleDict<Post>;
|
||||
declare config: any;
|
||||
|
||||
toString() { return this.ID; }
|
||||
|
||||
constructor(ID) {
|
||||
|
||||
@ -6,11 +6,9 @@ import $ from "../platform/$";
|
||||
import Main from "../main/Main";
|
||||
import Index from "../General/Index";
|
||||
import { E, g, Conf, d } from "../globals/globals";
|
||||
import ImageHost from "../Images/ImageHost";
|
||||
import CrossOrigin from "../platform/CrossOrigin";
|
||||
import Get from "../General/Get";
|
||||
import { dict } from "../platform/helpers";
|
||||
import { isEscaped } from "../globals/jsx";
|
||||
import parseArchivePost from "../Archive/Parse";
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
@ -45,8 +43,14 @@ export default class Fetcher {
|
||||
'[/blue]': {innerHTML: "</span>"}
|
||||
};
|
||||
|
||||
constructor(boardID, threadID, postID, root, quoter) {
|
||||
let post, thread;
|
||||
declare boardID: string;
|
||||
declare threadID: number;
|
||||
declare postID: string;
|
||||
declare root: HTMLElement;
|
||||
declare quoter: Post;
|
||||
|
||||
constructor(boardID: string, threadID: number, postID: string, root: HTMLElement, quoter: Post) {
|
||||
let post: Post, thread: Thread;
|
||||
this.boardID = boardID;
|
||||
this.threadID = threadID;
|
||||
this.postID = postID;
|
||||
@ -175,7 +179,7 @@ export default class Fetcher {
|
||||
}
|
||||
|
||||
archivedPost() {
|
||||
let url;
|
||||
let url: string;
|
||||
if (!Conf['Resurrect Quotes']) { return false; }
|
||||
if (!(url = Redirect.to('post', {boardID: this.boardID, postID: this.postID}))) { return false; }
|
||||
const archive = Redirect.data.post[this.boardID];
|
||||
@ -203,7 +207,7 @@ export default class Fetcher {
|
||||
parseArchivedPost(data, url, archive) {
|
||||
// In case of multiple callbacks for the same request,
|
||||
// don't parse the same original post more than once.
|
||||
let post;
|
||||
let post: Post;
|
||||
if (post = g.posts.get(`${this.boardID}.${this.postID}`)) {
|
||||
this.insert(post);
|
||||
return;
|
||||
@ -221,94 +225,8 @@ export default class Fetcher {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/eksopl/asagi/blob/v0.4.0b74/src/main/java/net/easymodo/asagi/YotsubaAbstract.java#L82-L129
|
||||
// https://github.com/FoolCode/FoolFuuka/blob/800bd090835489e7e24371186db6e336f04b85c0/src/Model/Comment.php#L368-L428
|
||||
// https://github.com/bstats/b-stats/blob/6abe7bffaf6e5f523498d760e54b110df5331fbb/inc/classes/Yotsuba.php#L157-L168
|
||||
let comment = (data.comment || '').split(/(\n|\[\/?(?:b|spoiler|code|moot|banned|fortune(?: color="#\w+")?|i|red|green|blue)\])/);
|
||||
comment = comment.map((text, i) => {
|
||||
if ((i % 2) === 1) {
|
||||
var tag = Fetcher.archiveTags[text.replace(/\ .*\]/, ']')];
|
||||
return (typeof tag === 'function') ? tag(text) : tag;
|
||||
} else {
|
||||
var greentext = text[0] === '>';
|
||||
text = text
|
||||
.replace(/(\[\/?[a-z]+):lit(\])/g, '$1$2')
|
||||
.split(/(>>(?:>\/[a-z\d]+\/)?\d+)/g)
|
||||
.map((text2, j) => ((j % 2) ? `<span class="deadlink">${E(text2)}</span>`: E(text2)))
|
||||
.join('');
|
||||
return {innerHTML: (greentext ? `<span class="quote">${text}</span>` : text)};
|
||||
}
|
||||
});
|
||||
comment = { innerHTML: E.cat(comment), [isEscaped]: true };
|
||||
|
||||
this.threadID = +data.thread_num;
|
||||
const o = {
|
||||
ID: this.postID,
|
||||
threadID: this.threadID,
|
||||
boardID: this.boardID,
|
||||
isReply: this.postID !== this.threadID
|
||||
};
|
||||
o.info = {
|
||||
subject: data.title,
|
||||
email: data.email,
|
||||
name: data.name || '',
|
||||
tripcode: data.trip,
|
||||
capcode: (() => { switch (data.capcode) {
|
||||
// https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/assets/themes/foolz/foolfuuka-theme-fuuka/src/Partial/Board.php#L77
|
||||
case 'M': return 'Mod';
|
||||
case 'A': return 'Admin';
|
||||
case 'D': return 'Developer';
|
||||
case 'V': return 'Verified';
|
||||
case 'F': return 'Founder';
|
||||
case 'G': return 'Manager';
|
||||
} })(),
|
||||
uniqueID: data.poster_hash,
|
||||
flagCode: data.poster_country,
|
||||
flagCodeTroll: data.troll_country_code,
|
||||
flag: data.poster_country_name || data.troll_country_name,
|
||||
dateUTC: data.timestamp,
|
||||
dateText: data.fourchan_date,
|
||||
commentHTML: comment
|
||||
};
|
||||
if (o.info.capcode) { delete o.info.uniqueID; }
|
||||
if (data.media && !!+data.media.banned) {
|
||||
o.fileDeleted = true;
|
||||
} else if (data.media?.media_filename) {
|
||||
let {thumb_link} = data.media;
|
||||
// Fix URLs missing origin
|
||||
if (thumb_link?.[0] === '/') { thumb_link = url.split('/', 3).join('/') + thumb_link; }
|
||||
if (!Redirect.securityCheck(thumb_link)) { thumb_link = ''; }
|
||||
let media_link = Redirect.to('file', {boardID: this.boardID, filename: data.media.media_orig});
|
||||
if (!Redirect.securityCheck(media_link)) { media_link = ''; }
|
||||
o.file = {
|
||||
name: data.media.media_filename,
|
||||
url: media_link ||
|
||||
(this.boardID === 'f' ?
|
||||
`${location.protocol}//${ImageHost.flashHost()}/${this.boardID}/${encodeURIComponent(E(data.media.media_filename))}`
|
||||
:
|
||||
`${location.protocol}//${ImageHost.host()}/${this.boardID}/${data.media.media_orig}`),
|
||||
height: data.media.media_h,
|
||||
width: data.media.media_w,
|
||||
MD5: data.media.media_hash,
|
||||
size: $.bytesToString(data.media.media_size),
|
||||
thumbURL: thumb_link || `${location.protocol}//${ImageHost.thumbHost()}/${this.boardID}/${data.media.preview_orig}`,
|
||||
theight: data.media.preview_h,
|
||||
twidth: data.media.preview_w,
|
||||
isSpoiler: data.media.spoiler === '1'
|
||||
};
|
||||
if (!/\.pdf$/.test(o.file.url)) { o.file.dimensions = `${o.file.width}x${o.file.height}`; }
|
||||
if ((this.boardID === 'f') && data.media.exif) { o.file.tag = JSON.parse(data.media.exif).Tag; }
|
||||
}
|
||||
o.extra = dict();
|
||||
|
||||
const board = g.boards[this.boardID] ||
|
||||
new Board(this.boardID);
|
||||
const thread = g.threads.get(`${this.boardID}.${this.threadID}`) ||
|
||||
new Thread(this.threadID, board);
|
||||
post = new Post(g.SITE.Build.post(o), thread, board, {isFetchedQuote: true});
|
||||
post.kill();
|
||||
if (post.file) { post.file.thumbURL = o.file.thumbURL; }
|
||||
Main.callbackNodes('Post', [post]);
|
||||
post = parseArchivePost(data);
|
||||
return this.insert(post);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ export interface File {
|
||||
sizeInBytes: number,
|
||||
isDead: boolean,
|
||||
url: string,
|
||||
thumbURL?: string,
|
||||
name: string,
|
||||
isImage: boolean,
|
||||
isVideo: boolean,
|
||||
@ -24,6 +25,13 @@ export interface File {
|
||||
fullImage?: HTMLImageElement | HTMLVideoElement,
|
||||
audio?: HTMLAudioElement,
|
||||
audioSlider?:HTMLSpanElement,
|
||||
dimensions?: string,
|
||||
height?: string,
|
||||
width?: string,
|
||||
theight: string,
|
||||
twidth: string,
|
||||
MD5?: string,
|
||||
isSpoiler?: boolean,
|
||||
};
|
||||
|
||||
export default class Post {
|
||||
@ -336,7 +344,7 @@ export default class Post {
|
||||
return file as File;
|
||||
}
|
||||
|
||||
kill(file, index=0) {
|
||||
kill(file = false, index = 0) {
|
||||
let strong;
|
||||
if (file) {
|
||||
if (this.isDead || this.files[index].isDead) { return; }
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import SimpleDict from "./SimpleDict";
|
||||
import $ from "../platform/$";
|
||||
import { g } from "../globals/globals";
|
||||
import type Board from "./Board";
|
||||
import type Post from "./Post";
|
||||
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
@ -8,9 +10,30 @@ import { g } from "../globals/globals";
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
||||
*/
|
||||
export default class Thread {
|
||||
declare board: Board;
|
||||
declare ID: number;
|
||||
declare threadID: number;
|
||||
declare boardID: string | number;
|
||||
declare siteID: string;
|
||||
declare fullID: string;
|
||||
declare posts: SimpleDict<Post>;
|
||||
declare isDead: boolean;
|
||||
declare isHidden: boolean;
|
||||
declare isSticky: boolean;
|
||||
declare isClosed: boolean;
|
||||
declare isArchived: boolean;
|
||||
declare postLimit: boolean;
|
||||
declare fileLimit: boolean;
|
||||
declare lastPost: number;
|
||||
declare ipCount: number;
|
||||
declare json: any;
|
||||
declare OP: any;
|
||||
declare catalogView: any
|
||||
declare nodes: any
|
||||
|
||||
toString() { return this.ID; }
|
||||
|
||||
constructor(ID, board) {
|
||||
constructor(ID: string, board: Board) {
|
||||
this.board = board;
|
||||
this.ID = +ID;
|
||||
this.threadID = this.ID;
|
||||
|
||||
@ -44,9 +44,11 @@ export const g: {
|
||||
VERSION: string,
|
||||
NAMESPACE: string,
|
||||
sites: (typeof SWTinyboard)[],
|
||||
boardID?: string,
|
||||
boards: Board[],
|
||||
posts?: SimpleDict<Post>,
|
||||
threads?: SimpleDict<Thread>
|
||||
threads?: SimpleDict<Thread>,
|
||||
threadID?: number,
|
||||
THREADID?: number,
|
||||
SITE?: typeof SWTinyboard,
|
||||
BOARD?: Board,
|
||||
|
||||
@ -88,9 +88,9 @@ import Menu from "../Menu/Menu";
|
||||
import BoardConfig from "../General/BoardConfig";
|
||||
import CaptchaReplace from "../Posting/Captcha.replace";
|
||||
import Get from "../General/Get";
|
||||
import Captcha from "../Posting/Captcha";
|
||||
import { dict, platform } from "../platform/helpers";
|
||||
import Polyfill from "../General/Polyfill";
|
||||
import RestoreDeletedFromArchive from "../Archive/RestoreDeletedFromArchive";
|
||||
// import Test from "../General/Test";
|
||||
|
||||
/*
|
||||
@ -952,7 +952,8 @@ User agent: ${navigator.userAgent}\
|
||||
['Announcements', PSA],
|
||||
['Flash Features', Flash],
|
||||
['Reply Pruning', ReplyPruning],
|
||||
['Mod Contact Links', ModContact]
|
||||
['Mod Contact Links', ModContact],
|
||||
['Restore deleted posts from archive', RestoreDeletedFromArchive],
|
||||
]
|
||||
};
|
||||
export default Main;
|
||||
|
||||
@ -389,14 +389,14 @@ $.before = (root, el) => root.parentNode.insertBefore($.nodes(el), root);
|
||||
|
||||
$.replace = (root, el) => root.parentNode.replaceChild($.nodes(el), root);
|
||||
|
||||
$.el = function(tag, properties, properties2) {
|
||||
$.el = function (tag: string, properties?: Record<string, any>, properties2?: Record<string, any>) {
|
||||
const el = d.createElement(tag);
|
||||
if (properties) { $.extend(el, properties); }
|
||||
if (properties2) { $.extend(el, properties2); }
|
||||
return el;
|
||||
};
|
||||
|
||||
$.on = function(el, events, handler) {
|
||||
$.on = function (el: Element, events: string, handler: (event: Event) => void) {
|
||||
for (var event of events.split(' ')) {
|
||||
el.addEventListener(event, handler, false);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "XT 2.1.4",
|
||||
"date": "2023-09-02T15:03:39.080Z"
|
||||
"version": "XT 2.2.0",
|
||||
"date": "2023-10-27T13:58:44.136Z"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user