User:Bosco/Unsigned helper.js: Difference between revisions

From Test Wiki
Content deleted Content added
mNo edit summary
 
(24 intermediate revisions by 2 users not shown)
Line 3: Line 3:
*/
*/
(function () {
(function () {
const LOG_PREFIX = `[Unsigned Helper]:`;
const DEBUG = false;
const LOG_PREFIX = `[簽名工具]:`;


function error(...toLog) {
function error(...toLog) {
Line 21: Line 22:
}
}


const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];


const CONFIG = {
info('Loading...');
undated: 'Undated', // [[Template:Undated]]
unsignedLoggedIn: '未签名', // [[Template:Unsigned]]
unsignedIp: '未签名', // [[Template:Unsigned]]
};

if (mw.config.get('wgAction') !== 'edit' && mw.config.get('wgAction') !== 'submit' && document.getElementById("editform") == null) {
info('未編輯頁面。暫停中。');
return;
}

info('載入中...');


function formatErrorSpan(errorMessage) {
function formatErrorSpan(errorMessage) {
return `<span style="color:maroon;"><b>Error:</b> ${errorMessage}</span>`;
return `<span style="color:maroon;"><b>錯誤:</b> ${errorMessage}</span>`;
}
}


/**
* Batch size for {@link LazyRevisionIdsLoader}.
*/
const LAZY_REVISION_LOADING_INTERVAL = 50;
const LAZY_REVISION_LOADING_INTERVAL = 50;


/**
/**
* Lazily loads revision IDs for a page.
* Lazily loads revision IDs for a page. The loading is done linearly,
* in batches of the size of {@link LAZY_REVISION_LOADING_INTERVAL}.
* This is relatively fast because we are not loading the heavy contents
* of the page, only the metadata.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
*/
*/
Line 50: Line 68:


#getLastLoadedInterval(upToIndex) {
#getLastLoadedInterval(upToIndex) {
debug(`#getLastLoadedInterval(${upToIndex}): `, this.#historyIntervalPromises.length);
let i = 0;
let i = 0;
while (this.#historyIntervalPromises[i] != undefined && i <= upToIndex) {
while (this.#historyIntervalPromises[i] != undefined && i <= upToIndex) {
i++;
i++;
}
}
debug(`#getLastLoadedInterval(${upToIndex}) = ${i}`);
return [i, this.#historyIntervalPromises[i - 1]];
return [i, this.#historyIntervalPromises[i - 1]];
}
}
Line 61: Line 81:
return undefined;
return undefined;
}
}
return {
const interval = {
rvcontinue: response.continue?.rvcontinue,
rvcontinue: response.continue?.rvcontinue,
revisions: response.query.pages[0].revisions,
revisions: response.query.pages[0].revisions,
};
};
if (response.batchcomplete) {
// remember that MediaWiki has no more revisions to return
interval.batchcomplete = true;
} else {
interval.batchcomplete = false;
}
return interval;
}
}


