Merge branch 'master' into v3
This commit is contained in:
commit
20ece62290
134
4chan_x.user.js
134
4chan_x.user.js
@ -43,7 +43,7 @@
|
||||
*/
|
||||
|
||||
(function() {
|
||||
var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Notification, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g,
|
||||
var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g,
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
|
||||
@ -55,6 +55,7 @@
|
||||
'404 Redirect': [true, 'Redirect dead threads and images.'],
|
||||
'Keybinds': [true, 'Bind actions to keyboard shortcuts.'],
|
||||
'Time Formatting': [true, 'Localize and format timestamps arbitrarily.'],
|
||||
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.'],
|
||||
'File Info Formatting': [true, 'Reformat the file information.'],
|
||||
'Comment Expansion': [true, 'Can expand too long comments.'],
|
||||
'Thread Expansion': [true, 'Can expand threads to view all replies.'],
|
||||
@ -834,8 +835,19 @@
|
||||
open: function(url) {
|
||||
return (GM_openInTab || window.open)(url, '_blank');
|
||||
},
|
||||
hidden: function() {
|
||||
return d.hidden || d.oHidden || d.mozHidden || d.webkitHidden;
|
||||
debounce: function(wait, fn) {
|
||||
var timeout;
|
||||
timeout = null;
|
||||
return function() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
} else {
|
||||
fn.apply(this, arguments);
|
||||
}
|
||||
return timeout = setTimeout((function() {
|
||||
return timeout = null;
|
||||
}), wait);
|
||||
};
|
||||
},
|
||||
queueTask: (function() {
|
||||
var execTask, taskChannel, taskQueue;
|
||||
@ -940,6 +952,34 @@
|
||||
}
|
||||
});
|
||||
|
||||
Polyfill = {
|
||||
init: function() {
|
||||
return Polyfill.visibility();
|
||||
},
|
||||
visibility: function() {
|
||||
var event, prefix, property;
|
||||
if ('visibilityState' in document) {
|
||||
return;
|
||||
}
|
||||
if ('webkitVisibilityState' in document) {
|
||||
prefix = 'webkit';
|
||||
} else if ('mozVisibilityState' in document) {
|
||||
prefix = 'moz';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
property = prefix + 'VisibilityState';
|
||||
event = prefix + 'visibilitychange';
|
||||
d.visibilityState = d[property];
|
||||
d.hidden = d.visibilityState === 'hidden';
|
||||
return $.on(d, event, function() {
|
||||
d.visibilityState = d[property];
|
||||
d.hidden = d.visibilityState === 'hidden';
|
||||
return $.event('visibilitychange');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Header = {
|
||||
init: function() {
|
||||
var boardList, boardListButton, boardTitle, catalogToggler, headerBar, menuButton, toggleBar;
|
||||
@ -2220,7 +2260,7 @@
|
||||
case 'u':
|
||||
return "//nsfw.foolz.us/" + board + "/full_image/" + filename;
|
||||
case 'po':
|
||||
return "http://archive.thedarkcave.org/" + board + "/full_image/" + filename;
|
||||
return "//archive.thedarkcave.org/" + board + "/full_image/" + filename;
|
||||
case 'ck':
|
||||
case 'lit':
|
||||
return "//fuuka.warosu.org/" + board + "/full_image/" + filename;
|
||||
@ -2263,8 +2303,10 @@
|
||||
case 'u':
|
||||
case 'kuku':
|
||||
return "//nsfw.foolz.us/_/api/chan/post/?board=" + board + "&num=" + postID;
|
||||
case 'c':
|
||||
case 'int':
|
||||
case 'po':
|
||||
return "http://archive.thedarkcave.org/_/api/chan/post/?board=" + board + "&num=" + postID;
|
||||
return "//archive.thedarkcave.org/_/api/chan/post/?board=" + board + "&num=" + postID;
|
||||
}
|
||||
},
|
||||
to: function(data) {
|
||||
@ -2290,8 +2332,9 @@
|
||||
case 'kuku':
|
||||
url = Redirect.path('//nsfw.foolz.us', 'foolfuuka', data);
|
||||
break;
|
||||
case 'int':
|
||||
case 'po':
|
||||
url = Redirect.path('http://archive.thedarkcave.org', 'foolfuuka', data);
|
||||
url = Redirect.path('//archive.thedarkcave.org', 'foolfuuka', data);
|
||||
break;
|
||||
case 'ck':
|
||||
case 'lit':
|
||||
@ -3284,6 +3327,73 @@
|
||||
}
|
||||
};
|
||||
|
||||
RelativeDates = {
|
||||
INTERVAL: $.MINUTE,
|
||||
init: function() {
|
||||
if (g.VIEW === 'catalog' || !Conf['Relative Post Dates']) {
|
||||
return;
|
||||
}
|
||||
$.on(d, 'visibilitychange ThreadUpdate', this.flush);
|
||||
return Post.prototype.callbacks.push({
|
||||
name: 'Relative Post Dates',
|
||||
cb: this.node
|
||||
});
|
||||
},
|
||||
node: function() {
|
||||
var dateEl, diff;
|
||||
dateEl = this.nodes.date;
|
||||
dateEl.title = dateEl.textContent;
|
||||
diff = Date.now() - this.info.date;
|
||||
dateEl.textContent = RelativeDates.relative(diff);
|
||||
return RelativeDates.setUpdate(this, diff);
|
||||
},
|
||||
relative: function(diff) {
|
||||
var number, rounded, unit;
|
||||
unit = (number = diff / $.DAY) > 1 ? 'day' : (number = diff / $.HOUR) > 1 ? 'hour' : (number = diff / $.MINUTE) > 1 ? 'minute' : (number = diff / $.SECOND, 'second');
|
||||
rounded = Math.round(number);
|
||||
if (rounded !== 1) {
|
||||
unit += 's';
|
||||
}
|
||||
return "" + rounded + " " + unit + " ago";
|
||||
},
|
||||
stale: [],
|
||||
flush: $.debounce($.SECOND, function() {
|
||||
var now, update, _i, _len, _ref;
|
||||
if (d.hidden) {
|
||||
return;
|
||||
}
|
||||
now = Date.now();
|
||||
_ref = RelativeDates.stale;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
update = _ref[_i];
|
||||
update(now);
|
||||
}
|
||||
RelativeDates.stale = [];
|
||||
clearTimeout(RelativeDates.timeout);
|
||||
return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL);
|
||||
}),
|
||||
setUpdate: function(post, diff) {
|
||||
var dateEl, markStale, setOwnTimeout, update;
|
||||
setOwnTimeout = function(diff) {
|
||||
var delay;
|
||||
delay = diff > $.HOUR ? diff % $.HOUR : diff > $.MINUTE ? diff % $.MINUTE : diff % $.SECOND;
|
||||
return setTimeout(markStale, delay);
|
||||
};
|
||||
dateEl = post.nodes.date;
|
||||
update = function(now) {
|
||||
if (d.contains(dateEl)) {
|
||||
diff = now - post.info.date;
|
||||
dateEl.textContent = RelativeDates.relative(diff);
|
||||
return setOwnTimeout(diff);
|
||||
}
|
||||
};
|
||||
markStale = function() {
|
||||
return RelativeDates.stale.push(update);
|
||||
};
|
||||
return setOwnTimeout(diff);
|
||||
}
|
||||
};
|
||||
|
||||
FileInfo = {
|
||||
init: function() {
|
||||
if (g.VIEW === 'catalog' || !Conf['File Info Formatting']) {
|
||||
@ -3839,7 +3949,7 @@
|
||||
}
|
||||
$.on(window, 'online offline', ThreadUpdater.cb.online);
|
||||
$.on(d, 'QRPostSuccessful', ThreadUpdater.cb.post);
|
||||
$.on(d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', ThreadUpdater.cb.visibility);
|
||||
$.on(d, 'visibilitychange', ThreadUpdater.cb.visibility);
|
||||
ThreadUpdater.cb.online();
|
||||
return $.add(d.body, ThreadUpdater.dialog);
|
||||
},
|
||||
@ -3874,7 +3984,7 @@
|
||||
}
|
||||
},
|
||||
visibility: function() {
|
||||
if ($.hidden()) {
|
||||
if (d.hidden) {
|
||||
return;
|
||||
}
|
||||
ThreadUpdater.outdateCount = 0;
|
||||
@ -3886,7 +3996,7 @@
|
||||
return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? function() {
|
||||
return true;
|
||||
} : function() {
|
||||
return !$.hidden();
|
||||
return !d.hidden;
|
||||
};
|
||||
},
|
||||
autoUpdate: function() {
|
||||
@ -3942,7 +4052,7 @@
|
||||
var i, j;
|
||||
i = ThreadUpdater.interval;
|
||||
j = Math.min(ThreadUpdater.outdateCount, 10);
|
||||
if (!$.hidden()) {
|
||||
if (!d.hidden) {
|
||||
j = Math.min(j, 7);
|
||||
}
|
||||
return ThreadUpdater.seconds = Math.max(i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]);
|
||||
@ -4037,7 +4147,7 @@
|
||||
} else {
|
||||
ThreadUpdater.set('status', "+" + count, 'new');
|
||||
ThreadUpdater.outdateCount = 0;
|
||||
if (Conf['Beep'] && $.hidden()) {
|
||||
if (Conf['Beep'] && d.hidden) {
|
||||
if (!ThreadUpdater.audio) {
|
||||
ThreadUpdater.audio = $.el('audio', {
|
||||
src: ThreadUpdater.beep
|
||||
@ -5381,6 +5491,7 @@
|
||||
return console.timeEnd("" + name + " initialization");
|
||||
};
|
||||
console.time('All initializations');
|
||||
initFeature('Polyfill', Polyfill);
|
||||
initFeature('Header', Header);
|
||||
initFeature('Settings', Settings);
|
||||
initFeature('Resurrect Quotes', Quotify);
|
||||
@ -5404,6 +5515,7 @@
|
||||
initFeature('Mark Cross-thread Quotes', QuoteCT);
|
||||
initFeature('Anonymize', Anonymize);
|
||||
initFeature('Time Formatting', Time);
|
||||
initFeature('Relative Post Dates', RelativeDates);
|
||||
initFeature('File Info Formatting', FileInfo);
|
||||
initFeature('Sauce', Sauce);
|
||||
initFeature('Image Expansion', ImageExpand);
|
||||
|
||||
23
changelog
23
changelog
@ -26,8 +26,31 @@ alpha
|
||||
Fix unreadable inlined posts with the Tomorrow theme.
|
||||
|
||||
master
|
||||
|
||||
2.38.1
|
||||
- Mayhem
|
||||
Fix a little regression introduced in 2.38.0 for webkit browsers.
|
||||
|
||||
2.38.0
|
||||
- Queue
|
||||
Add Relative Post Dates ("35 seconds ago"), disabled by default.
|
||||
- Mayhem
|
||||
Add /int/ archive redirection for threads, and post resurrection.
|
||||
|
||||
2.37.6
|
||||
- Mayhem
|
||||
Fix image expanding.
|
||||
|
||||
2.37.5
|
||||
- Mayhem
|
||||
Fix quoting inside inlined backlinks.
|
||||
|
||||
2.37.4
|
||||
- James Campos
|
||||
Don't expand pdfs
|
||||
- Mayhem
|
||||
Add /po/ archive redirection for threads, images and post resurrection.
|
||||
Fix quoting.
|
||||
|
||||
2.37.3
|
||||
- Mayhem
|
||||
|
||||
1
grunt.js
1
grunt.js
@ -19,6 +19,7 @@ module.exports = function(grunt) {
|
||||
'<file_template:src/globals.coffee>',
|
||||
'<file_template:lib/ui.coffee>',
|
||||
'<file_template:lib/$.coffee>',
|
||||
'<file_template:lib/polyfill.coffee>',
|
||||
'<file_template:src/features.coffee>',
|
||||
'<file_template:src/qr.coffee>',
|
||||
'<file_template:src/main.coffee>'
|
||||
|
||||
@ -1 +1 @@
|
||||
postMessage({version:'2.37.3'},'*')
|
||||
postMessage({version:'2.38.1'},'*')
|
||||
13
lib/$.coffee
13
lib/$.coffee
@ -140,8 +140,17 @@ $.extend $,
|
||||
root.dispatchEvent new CustomEvent event, {bubbles: true, detail}
|
||||
open: (url) ->
|
||||
(GM_openInTab or window.open) url, '_blank'
|
||||
hidden: ->
|
||||
d.hidden or d.oHidden or d.mozHidden or d.webkitHidden
|
||||
debounce: (wait, fn) ->
|
||||
timeout = null
|
||||
return ->
|
||||
if timeout
|
||||
# stop current reset
|
||||
clearTimeout timeout
|
||||
else
|
||||
fn.apply this, arguments
|
||||
|
||||
# after wait, let next invocation execute immediately
|
||||
timeout = setTimeout (-> timeout = null), wait
|
||||
queueTask: (->
|
||||
# inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007
|
||||
taskQueue = []
|
||||
|
||||
@ -5,6 +5,7 @@ Config =
|
||||
'404 Redirect': [true, 'Redirect dead threads and images.']
|
||||
'Keybinds': [true, 'Bind actions to keyboard shortcuts.']
|
||||
'Time Formatting': [true, 'Localize and format timestamps arbitrarily.']
|
||||
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.']
|
||||
'File Info Formatting': [true, 'Reformat the file information.']
|
||||
'Comment Expansion': [true, 'Can expand too long comments.']
|
||||
'Thread Expansion': [true, 'Can expand threads to view all replies.']
|
||||
|
||||
@ -1032,7 +1032,7 @@ Redirect =
|
||||
when 'u'
|
||||
"//nsfw.foolz.us/#{board}/full_image/#{filename}"
|
||||
when 'po'
|
||||
"http://archive.thedarkcave.org/#{board}/full_image/#{filename}"
|
||||
"//archive.thedarkcave.org/#{board}/full_image/#{filename}"
|
||||
when 'ck', 'lit'
|
||||
"//fuuka.warosu.org/#{board}/full_image/#{filename}"
|
||||
when 'diy', 'sci'
|
||||
@ -1049,8 +1049,8 @@ Redirect =
|
||||
"//archive.foolz.us/_/api/chan/post/?board=#{board}&num=#{postID}"
|
||||
when 'u', 'kuku'
|
||||
"//nsfw.foolz.us/_/api/chan/post/?board=#{board}&num=#{postID}"
|
||||
when 'po'
|
||||
"http://archive.thedarkcave.org/_/api/chan/post/?board=#{board}&num=#{postID}"
|
||||
when 'c', 'int', 'po'
|
||||
"//archive.thedarkcave.org/_/api/chan/post/?board=#{board}&num=#{postID}"
|
||||
# for fuuka-based archives:
|
||||
# https://github.com/eksopl/fuuka/issues/27
|
||||
to: (data) ->
|
||||
@ -1060,8 +1060,8 @@ Redirect =
|
||||
url = Redirect.path '//archive.foolz.us', 'foolfuuka', data
|
||||
when 'u', 'kuku'
|
||||
url = Redirect.path '//nsfw.foolz.us', 'foolfuuka', data
|
||||
when 'po'
|
||||
url = Redirect.path 'http://archive.thedarkcave.org', 'foolfuuka', data
|
||||
when 'int', 'po'
|
||||
url = Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data
|
||||
when 'ck', 'lit'
|
||||
url = Redirect.path '//fuuka.warosu.org', 'fuuka', data
|
||||
when 'diy', 'sci'
|
||||
@ -1998,6 +1998,93 @@ Time =
|
||||
S: -> Time.zeroPad @getSeconds()
|
||||
y: -> @getFullYear() - 2000
|
||||
|
||||
RelativeDates =
|
||||
INTERVAL: $.MINUTE
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog' or !Conf['Relative Post Dates']
|
||||
|
||||
# flush when page becomes visible again
|
||||
$.on d, 'visibilitychange ThreadUpdate', @flush
|
||||
|
||||
Post::callbacks.push
|
||||
name: 'Relative Post Dates'
|
||||
cb: @node
|
||||
node: ->
|
||||
dateEl = @nodes.date
|
||||
|
||||
# Show original absolute time as tooltip so users can still know exact times
|
||||
# Since "Time Formatting" runs `node` before us, the title tooltip will
|
||||
# pick up the user-formatted time instead of 4chan time when enabled.
|
||||
dateEl.title = dateEl.textContent
|
||||
|
||||
diff = Date.now() - @info.date
|
||||
|
||||
dateEl.textContent = RelativeDates.relative diff
|
||||
RelativeDates.setUpdate @, diff
|
||||
|
||||
# diff is milliseconds from now
|
||||
relative: (diff) ->
|
||||
unit = if (number = (diff / $.DAY)) > 1
|
||||
'day'
|
||||
else if (number = (diff / $.HOUR)) > 1
|
||||
'hour'
|
||||
else if (number = (diff / $.MINUTE)) > 1
|
||||
'minute'
|
||||
else
|
||||
number = diff / $.SECOND
|
||||
'second'
|
||||
|
||||
rounded = Math.round number
|
||||
unit += 's' if rounded isnt 1 # pluralize
|
||||
|
||||
"#{rounded} #{unit} ago"
|
||||
|
||||
# changing all relative dates as soon as possible incurs many annoying
|
||||
# redraws and scroll stuttering. Thus, sacrifice accuracy for UX/CPU economy,
|
||||
# and perform redraws when the DOM is otherwise being manipulated (and scroll
|
||||
# stuttering won't be noticed), falling back to INTERVAL while the page
|
||||
# is visible.
|
||||
#
|
||||
# each individual dateTime element will add its update() function to the stale list
|
||||
# when it is to be called.
|
||||
stale: []
|
||||
flush: $.debounce($.SECOND, ->
|
||||
# no point in changing the dates until the user sees them
|
||||
return if d.hidden
|
||||
|
||||
now = Date.now()
|
||||
update now for update in RelativeDates.stale
|
||||
RelativeDates.stale = []
|
||||
|
||||
# reset automatic flush
|
||||
clearTimeout RelativeDates.timeout
|
||||
RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL)
|
||||
|
||||
# create function `update()`, closed over post and diff, that, when called
|
||||
# from `flush()`, updates the element, and re-calls `setOwnTimeout()` to
|
||||
# re-add `update()` to the stale list later.
|
||||
setUpdate: (post, diff) ->
|
||||
setOwnTimeout = (diff) ->
|
||||
delay = if diff > $.HOUR
|
||||
diff % $.HOUR
|
||||
else if diff > $.MINUTE
|
||||
diff % $.MINUTE
|
||||
else
|
||||
diff % $.SECOND
|
||||
setTimeout markStale, delay
|
||||
|
||||
dateEl = post.nodes.date
|
||||
update = (now) ->
|
||||
if d.contains dateEl # not removed from DOM
|
||||
diff = now - post.info.date
|
||||
dateEl.textContent = RelativeDates.relative diff
|
||||
setOwnTimeout diff
|
||||
|
||||
markStale = -> RelativeDates.stale.push update
|
||||
|
||||
# kick off initial timeout with current diff
|
||||
setOwnTimeout diff
|
||||
|
||||
FileInfo =
|
||||
init: ->
|
||||
return if g.VIEW is 'catalog' or !Conf['File Info Formatting']
|
||||
@ -2360,7 +2447,7 @@ ThreadUpdater =
|
||||
|
||||
$.on window, 'online offline', ThreadUpdater.cb.online
|
||||
$.on d, 'QRPostSuccessful', ThreadUpdater.cb.post
|
||||
$.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', ThreadUpdater.cb.visibility
|
||||
$.on d, 'visibilitychange', ThreadUpdater.cb.visibility
|
||||
|
||||
ThreadUpdater.cb.online()
|
||||
$.add d.body, ThreadUpdater.dialog
|
||||
@ -2387,7 +2474,7 @@ ThreadUpdater =
|
||||
ThreadUpdater.outdateCount = 0
|
||||
setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2
|
||||
visibility: ->
|
||||
return if $.hidden()
|
||||
return if d.hidden
|
||||
# Reset the counter when we focus this tab.
|
||||
ThreadUpdater.outdateCount = 0
|
||||
if ThreadUpdater.seconds > ThreadUpdater.interval
|
||||
@ -2396,7 +2483,7 @@ ThreadUpdater =
|
||||
ThreadUpdater.scrollBG = if Conf['Scroll BG']
|
||||
-> true
|
||||
else
|
||||
-> not $.hidden()
|
||||
-> not d.hidden
|
||||
autoUpdate: ->
|
||||
if Conf['Auto Update This'] and ThreadUpdater.online
|
||||
ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000
|
||||
@ -2447,7 +2534,7 @@ ThreadUpdater =
|
||||
getInterval: ->
|
||||
i = ThreadUpdater.interval
|
||||
j = Math.min ThreadUpdater.outdateCount, 10
|
||||
unless $.hidden()
|
||||
unless d.hidden
|
||||
# Lower the max refresh rate limit on visible tabs.
|
||||
j = Math.min j, 7
|
||||
ThreadUpdater.seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]
|
||||
@ -2523,7 +2610,7 @@ ThreadUpdater =
|
||||
else
|
||||
ThreadUpdater.set 'status', "+#{count}", 'new'
|
||||
ThreadUpdater.outdateCount = 0
|
||||
if Conf['Beep'] and $.hidden() # XXX and !Unread.replies.length
|
||||
if Conf['Beep'] and d.hidden # XXX and !Unread.replies.length
|
||||
unless ThreadUpdater.audio
|
||||
ThreadUpdater.audio = $.el 'audio', src: ThreadUpdater.beep
|
||||
ThreadUpdater.audio.play()
|
||||
|
||||
@ -295,6 +295,7 @@ Main =
|
||||
console.timeEnd "#{name} initialization"
|
||||
|
||||
console.time 'All initializations'
|
||||
initFeature 'Polyfill', Polyfill
|
||||
initFeature 'Header', Header
|
||||
initFeature 'Settings', Settings
|
||||
initFeature 'Resurrect Quotes', Quotify
|
||||
@ -318,6 +319,7 @@ Main =
|
||||
initFeature 'Mark Cross-thread Quotes', QuoteCT
|
||||
initFeature 'Anonymize', Anonymize
|
||||
initFeature 'Time Formatting', Time
|
||||
initFeature 'Relative Post Dates', RelativeDates
|
||||
initFeature 'File Info Formatting', FileInfo
|
||||
initFeature 'Sauce', Sauce
|
||||
initFeature 'Image Expansion', ImageExpand
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user