async #loadIntervalsRecursive(index, upToIndex, rvcontinue) {
async #loadIntervalsRecursive(startIndex, targetIndex, rvcontinue) {
const logMsgPrefix = `#loadIntervalsRecursive(${startIndex}, ${targetIndex}, '${rvcontinue}')`;
return new Promise(async (resolve, reject) => {
return new Promise(async (resolve, reject) => {
// reference documentation: https://en.wikipedia.org/w/api.php?action=help&modules=query%2Brevisions
// reference documentation: https://en.wikipedia.org/w/api.php?action=help&modules=query%2Brevisions
Line 83: Line 111:
intervalQuery.rvcontinue = rvcontinue;
intervalQuery.rvcontinue = rvcontinue;
}
}
debug('loadIntervalsRecursive Q: index =', index, 'upToIndex =', upToIndex, 'intervalQuery =', intervalQuery);
debug(`${logMsgPrefix} Q =`, intervalQuery);
this.#api.get(intervalQuery).then(async (response) => {
this.#api.get(intervalQuery).then(async (response) => {
try {
try {
if (DEBUG) {
// debug('loadIntervalsRecursive R:', response);
debug(`${logMsgPrefix} R =`, response);
}
const interval = this.#createIntervalFromResponse(response);
const interval = this.#createIntervalFromResponse(response);
this.#historyIntervalPromises[index] = Promise.resolve(interval);
this.#historyIntervalPromises[startIndex] = Promise.resolve(interval);
if (index == upToIndex) {
if (startIndex == targetIndex) {
// we've hit the limit of what we want to load so far
// we've hit the limit of what we want to load so far
resolve(interval);
resolve(interval);
return;
return;
}
}
if (response.batchcomplete) {
if (interval.batchcomplete) {
for (let i = index; i <= upToIndex; i++) {
// reached the end of batch loading => cannot ask for one more
// for convenience, fill the rest of the array with undefined
for (let i = startIndex + 1; i <= targetIndex; i++) {
this.#historyIntervalPromises[i] = Promise.resolve(undefined);
this.#historyIntervalPromises[i] = Promise.resolve(undefined);
}
}
info(`${logMsgPrefix}: This is the last batch returned by MediaWiki`);
// we've asked for an interval of history which doesn't exist
if (targetIndex <= startIndex) {
error(`${logMsgPrefix}: something went very wrong`);
}
resolve(undefined);
resolve(undefined);
return;
return;
}
}
// recursive call for one more interval
// .batchcomplete has not been reached, call for one more interval (recursive)
const ignored = await this.#loadIntervalsRecursive(index + 1, upToIndex, interval.rvcontinue);
const ignored = await this.#loadIntervalsRecursive(startIndex + 1, targetIndex, interval.rvcontinue);
if (this.#historyIntervalPromises[upToIndex] == undefined) {
if (this.#historyIntervalPromises[targetIndex] == undefined) {
resolve(undefined);
resolve(undefined);
return;
return;
}
}
this.#historyIntervalPromises[upToIndex].then(
this.#historyIntervalPromises[targetIndex].then(
result => resolve(result),
result => resolve(result),
rejection => reject(rejection)
rejection => reject(rejection)
Line 122: Line 157:


async #loadInterval(intervalIndex) {
async #loadInterval(intervalIndex) {
const [firstNotLoadedIntervalIndex, latestLoadedInterval] = this.#getLastLoadedInterval(intervalIndex);
const [firstNotLoadedIntervalIndex, latestLoadedIntervalPromise] = this.#getLastLoadedInterval(intervalIndex);
if (firstNotLoadedIntervalIndex > intervalIndex) {
if (firstNotLoadedIntervalIndex > intervalIndex) {
return this.#historyIntervalPromises[intervalIndex];
return this.#historyIntervalPromises[intervalIndex];
}
}
if (await latestLoadedIntervalPromise?.then(interval => interval.batchcomplete)) {
const rvcontinue = latestLoadedInterval?.rvcontinue;
// latest request returned the last batch in the batch loading of revisions
return Promise.resolve(undefined);
}
const rvcontinue = await latestLoadedIntervalPromise?.then(interval => interval.rvcontinue);
debug(`#loadInterval(${intervalIndex}): ${firstNotLoadedIntervalIndex}, ${rvcontinue}`);
return this.#loadIntervalsRecursive(firstNotLoadedIntervalIndex, intervalIndex, rvcontinue);
return this.#loadIntervalsRecursive(firstNotLoadedIntervalIndex, intervalIndex, rvcontinue);
}
}
Line 136: Line 176:
#indexToIndexInInterval(index) {
#indexToIndexInInterval(index) {
return index % LAZY_REVISION_LOADING_INTERVAL;
return index % LAZY_REVISION_LOADING_INTERVAL;
}

#revisionsToString(revisions) {
if (!revisions) {
return "<undefined revisions>";
}
return Array.from(revisions).map((revision, index) => {
return `[${index}]={revid=${revision.revid} by User:${revision.user}}`
}).join(", ");
}
}


Line 147: Line 196:
const promise = new Promise(async (resolve, reject) => {
const promise = new Promise(async (resolve, reject) => {
const intervalIndex = this.#indexToIntervalIndex(index);
const intervalIndex = this.#indexToIntervalIndex(index);
debug(`loadRevision: loading from interval #${intervalIndex}...`);
try {
try {
const interval = await this.#loadInterval(intervalIndex);
const interval = await this.#loadInterval(intervalIndex);
if (DEBUG) {
debug(`loadRevision: loaded the interval#${intervalIndex} with revisions: (length=${interval?.revisions?.length}) ${this.#revisionsToString(interval?.revisions)}`);
}
if (interval == undefined) {
if (interval == undefined) {
resolve(undefined);
resolve(undefined);
return;
return;
}
}
const theRevision = interval.revisions[this.#indexToIndexInInterval(index)];
const indexInInterval = this.#indexToIndexInInterval(index);
if (DEBUG) {
debug(`loadRevision: from the above interval, looking at [${indexInInterval}]`);
}
const theRevision = interval.revisions[indexInInterval];
debug('loadRevision: loaded revision', index, theRevision);
debug('loadRevision: loaded revision', index, theRevision);
resolve(theRevision);
resolve(theRevision);
Line 166: Line 223:


/**
/**
* Lazily loads full revisions (wikitext, user, revid, tags, edit summary, etc) for a page.
* Lazily loads full revisions (full wikitext and metadata) for a page.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
* Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
* Loaded revisions are cached to speed up consecutive requests about the
* same page.
*/
*/
class LazyFullRevisionsLoader {
class LazyFullRevisionsLoader {
Line 243: Line 302:
*/
*/
async function exponentialSearch(lower, upper, candidateIndex, testFunc) {
async function exponentialSearch(lower, upper, candidateIndex, testFunc) {
if (upper === null && lower === candidateIndex) {
throw new Error(`exponentialSearch 的錯誤參數(${lower}, ${upper}, ${candidateIndex}).`);
}
if (lower === upper && lower === candidateIndex) {
throw new Error("不能找到");
}
const progressMessage = `Examining [${lower}, ${upper ? upper : '...'}]. Current candidate: ${candidateIndex}`;
const progressMessage = `Examining [${lower}, ${upper ? upper : '...'}]. Current candidate: ${candidateIndex}`;
if (await testFunc(candidateIndex, progressMessage)) {
if (await testFunc(candidateIndex, progressMessage)) {
Line 277: Line 342:
}
}


getContentLoader() {
return this.#contentLoader;
}

/**
* Uses an exponential initial search followed by a binary search to find
* a snippet of text, which was added to the page.
*/
async findRevisionWhenTextAdded(text, startIndex) {
async findRevisionWhenTextAdded(text, startIndex) {
info('findRevisionWhenTextAdded: searching for', text);
info(`findRevisionWhenTextAdded(startIndex=${startIndex}): searching for '${text}'`);
return new Promise(async (resolve, reject) => {
return new Promise(async (resolve, reject) => {
try {
try {
const startRevision = await this.#contentLoader.loadRevisionId(startIndex);
if (startRevision == undefined) {
if (startIndex === 0) {
reject("不能找到最新版本。此頁面存在嗎?");
} else {
reject(`不能尋找開始版本 (版本=${startIndex}).`);
}
return;
}
if (startIndex === 0) {
if (startIndex === 0) {
const latestFullRevision = await this.#contentLoader.loadContent(startIndex);
const latestFullRevision = await this.#contentLoader.loadContent(startIndex);
if (latestFullRevision == undefined) {
reject("Cannot find the latest revision. Does this page exist?");
return;
}
if (!latestFullRevision.slots.main.content.includes(text)) {
if (!latestFullRevision.slots.main.content.includes(text)) {
reject("Cannot find text in the latest revision. Did you edit it?");
reject("不能尋找最新版本的內容。你編輯了嗎?");
return;
return;
}
}
}
}

const foundIndex = await exponentialSearch(startIndex, null, startIndex + 10, async (candidateIndex, progressInfo) => {
const foundIndex = await exponentialSearch(startIndex, null, startIndex + 10, async (candidateIndex, progressInfo) => {
try {
try {
this.#progressCallback(progressInfo);
this.#progressCallback(progressInfo);
const candidateFullRevision = await this.#contentLoader.loadContent(candidateIndex);
const candidateFullRevision = await this.#contentLoader.loadContent(candidateIndex);
if (candidateFullRevision == undefined) {
if (candidateFullRevision?.slots?.main?.content == undefined) {
return false;
/*
* TODO can we distinguish between
* - `candidateIndex` is out of bounds of the history
* vs
* - `candidateIndex` is obscured from current user ([[WP:REVDEL]] or [[WP:SUPPRESS]])
* ?
*/
warn('測試功能:不能載入candidateIndex的內容 = ' + candidateIndex);
return undefined;
}
}
// debug('testFunc: checking text of revision:', candidateFullRevision, candidateFullRevision?.slots, candidateFullRevision?.slots?.main);
// debug('testFunc: checking text of revision:', candidateFullRevision, candidateFullRevision?.slots, candidateFullRevision?.slots?.main);
return candidateFullRevision.slots.main.content.includes(text);
return candidateFullRevision.slots.main.content.includes(text);
} catch (e) {
} catch (e) {
reject('testFunc: ' + e);
reject('測試內容: ' + e);
}
}
});
});
if (foundIndex === undefined) {
reject("不能尋找這個內容");
return;
}
const foundFullRevision = await this.#contentLoader.loadContent(foundIndex);
const foundFullRevision = await this.#contentLoader.loadContent(foundIndex);
resolve({
resolve({
Line 333: Line 424:
}
}


function chooseTemplateFromRevision(fullRevision) {
function chooseUnsignedTemplateFromRevision(fullRevision) {
if (typeof (fullRevision.anon) !== 'undefined') {
if (typeof (fullRevision.anon) !== 'undefined') {
return 'Unsigned IP';
return CONFIG.unsignedIp;
} else if (typeof (fullRevision.temp) !== 'undefined') {
} else if (typeof (fullRevision.temp) !== 'undefined') {
// Seems unlikely "temporary" users will have a user page, so this seems the better template for them for now.
// Seems unlikely "temporary" users will have a user page, so this seems the better template for them for now.
return 'Unsigned IP';
return CONFIG.unsignedIp;
} else {
} else {
return 'Unsigned';
return CONFIG.unsignedLoggedIn;
}
}
}
}


function makeUnsignedTemplate(user, timestamp, template) {
function chooseTemplate(selectedText, fullRevision) {
const ts = new Date(timestamp);
const user = fullRevision.user;
if (selectedText.includes(`[[User talk:${user}|`)) {
let h = ts.getUTCHours();
/*
if (h < 10) {
* assume that presense of something that looks like a wikilink to the user's talk page
h = '0' + h;
* means that the message is just undated, not unsigned
* NB: IP editors have `Special:Contributions` and `User talk` in their signature.
*/
return CONFIG.undated;
}
}
if (selectedText.includes(`[[User:${user}|`)) {
let m = ts.getUTCMinutes();
// some ancient undated signatures have only `[[User:` links
if (m < 10) {
return CONFIG.undated;
m = '0' + m;
}
return chooseUnsignedTemplateFromRevision(fullRevision);
}

function createTimestampWikitext(timestamp) {
/*
* Format is from https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time_format_like_in_signatures
*
* The unicode escapes are needed to avoid actual substitution, see
* https://en.wikipedia.org/w/index.php?title=User:Andrybak/Scripts/Unsigned_generator.js&diff=prev&oldid=1229098580
*/
return `\u007B\u007Bsubst:#time:H:i, j xg Y "(UTC)"|${timestamp}}}`;
}

function makeTemplate(user, timestamp, template) {
// <nowiki>
const formattedTimestamp = createTimestampWikitext(timestamp);
if (template == CONFIG.undated) {
return '{{subst:' + template + '|' + formattedTimestamp + '}}';
}
}
const formattedTimestamp = `${h}:${m}, ${ts.getUTCDate()} ${months[ts.getUTCMonth()]} ${ts.getUTCFullYear()} (UTC)`;
return '{{subst:' + template + '|' + user + '|' + formattedTimestamp + '}}';
return '{{subst:' + template + '|' + user + '|' + formattedTimestamp + '}}';
// </nowiki>
}

function constructAd() {
return " (using [[w:User:Andrybak/Scripts/Unsigned helper|Unsigned helper]])";
}
}


Line 364: Line 482:
return;
return;
}
}
const oldText = editSummaryField.val().trimEnd();
// get text without trailing whitespace
// get text without trailing whitespace
let oldText = editSummaryField.val().trimEnd();
const ad = constructAd();
if (oldText.includes(ad)) {
oldText = oldText.replace(ad, '');
}
let newText = "";
let newText = "";
if (oldText.match(/[*]\/$/)) {
if (oldText.match(/[*]\/$/)) {
Line 375: Line 497:
newText = newSummary;
newText = newSummary;
}
}
editSummaryField.val(newText);
editSummaryField.val(newText + ad);
}
}


Line 394: Line 516:
const form = document.getElementById('editform');
const form = document.getElementById('editform');
const wikitextEditor = form.elements.wpTextbox1;
const wikitextEditor = form.elements.wpTextbox1;
/*
let pos = $(wikitextEditor).textSelection('getCaretPosition', { startAndEnd: true });
* https://doc.wikimedia.org/mediawiki-core/master/js/module-jquery.textSelection.html
let txt;
* We cannot use wikitextEditor.value here, because this textarea is hidden and
if (pos[0] != pos[1]) {
* is not updated with CodeMirror. Therefore, the selection in CodeMirror becomes
txt = wikitextEditor.value.substring(pos[0], pos[1]);
* desynced from the text in wikitextEditor.
pos = pos[1];
* However, CodeMirror does respond to textSelection "commands" sent to wikitextEditor.
} else {
* The responses correspond with up-to-date wikitext in CodeMirror.
pos = pos[1];
* For reference, see https://en.wikipedia.org/wiki/MediaWiki:Gadget-charinsert-core.js#L-251--L-258
if (pos <= 0) {
*/
pos = wikitextEditor.value.length;
const $editor = $(wikitextEditor);
}
while ($editor.textSelection('getSelection').endsWith('\n')) {
txt = wikitextEditor.value.substr(0, pos);
const [selectionStart, selectionEnd] = $editor.textSelection('getCaretPosition', {startAndEnd:true});
txt = txt.replace(new RegExp('[\\s\\S]*\\d\\d:\\d\\d, \\d+ (' + months.join('|') + ') \\d\\d\\d\\d \\(UTC\\)'), '');
$editor.textSelection('setSelection', {start: selectionStart, end:(selectionEnd - 1)});
txt = txt.replace(/[\s\S]*\n=+.*=+\s*\n/, '');
}
}
const originalSelection = $(wikitextEditor).textSelection('getSelection');
txt = txt.replace(/^\s+|\s+$/g, '');
let selection = originalSelection;
debug(`doAddUnsignedTemplate: getSelection: '${selection}'`);
selection = selection.replace(new RegExp('[\\s\\S]*\\d\\d:\\d\\d, \\d+ (' + months.join('|') + ') \\d\\d\\d\\d \\(UTC\\)([<]/small[>])?'), '');
selection = selection.replace(/[\s\S]*\n=+.*=+\s*\n/, '');
selection = selection.replace(/^\s+|\s+$/g, '');
debug(`doAddUnsignedTemplate: getSelection filtered: '${selection}'`);


// TODO maybe migrate to https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
// TODO maybe migrate to https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
const mainDialog = $('<div>Examining...</div>').dialog({
const mainDialog = $('<div>檢查中...</div>').dialog({
buttons: {
buttons: {
Cancel: function () {
Cancel: function () {
Line 429: Line 557:
function applySearcherResult(searcherResult) {
function applySearcherResult(searcherResult) {
const fullRevision = searcherResult.fullRevision;
const fullRevision = searcherResult.fullRevision;
const template = chooseTemplateFromRevision(fullRevision);
const template = chooseTemplate(selection, fullRevision);
const templateWikitext = makeUnsignedTemplate(
const templateWikitext = makeTemplate(
fullRevision.user,
fullRevision.user,
fullRevision.timestamp,
fullRevision.timestamp,
template
template
);
);
// https://doc.wikimedia.org/mediawiki-core/master/js/module-jquery.textSelection.html
const newWikitextTillSelection = wikitextEditor.value.substr(0, pos).replace(/\s*$/, ' ') + templateWikitext;
wikitextEditor.value = newWikitextTillSelection + wikitextEditor.value.substr(pos);
$(wikitextEditor).textSelection(
'encapsulateSelection', {
$(wikitextEditor).textSelection('setSelection', { start: newWikitextTillSelection.length });
post: " " + templateWikitext
appendToEditSummary(`mark unsigned [[Special:Diff/${fullRevision.revid}]]`);
}
);
appendToEditSummary(`mark [[Template:${template}|{{${template}}}]] [[Special:Diff/${fullRevision.revid}]]`);
mainDialog.dialog('close');
mainDialog.dialog('close');
}
}


function reportPossibleRevertToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
function reportSearcherResultToUser(searcherResult, dialogTitle, useCb, keepLookingCb, cancelCb, createMainMessageDivFn) {
const fullRevision = searcherResult.fullRevision;
const fullRevision = searcherResult.fullRevision;
const revid = fullRevision.revid;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
const comment = fullRevision.parsedcomment;
const suspicionDialog = $('<div>')
const questionDialog = createMainMessageDivFn()
.append(
"The ",
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`found revision (index=${searcherResult.index})`),
" may be a revert: ",
comment
)
.dialog({
.dialog({
title: "Possible revert!",
title: dialogTitle,
minWidth: document.body.clientWidth / 5,
modal: true,
modal: true,
buttons: {
buttons: {
"Use that revision": function () {
"Use that revision": function () {
suspicionDialog.dialog('close');
questionDialog.dialog('close');
useCb();
useCb();
},
},
"Keep looking": function () {
"Keep looking": function () {
suspicionDialog.dialog('close');
questionDialog.dialog('close');
keepLookingCb();
keepLookingCb();
},
},
"Cancel": function () {
"Cancel": function () {
suspicionDialog.dialog('close');
questionDialog.dialog('close');
cancelCb();
cancelCb();
},
},
}
}
});
}

function reportPossibleRevertToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
const fullRevision = searcherResult.fullRevision;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
reportSearcherResultToUser(searcherResult, "Possible revert!", useCb, keepLookingCb, cancelCb, () => {
return $('<div>').append(
"The ",
$('<a>').prop({
href: '/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`已尋找版本 (版本=${searcherResult.index})`),
" may be a revert: ",
comment
);
});
}

function formatTimestamp(timestamp) {
// return new Date(timestamp).toLocaleString();
return timestamp;
}

function revisionByteSize(fullRevision) {
if (fullRevision == null) {
return null;
}
return new Blob([fullRevision.slots.main.content]).size;
}

function formatSizeChange(afterSize, beforeSize) {
const change = afterSize - beforeSize;
const title = `編輯後共${afterSize}位元組`;
const titleAttribute = `title="${title}"`;
let style = '';
let changeText = "" + change;
if (change > 0) {
changeText = "+" + change;
style = 'color: var(--color-content-added,#006400);';
}
if (change < 0) {
// use proper minus sign ([[Plus and minus signs#Minus sign]])
changeText = "−" + Math.abs(change);
style = 'color: var(--color-content-removed,#8b0000);';
}
if (Math.abs(change) > 500) {
// [[Help:Watchlist#How to read a watchlist (or recent changes)]]
style = style + "font-weight:bold;";
}
changeText = `(${changeText})`;
return $('<span>').text(changeText).attr('style', style).attr('title', title);
}

async function reportNormalSearcherResultToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
const fullRevision = searcherResult.fullRevision;
const user = fullRevision.user;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
const afterSize = revisionByteSize(fullRevision);
const beforeSize = revisionByteSize(await searcher.getContentLoader().loadContent(searcherResult.index + 1));
reportSearcherResultToUser(searcherResult, "Do you want to use this?", useCb, keepLookingCb, cancelCb, () => {
return $('<div>').append(
"Found a revision: ",
$('<a>').prop({
href: '/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`[[Special:Diff/${revid}]] (index=${searcherResult.index})`),
$('<br/>'), '• ', formatTimestamp(fullRevision.timestamp),
$('<br/>'), "• by ", $('<a>').prop({
href: '/wiki/Special:Contributions/' + user.replaceAll(' ', '_'),
target: '_blank'
}).text(`User:${user}`),
$('<br/>'), "• ", formatSizeChange(afterSize, beforeSize),
$('<br/>'), "• edit summary: ", $('<i>').html(comment)
);
});
});
}
}


function searchFromIndex(index) {
function searchFromIndex(index) {
if (selection == undefined || selection == '') {
searcher.findRevisionWhenTextAdded(txt, index).then(searcherResult => {
mainDialog.html(formatErrorSpan("請選擇一個未簽名的訊息") +
" Selected: <code>" + originalSelection + "</code>");
return;
}
searcher.findRevisionWhenTextAdded(selection, index).then(searcherResult => {
if (!mainDialog.dialog('isOpen')) {
if (!mainDialog.dialog('isOpen')) {
// user clicked [cancel]
// user clicked [cancel]
Line 483: Line 688:
}
}
info('Searcher found:', searcherResult);
info('Searcher found:', searcherResult);
const useCallback = () => { /* use */
applySearcherResult(searcherResult);
};
const keepLookingCallback = () => { /* keep looking */
// recursive call from a differfent index: `+1` is very important here
searchFromIndex(searcherResult.index + 1);
};
const cancelCallback = () => { /* cancel */
mainDialog.dialog('close');
};
if (isRevisionARevert(searcherResult.fullRevision)) {
if (isRevisionARevert(searcherResult.fullRevision)) {
reportPossibleRevertToUser(
reportPossibleRevertToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
searcherResult,
() => { /* use */
applySearcherResult(searcherResult);
},
() => { /* keep looking */
// recursive call from a differfent index: `+1` is very important here
searchFromIndex(searcherResult.index + 1);
},
() => { /* cancel */
mainDialog.dialog('close');
}
);
return;
return;
}
}
applySearcherResult(searcherResult);
reportNormalSearcherResultToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
}, rejection => {
}, rejection => {
error(`Searcher cannot find requested index=${index}. Got error:`, rejection);
error(`搜尋器不能尋找已要求的索引=${index}. Got error:`, rejection);
if (!mainDialog.dialog('isOpen')) {
if (!mainDialog.dialog('isOpen')) {
// user clicked [cancel]
// user clicked [cancel]
Line 514: Line 717:


window.unsignedHelperAddUnsignedTemplate = function(event) {
window.unsignedHelperAddUnsignedTemplate = function(event) {
mw.loader.using(['mediawiki.util', 'jquery.ui'], doAddUnsignedTemplate);
event.preventDefault();
event.preventDefault();
event.stopPropagation();
event.stopPropagation();
mw.loader.using(['mediawiki.util', 'jquery.ui'], doAddUnsignedTemplate);
return false;
return false;
}
}