Merge branch 'v3'

Conflicts:
	builds/crx/script.js
	src/General/Config.coffee
	src/General/Settings.coffee
	src/Miscellaneous/ExpandComment.coffee
	src/Monitoring/ThreadWatcher.coffee
This commit is contained in:
Zixaphir 2013-08-12 19:22:37 -07:00
commit 6c2c9b281a
31 changed files with 3309 additions and 1196 deletions

View File

@ -16,6 +16,18 @@
**MayhemYDG**: **MayhemYDG**:
- **New feature**: `Show Dice Roll` (with @carboncopy) - **New feature**: `Show Dice Roll` (with @carboncopy)
- Shows dice that were entered into the email field on /tg/. - Shows dice that were entered into the email field on /tg/.
- **Thread Watcher** improvements:
- It is now possible to open all watched threads via the `Open all threads` button in the Thread Watcher's menu.
- Added the `Current Board` setting to switch between showing watched threads from the current board or all boards, disabled by default.
- About dead (404'd) threads:
- Dead threads will be typographically indicated with a strikethrough.
- Dead threads will directly link to the corresponding archive when available.
- A button to prune all 404'd threads from the list is now available.
- Added the `Auto Prune` setting to automatically prune 404'd threads, disabled by default.
- The current thread is now highlighted in the list of watched threads.
- Watching the current thread can be done in the Header's menu too.
- Removed the `Check for Updates` setting:
- Your browser/userscript manager should handle updates itself automatically.
- Fix impossibility to create new threads when in dead threads. - Fix impossibility to create new threads when in dead threads.
- Fix flag filtering on /sp/ and /int/. - Fix flag filtering on /sp/ and /int/.
- Update archives. (with @woxxy and @proplex) - Update archives. (with @woxxy and @proplex)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

929
css/style.css Normal file
View File

@ -0,0 +1,929 @@
/* General */
.dialog {
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
border: 1px solid;
display: block;
padding: 0;
}
.field {
background-color: #FFF;
border: 1px solid #CCC;
-moz-box-sizing: border-box;
box-sizing: border-box;
color: #333;
font-family: inherit;
font-size: 13px;
margin: 0;
padding: 2px 4px 3px;
outline: none;
transition: color .25s, border-color .25s, -webkit-flex .25s;
transition: color .25s, border-color .25s, flex .25s;
}
.field::-moz-placeholder,
.field:hover::-moz-placeholder {
color: #AAA !important;
}
.field:hover {
border-color: #999;
}
.field:hover, .field:focus {
color: #000;
}
.field[disabled] {
background-color: #F2F2F2;
color: #888;
}
.move {
cursor: move;
}
label, .watcher-toggler {
cursor: pointer;
}
a[href="javascript:;"] {
text-decoration: none;
}
.warning {
color: red;
}
/* 4chan style fixes */
.opContainer, .op {
display: block !important;
}
.post {
overflow: visible !important;
}
[hidden] {
display: none !important;
}
/* fixed, z-index */
#overlay,
#qp, #ihover,
#updater, #thread-stats,
#navlinks, #header,
#qr {
position: fixed;
}
#overlay {
z-index: 999;
}
#notifications {
z-index: 70;
}
#qp, #ihover {
z-index: 60;
}
#menu {
z-index: 50;
}
#navlinks, #updater, #thread-stats {
z-index: 40;
}
#qr {
z-index: 30;
}
#thread-watcher:hover {
z-index: 20;
}
#header {
z-index: 10;
}
#thread-watcher {
z-index: 5;
}
/* Header */
:root.top-header body {
margin-top: 2em;
}
:root.bottom-header body {
margin-bottom: 2em;
}
:root.fourchan-x #navtopright,
:root.fourchan-x #navbotright,
:root.fourchan-x:not(.show-original-top-board-list) #boardNavDesktop,
:root.fourchan-x:not(.show-original-bot-board-list) #boardNavDesktopFoot {
display: none !important;
}
#header {
right: 0;
left: 0;
}
#header.top {
top: 0;
}
#header.bottom {
bottom: 0;
}
#header-bar {
border-width: 0;
display: -webkit-flex;
display: flex;
padding: 3px 4px 4px;
position: relative;
transition: all .1s .05s ease-in-out;
}
#header.top #header-bar {
border-bottom-width: 1px;
}
#header.bottom #header-bar {
box-shadow: 0 -1px 2px rgba(0, 0, 0, .15);
border-top-width: 1px;
}
#header.bottom .menu-button i {
border-top: none;
border-bottom: 6px solid;
}
#board-list {
-webkit-flex: 1;
flex: 1;
text-align: center;
}
#header-bar.autohide:not(:hover) {
box-shadow: none;
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
}
#header.top #header-bar.autohide:not(:hover) {
margin-bottom: -1em;
-webkit-transform: translateY(-100%);
transform: translateY(-100%);
}
#header.bottom #header-bar.autohide:not(:hover) {
-webkit-transform: translateY(100%);
transform: translateY(100%);
}
#toggle-header-bar {
left: 0;
right: 0;
height: 10px;
position: absolute;
}
#header.top #toggle-header-bar {
cursor: n-resize;
bottom: -8px;
}
#header.bottom #toggle-header-bar {
cursor: s-resize;
top: -8px;
}
#header-bar.autohide:not(:hover) #toggle-header-bar,
#toggle-header-bar:hover {
height: 18px;
}
#header.top #header-bar.autohide:not(:hover) #toggle-header-bar,
#header.top #toggle-header-bar:hover {
bottom: -16px;
}
#header.bottom #header-bar.autohide:not(:hover) #toggle-header-bar,
#header.bottom #toggle-header-bar:hover {
top: -16px;
}
#header.top #header-bar.autohide #toggle-header-bar {
cursor: s-resize;
}
#header.bottom #header-bar.autohide #toggle-header-bar {
cursor: n-resize;
}
#header-bar a:not(.entry) {
text-decoration: none;
padding: 1px;
}
#shortcuts:empty {
display: none;
}
.shortcut:not(:last-child)::after {
content: " / ";
}
.brackets-wrap::before {
content: "\\00a0[";
}
.brackets-wrap::after {
content: "]\\00a0";
}
.expand-all-shortcut {
opacity: .35;
}
/* Notifications */
#notifications {
height: 0;
text-align: center;
}
#header.bottom #notifications {
position: fixed;
top: 0;
left: 0;
width: 100%;
}
.notification {
color: #FFF;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, .5);
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
border-radius: 2px;
margin: 1px auto;
width: 500px;
max-width: 100%;
position: relative;
transition: all .25s ease-in-out;
}
.notification.error {
background-color: hsla(0, 100%, 38%, .9);
}
.notification.warning {
background-color: hsla(36, 100%, 38%, .9);
}
.notification.info {
background-color: hsla(200, 100%, 38%, .9);
}
.notification.success {
background-color: hsla(104, 100%, 38%, .9);
}
.notification a {
color: white;
}
.notification > .close {
padding: 6px;
top: 0;
right: 0;
position: absolute;
}
.message {
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 6px 20px;
max-height: 200px;
width: 100%;
overflow: auto;
}
/* Settings */
:root.fourchan-x body {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#overlay {
background-color: rgba(0, 0, 0, .5);
display: -webkit-flex;
display: flex;
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
#fourchanx-settings {
-moz-box-sizing: border-box;
box-sizing: border-box;
box-shadow: 0 0 15px rgba(0, 0, 0, .15);
height: 600px;
max-height: 100%;
width: 900px;
max-width: 100%;
margin: auto;
padding: 3px;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
#fourchanx-settings > nav {
display: -webkit-flex;
display: flex;
padding: 2px 2px 0;
}
#fourchanx-settings > nav a {
text-decoration: underline;
}
#fourchanx-settings > nav a.close {
text-decoration: none;
padding: 2px;
}
.sections-list {
-webkit-flex: 1;
flex: 1;
}
.tab-selected {
font-weight: 700;
}
.section-container {
-webkit-flex: 1;
flex: 1;
position: relative;
}
.section-container > section {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
.section-sauce ul,
.section-rice ul {
list-style: none;
margin: 0;
padding: 8px;
}
.section-sauce li,
.section-rice li {
padding-left: 4px;
}
.section-main label {
text-decoration: underline;
}
.section-filter ul,
.section-qr ul {
padding: 0;
}
.section-filter li,
.section-qr li {
margin: 10px 40px;
}
.section-filter textarea {
height: 500px;
}
.section-qr textarea {
height: 200px;
}
.section-sauce textarea {
height: 350px;
}
.section-rice .field[name="boardnav"] {
width: 100%;
}
.section-rice textarea {
height: 150px;
}
.section-archives table {
width: 100%;
}
.section-archives th:not(:first-child) {
width: 30%;
}
.section-archives td {
text-align: center;
}
.section-archives select {
width: 90%;
}
.section-keybinds .field {
font-family: monospace;
}
#fourchanx-settings fieldset {
border: 1px solid;
border-radius: 3px;
}
#fourchanx-settings legend {
font-weight: 700;
}
#fourchanx-settings textarea {
font-family: monospace;
min-width: 100%;
max-width: 100%;
}
#fourchanx-settings code {
color: #000;
background-color: #FFF;
padding: 0 2px;
}
.unscroll {
overflow: hidden;
}
/* Announcement Hiding */
:root.hide-announcement #globalMessage,
:root.hide-announcement-enabled #toggleMsgBtn {
display: none;
}
a.hide-announcement {
float: left;
}
/* Unread */
#unread-line {
margin: 0;
}
/* Thread Updater */
#updater:not(:hover) {
background: none;
border: none;
box-shadow: none;
}
#updater > .move {
padding: 0 3px;
}
#updater > div:last-child {
text-align: center;
}
#updater input[type=number] {
width: 4em;
}
#updater:not(:hover) > div:not(.move) {
display: none;
}
#updater input[type="button"] {
width: 100%;
}
.new {
color: limegreen;
}
/* Thread Watcher */
#thread-watcher {
max-width: 200px;
min-width: 150px;
padding: 3px;
position: absolute;
}
#thread-watcher > div:first-child {
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
}
#thread-watcher .move {
-webkit-flex: 1;
flex: 1;
}
#watcher-status:not(:empty)::before {
content: "(";
}
#watcher-status:not(:empty)::after {
content: ")";
}
#watched-threads:not(:hover) {
max-height: 150px;
overflow: hidden;
}
#watched-threads div {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#watched-threads .current {
font-weight: 700;
}
#watched-threads a {
text-decoration: none;
}
#watched-threads .dead-thread a[title] {
text-decoration: line-through;
}
/* Thread Stats */
#thread-stats {
background: none;
border: none;
box-shadow: none;
}
/* Quote */
.deadlink {
text-decoration: none !important;
}
.backlink.deadlink:not(.forwardlink),
.quotelink.deadlink:not(.forwardlink) {
text-decoration: underline !important;
}
.inlined {
opacity: .5;
}
#qp input, .forwarded {
display: none;
}
.quotelink.forwardlink,
.backlink.forwardlink {
text-decoration: none;
border-bottom: 1px dashed;
}
.filtered {
text-decoration: underline line-through;
}
.inline {
border: 1px solid;
display: table;
margin: 2px 0;
}
.inline .post {
border: 0 !important;
background-color: transparent !important;
display: table !important;
margin: 0 !important;
padding: 1px 2px !important;
}
#qp > .opContainer::after {
content: '';
clear: both;
display: table;
}
#qp .post {
border: none;
margin: 0;
padding: 2px 2px 5px;
}
#qp img {
max-height: 80vh;
max-width: 50vw;
}
.qphl {
outline: 2px solid rgba(216, 94, 49, .7);
}
/* File */
.fileText:hover .fntrunc,
.fileText:not(:hover) .fnfull,
.expanded-image > .post > .file > .fileThumb > img[data-md5],
:not(.expanded-image) > .post > .file > .fileThumb > .full-image {
display: none;
}
.expanding {
opacity: .5;
}
.expanded-image {
clear: both;
}
.expanded-image > .op > .file::after {
content: '';
clear: both;
display: table;
}
:root.fit-height .full-image {
max-height: 100vh;
}
:root.fit-width .full-image {
max-width: 100%;
}
:root.gecko.fit-width .full-image {
width: 100%;
}
#ihover {
-moz-box-sizing: border-box;
box-sizing: border-box;
max-height: 100%;
max-width: 75%;
padding-bottom: 16px;
}
/* Index/Reply Navigation */
#navlinks {
font-size: 16px;
top: 25px;
right: 10px;
}
/* Filter */
.opContainer.filter-highlight {
box-shadow: inset 5px 0 rgba(255, 0, 0, .5);
}
.filter-highlight > .reply {
box-shadow: -5px 0 rgba(255, 0, 0, .5);
}
/* Thread & Reply Hiding */
.hide-thread-button,
.hide-reply-button {
float: left;
margin-right: 2px;
}
.stub ~ * {
display: none !important;
}
.stub input {
display: inline-block;
}
/* QR */
:root.hide-original-post-form #postForm,
:root.hide-original-post-form .postingMode,
:root.hide-original-post-form #togglePostForm,
#qr.autohide:not(.has-focus):not(:hover) > form {
display: none;
}
#qr select, #dump-button, .remove, .captcha-img {
cursor: pointer;
}
#qr > div {
min-width: 300px;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
}
#qr .move {
-webkit-align-self: stretch;
align-self: stretch;
-webkit-flex: 1;
flex: 1;
}
#qr select {
margin: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
background: none;
}
#qr option {
color: #000;
background-color: #F7F7F7;
}
#qr .close {
padding: 0 3px;
}
#qr > form {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
.persona {
display: -webkit-flex;
display: flex;
}
.persona .field {
-webkit-flex: 1;
flex: 1;
}
.persona .field:hover,
.persona .field:focus {
-webkit-flex: 3;
flex: 3;
}
#dump-button {
background: linear-gradient(#EEE, #CCC);
border: 1px solid #CCC;
margin: 0;
padding: 2px 4px 3px;
outline: none;
width: 30px;
}
#dump-button:hover,
#dump-button:focus {
background: linear-gradient(#FFF, #DDD);
}
#dump-button:active,
.dump #dump-button:not(:hover):not(:focus) {
background: linear-gradient(#CCC, #DDD);
}
:root.gecko #dump-button {
padding: 0;
}
#qr:not(.dump) #dump-list-container {
display: none;
}
#dump-list-container {
height: 100px;
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
#dump-list {
counter-reset: qrpreviews;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
#dump-list:hover {
bottom: -12px;
overflow-x: auto;
z-index: 1;
}
#dump-list::-webkit-scrollbar {
height: 12px;
}
#dump-list::-webkit-scrollbar-thumb {
border: 1px solid;
}
.qr-preview {
background-position: 50% 20%;
background-size: cover;
border: 1px solid #808080;
color: #FFF !important;
font-size: 12px;
-moz-box-sizing: border-box;
box-sizing: border-box;
cursor: move;
display: inline-block;
height: 92px;
width: 92px;
margin: 4px;
padding: 2px;
opacity: .6;
outline: none;
overflow: hidden;
position: relative;
text-shadow: 0 1px 1px #000;
transition: opacity .25s ease-in-out;
vertical-align: top;
white-space: pre;
}
.qr-preview:hover,
.qr-preview:focus {
opacity: .9;
color: #FFF !important;
}
.qr-preview#selected {
opacity: 1;
}
.qr-preview::before {
counter-increment: qrpreviews;
content: counter(qrpreviews);
font-weight: 700;
text-shadow: 0 0 3px #000, 0 0 5px #000;
position: absolute;
top: 3px;
right: 3px;
}
.qr-preview.drag {
border-color: red;
border-style: dashed;
}
.qr-preview.over {
border-color: #FFF;
border-style: dashed;
}
.remove {
color: #E00 !important;
font-weight: 700;
padding: 3px;
}
.remove:hover::after {
content: ' Remove';
}
.qr-preview > label {
background: rgba(0, 0, 0, .5);
right: 0;
bottom: 0;
left: 0;
position: absolute;
text-align: center;
}
.qr-preview > label > input {
margin: 1px 0;
vertical-align: bottom;
}
#add-post {
display: inline-block;
font-size: 30px;
height: 30px;
width: 30px;
line-height: 1;
text-align: center;
position: absolute;
right: 0;
bottom: 0;
z-index: 1;
}
#qr textarea {
min-height: 160px;
min-width: 100%;
display: block;
}
#qr.has-captcha textarea {
min-height: 120px;
}
.textarea {
position: relative;
}
#char-count {
color: #000;
background: hsla(0, 0%, 100%, .5);
font-size: 8pt;
position: absolute;
bottom: 1px;
right: 1px;
pointer-events: none;
}
#char-count.warning {
color: red;
}
.captcha-img {
background: #FFF;
outline: 1px solid #CCC;
outline-offset: -1px;
}
.captcha-img > img {
display: block;
height: 57px;
width: 300px;
}
#file-n-submit > input {
margin: 0;
}
#file-n-submit.has-file #qr-no-file {
visibility: hidden;
}
#file-n-submit:not(.has-file) #qr-filename,
#file-n-submit:not(.has-file) #qr-file-spoiler,
#file-n-submit:not(.has-file) #qr-filerm {
display: none;
}
#file-n-submit {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
align-items: center;
}
#qr-no-file, #qr-filename-container {
-webkit-flex: 1;
flex: 1;
}
#qr-filename-container {
cursor: default;
position: relative;
margin-left: 2px;
}
#qr-filename {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
#qr-filerm {
padding: 0 2px;
}
#file-n-submit > #qr-file-spoiler {
margin: 0 2px;
}
#file-n-submit input[type='submit'] {
min-width: 40px;
-webkit-order: 1;
order: 1;
}
/* Menu */
.menu-button {
display: inline-block;
position: relative;
}
.menu-button i {
border-top: 6px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
display: inline-block;
margin: 2px;
vertical-align: middle;
}
#menu {
border-bottom: 0;
display: -webkit-flex;
display: flex;
margin: 2px 0;
-webkit-flex-direction: column;
flex-direction: column;
position: absolute;
outline: none;
}
.entry {
cursor: pointer;
outline: none;
padding: 3px 7px;
position: relative;
text-decoration: none;
white-space: nowrap;
}
.entry.disabled {
color: graytext !important;
}
.entry.has-submenu {
padding-right: 20px;
}
.has-submenu::after {
content: '';
border-left: 6px solid;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
display: inline-block;
margin: 4px;
position: absolute;
right: 3px;
}
.has-submenu:not(.focused) > .submenu {
display: none;
}
.submenu {
border-bottom: 0;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
position: absolute;
margin: -1px 0;
}
.entry input {
margin: 0;
}

View File

@ -50,8 +50,8 @@
"http": true, "http": true,
"https": true, "https": true,
"software": "foolfuuka", "software": "foolfuuka",
"boards": ["adv", "asp", "cm", "e", "i", "lgbt", "n", "o", "p", "pol", "s", "s4s", "t", "trv", "y"], "boards": ["adv", "asp", "cm", "d", "e", "i", "lgbt", "n", "o", "p", "pol", "s", "s4s", "t", "trv", "y"],
"files": ["adv", "asp", "cm", "e", "i", "lgbt", "n", "o", "p", "s", "s4s", "t", "trv", "y"] "files": ["cm", "d", "e", "i", "n", "o", "p", "s", "trv", "y"]
}, { }, {
"uid": 12, "uid": 12,
"name": "fap archive", "name": "fap archive",

View File

@ -50,8 +50,8 @@ Redirect =
http: true http: true
https: true https: true
software: 'foolfuuka' software: 'foolfuuka'
boards: ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv'] boards: ['adv', 'asp', 'cm', 'd', 'e', 'i', 'lgbt', 'n', 'o', 'p', 'pol', 's', 's4s', 't', 'trv', 'y']
files: ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv'] files: ['cm', 'd', 'e', 'i', 'n', 'o', 'p', 's', 'trv', 'y']
'Foolz Beta': 'Foolz Beta':
domain: 'beta.foolz.us' domain: 'beta.foolz.us'

View File

@ -27,7 +27,7 @@ Build =
date: data.now date: data.now
dateUTC: data.time dateUTC: data.time
comment: data.com comment: data.com
capReps: data.capcode_replies capcodeReplies: data.capcode_replies
# thread status # thread status
isSticky: !!data.sticky isSticky: !!data.sticky
isClosed: !!data.closed isClosed: !!data.closed
@ -59,7 +59,7 @@ Build =
postID, threadID, boardID postID, threadID, boardID
name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC
isSticky, isClosed isSticky, isClosed
comment, capReps comment, capcodeReplies
file file
} = o } = o
isOP = postID is threadID isOP = postID is threadID
@ -191,8 +191,23 @@ Build =
else else
'' ''
capcodeReplies = '' container = $.el 'div',
if capReps id: "pc#{postID}"
className: "postContainer #{if isOP then 'op' else 'reply'}Container"
innerHTML: <%= grunt.file.read('src/General/html/Build/post.html').replace(/>\s+/g, '>').replace(/\s+</g, '<').replace(/\s+/g, ' ').trim() %>
for quote in $$ '.quotelink', container
href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
Build.capcodeReplies {boardID, threadID, root: container, capcodeReplies}
container
capcodeReplies: ({boardID, threadID, bq, root, capcodeReplies}) ->
return unless capcodeReplies
generateCapcodeReplies = (capcodeType, array) -> generateCapcodeReplies = (capcodeType, array) ->
"<span class=smaller><span class=bold>#{ "<span class=smaller><span class=bold>#{
switch capcodeType switch capcodeType
@ -207,18 +222,15 @@ Build =
"<a href='/#{boardID}/res/#{threadID}#p#{ID}' class=quotelink>&gt;&gt;#{ID}</a>" "<a href='/#{boardID}/res/#{threadID}#p#{ID}' class=quotelink>&gt;&gt;#{ID}</a>"
.join ' ' .join ' '
}</span><br>" }</span><br>"
for capcodeType, array of capReps html = []
capcodeReplies += generateCapcodeReplies capcodeType, array for capcodeType, array of capcodeReplies
capcodeReplies = "<br><br><span class=capcodeReplies>#{capcodeReplies}</span>" html.push generateCapcodeReplies capcodeType, array
container = $.el 'div', bq or= $ 'blockquote', root
id: "pc#{postID}" $.add bq, [
className: "postContainer #{if isOP then 'op' else 'reply'}Container" $.el 'br'
innerHTML: <%= grunt.file.read('src/General/html/Build/post.html').replace(/>\s+/g, '>').replace(/\s+</g, '<').replace(/\s+/g, ' ').trim() %> $.el 'br'
$.el 'span',
for quote in $$ '.quotelink', container className: 'capcodeReplies'
href = quote.getAttribute 'href' innerHTML: html.join ''
continue if href[0] is '/' # Cross-board quote, or board link ]
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
container

View File

@ -53,12 +53,6 @@ Config =
true true
'Show dice that were entered into the email field.' 'Show dice that were entered into the email field.'
] ]
<% if (type !== 'crx') { %>
'Check for Updates': [
true
'Check for updated versions of <%= meta.name %>.'
]
<% } %>
'Color User IDs': [ 'Color User IDs': [
false false
'Assign unique colors to user IDs on boards that use them' 'Assign unique colors to user IDs on boards that use them'
@ -239,14 +233,6 @@ Config =
true true
'Bookmark threads.' 'Bookmark threads.'
] ]
'Auto Watch': [
true
'Automatically watch threads you start.'
]
'Auto Watch Reply': [
false
'Automatically watch threads you reply to.'
]
'Posting': 'Posting':
'Header Shortcut': [ 'Header Shortcut': [
@ -754,6 +740,24 @@ Config =
['before', 'after'] ['before', 'after']
] ]
threadWatcher:
'Current Board': [
false
'Only show watched threads from the current board.'
]
'Auto Watch': [
true
'Automatically watch threads you start.'
]
'Auto Watch Reply': [
false
'Automatically watch threads you reply to.'
]
'Auto Prune': [
false
'Automatically prune 404\'d threads.'
]
filter: filter:
name: """ name: """
# Filter any namefags: # Filter any namefags:

View File

@ -79,7 +79,6 @@ Header =
fourchannav = $.id 'boardNavDesktop' fourchannav = $.id 'boardNavDesktop'
if a = $ "a[href*='/#{g.BOARD}/']", fourchannav if a = $ "a[href*='/#{g.BOARD}/']", fourchannav
a.className = 'current' a.className = 'current'
boardList = $.el 'span', boardList = $.el 'span',
id: 'board-list' id: 'board-list'
innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container brackets-wrap'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>" innerHTML: "<span id=custom-board-list></span><span id=full-board-list hidden><span class='hide-board-list-container brackets-wrap'><a href=javascript:; class='hide-board-list-button'>&nbsp;-&nbsp;</a></span> #{fourchannav.innerHTML}</span>"

View File

@ -163,6 +163,7 @@ Main =
'Thread Updater': ThreadUpdater 'Thread Updater': ThreadUpdater
'Thread Stats': ThreadStats 'Thread Stats': ThreadStats
'Thread Watcher': ThreadWatcher 'Thread Watcher': ThreadWatcher
'Thread Watcher (Menu)': ThreadWatcher.menu
'Index Navigation': Nav 'Index Navigation': Nav
'Keybinds': Keybinds 'Keybinds': Keybinds
'Show Dice Roll': Dice 'Show Dice Roll': Dice
@ -204,9 +205,6 @@ Main =
Main.callbackNodes Thread, threads Main.callbackNodes Thread, threads
Main.callbackNodesDB Post, posts, -> Main.callbackNodesDB Post, posts, ->
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
if styleSelector = $.id 'styleSelector' if styleSelector = $.id 'styleSelector'
passLink = $.el 'a', passLink = $.el 'a',
@ -226,9 +224,6 @@ Main =
new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30 new Notification 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
$.event '4chanXInitFinished' $.event '4chanXInitFinished'
<% if (type !== 'crx') { %>
Main.checkUpdate()
<% } %>
callbackNodes: (klass, nodes) -> callbackNodes: (klass, nodes) ->
# get the nodes' length only once # get the nodes' length only once
@ -302,27 +297,6 @@ Main =
obj.callback.isAddon = true obj.callback.isAddon = true
Klass::callbacks.push obj.callback Klass::callbacks.push obj.callback
<% if (type !== 'crx') { %>
message: (e) ->
{version} = e.data
if version and version isnt g.VERSION
el = $.el 'span',
innerHTML: "Update: <%= meta.name %> v#{version} is out, get it <a href=<%= meta.page %> target=_blank>here</a>."
new Notification 'info', el, 120
checkUpdate: ->
return unless Conf['Check for Updates'] and Main.isThisPageLegit()
now = Date.now()
$.get 'lastchecked', 0, ({lastchecked}) ->
if (lastchecked > now - $.DAY)
return
$.ready ->
$.on window, 'message', Main.message
$.set 'lastchecked', now
$.add d.head, $.el 'script',
src: '<%= meta.repo %>raw/<%= meta.mainBranch %>/latest.js'
<% } %>
handleErrors: (errors) -> handleErrors: (errors) ->
unless errors instanceof Array unless errors instanceof Array
error = errors error = errors

View File

@ -29,7 +29,8 @@ Settings =
else else
$.on d, '4chanXInitFinished', Settings.open $.on d, '4chanXInitFinished', Settings.open
$.set $.set
lastchecked: Date.now() archives: Conf['archives']
lastarchivecheck: now
previousversion: g.VERSION previousversion: g.VERSION
Settings.addSection 'Style', Settings.style Settings.addSection 'Style', Settings.style
@ -173,7 +174,6 @@ Settings =
data = data =
version: g.VERSION version: g.VERSION
date: now date: now
Conf['WatchedThreads'] = {}
for db in DataBoards for db in DataBoards
Conf[db] = boards: {} Conf[db] = boards: {}
# Make sure to export the most recent data. # Make sure to export the most recent data.
@ -220,7 +220,9 @@ Settings =
reader.readAsText file reader.readAsText file
loadSettings: (data) -> loadSettings: (data) ->
version = data.version.split '.' if data.Conf['WatchedThreads']
data.Conf['watchedThreads'] = boards: ThreadWatcher.convert data.Conf['WatchedThreads']
delete data.Conf['WatchedThreads']
$.set data.Conf $.set data.Conf
convertSettings: (data, map) -> convertSettings: (data, map) ->

View File

@ -1,7 +1,7 @@
#navtopright .exlinksOptionsLink::after, #navtopright .exlinksOptionsLink::after,
#main-menu, #main-menu,
body > div.navLinks > a:first-of-type::after, body > div.navLinks > a:first-of-type::after,
.slideout-watcher #watcher::after, .slideout-watcher #thread-watcher::after,
.announcements-slideout #globalMessage::after, .announcements-slideout #globalMessage::after,
#boardNavDesktopFoot::after, #boardNavDesktopFoot::after,
#img-controls, #img-controls,
@ -26,7 +26,7 @@ body::after {
.invisible-icons #navtopright .exlinksOptionsLink::after, .invisible-icons #navtopright .exlinksOptionsLink::after,
.invisible-icons #main-menu, .invisible-icons #main-menu,
.invisible-icons body > div.navLinks > a:first-of-type::after, .invisible-icons body > div.navLinks > a:first-of-type::after,
.invisible-icons.slideout-watcher #watcher::after, .invisible-icons.slideout-watcher #thread-watcher::after,
.invisible-icons.announcements-slideout #globalMessage::after, .invisible-icons.announcements-slideout #globalMessage::after,
.invisible-icons #boardNavDesktopFoot::after, .invisible-icons #boardNavDesktopFoot::after,
.invisible-icons #img-controls, .invisible-icons #img-controls,
@ -36,7 +36,7 @@ body::after {
} }
#navtopright .exlinksOptionsLink, #navtopright .exlinksOptionsLink,
body > div.navLinks > a:first-of-type, body > div.navLinks > a:first-of-type,
#{if Conf['Slideout Watcher'] then '#watcher,' else ''} #{if Conf['Slideout Watcher'] then '#thread-watcher,' else ''}
#{if Conf['Announcements'] is 'slideout' then '#globalMessage,' else ''} #{if Conf['Announcements'] is 'slideout' then '#globalMessage,' else ''}
#boardNavDesktopFoot, #boardNavDesktopFoot,
#catalog { #catalog {
@ -44,7 +44,7 @@ body > div.navLinks > a:first-of-type,
} }
#navtopright .exlinksOptionsLink:hover, #navtopright .exlinksOptionsLink:hover,
body > div.navLinks > a:first-of-type:hover, body > div.navLinks > a:first-of-type:hover,
.slideout-watcher #watcher:hover, .slideout-watcher #thread-watcher:hover,
.announcements-slideout #globalMessage:hover, .announcements-slideout #globalMessage:hover,
#boardNavDesktopFoot:hover, #boardNavDesktopFoot:hover,
#img-controls, #img-controls,
@ -60,7 +60,7 @@ body > div.navLinks > a:first-of-type::after {
cursor: pointer; cursor: pointer;
background-position: 0 -15px; background-position: 0 -15px;
} }
.slideout-watcher #watcher::after { .slideout-watcher #thread-watcher::after {
background-position: 0 -30px; background-position: 0 -30px;
} }
.announcements-slideout #globalMessage::after { .announcements-slideout #globalMessage::after {
@ -90,7 +90,7 @@ body > div.navLinks > a:first-of-type::after {
#main-menu:hover, #main-menu:hover,
#navtopright .exlinksOptionsLink:hover::after, #navtopright .exlinksOptionsLink:hover::after,
#qr #qrtab, #qr #qrtab,
.slideout-watcher #watcher:hover::after, .slideout-watcher #thread-watcher:hover::after,
.thumbnail#selected, .thumbnail#selected,
div.navLinks > a:first-of-type:hover::after, div.navLinks > a:first-of-type:hover::after,
#catalog:hover::after, #catalog:hover::after,

View File

@ -14,7 +14,7 @@ body::after {
#{align}: #{position[i++]}px; #{align}: #{position[i++]}px;
} }
/* Watcher */ /* Watcher */
.slideout-watcher #watcher::after { .slideout-watcher #thread-watcher::after {
#{align}: #{position[i++]}px; #{align}: #{position[i++]}px;
} }
/* ExLinks */ /* ExLinks */
@ -55,7 +55,7 @@ body::after {
#boardNavDesktopFoot::after, #boardNavDesktopFoot::after,
#navtopright .exlinksOptionsLink::after, #navtopright .exlinksOptionsLink::after,
#main-menu, #main-menu,
.slideout-watcher #watcher::after, .slideout-watcher #thread-watcher::after,
.announcements-slideout #globalMessage::after, .announcements-slideout #globalMessage::after,
#img-controls, #img-controls,
#fappeTyme, #fappeTyme,
@ -64,7 +64,7 @@ div.navLinks > a:first-of-type::after,
top: 1px !important; top: 1px !important;
} }
.slideout-watcher #globalMessage, .slideout-watcher #globalMessage,
.slideout-watcher #watcher, .slideout-watcher #thread-watcher,
#boardNavDesktopFoot { #boardNavDesktopFoot {
top: 16px !important; top: 16px !important;
} }

View File

@ -20,8 +20,8 @@ body::after {
top: #{position[i++]}px; top: #{position[i++]}px;
} }
/* Watcher */ /* Watcher */
.slideout-watcher #watcher, .slideout-watcher #thread-watcher,
.slideout-watcher #watcher::after { .slideout-watcher #thread-watcher::after {
top: #{position[i++]}px !important; top: #{position[i++]}px !important;
} }
/* ExLinks */ /* ExLinks */
@ -58,21 +58,21 @@ body::after {
#globalMessage::after, #globalMessage::after,
#img-controls, #img-controls,
#fappeTyme, #fappeTyme,
.slideout-watcher #watcher::after, .slideout-watcher #thread-watcher::after,
#catalog::after, #catalog::after,
div.navLinks > a:first-of-type::after { div.navLinks > a:first-of-type::after {
#{align}: 3px !important; #{align}: 3px !important;
} }
#boardNavDesktopFoot, #boardNavDesktopFoot,
#globalMessage, #globalMessage,
.slideout-watcher #watcher.dialog { .slideout-watcher #thread-watcher.dialog {
<%= sizing %>: border-box; <%= sizing %>: border-box;
width: 232px !important; width: 232px !important;
#{align}: 18px !important; #{align}: 18px !important;
} }
.sidebar-large #boardNavDesktopFoot, .sidebar-large #boardNavDesktopFoot,
.sidebar-large #globalMessage, .sidebar-large #globalMessage,
.sidebar-large #watcher { .sidebar-large #thread-watcher {
width: 288px !important; width: 288px !important;
} }
.fourchan-ss-navigation.fixed.top #header-bar, .fourchan-ss-navigation.fixed.top #header-bar,

View File

@ -482,7 +482,7 @@ th {
.icons-4chan-ss #navtopright .exlinksOptionsLink::after, .icons-4chan-ss #navtopright .exlinksOptionsLink::after,
.icons-4chan-ss #main-menu, .icons-4chan-ss #main-menu,
.icons-4chan-ss .navLinks > a:first-of-type::after, .icons-4chan-ss .navLinks > a:first-of-type::after,
.icons-4chan-ss #watcher::after, .icons-4chan-ss #thread-watcher::after,
.icons-4chan-ss #globalMessage::after, .icons-4chan-ss #globalMessage::after,
.icons-4chan-ss #boardNavDesktopFoot::after, .icons-4chan-ss #boardNavDesktopFoot::after,
.icons-4chan-ss #img-controls, .icons-4chan-ss #img-controls,
@ -493,7 +493,7 @@ th {
.icons-oneechan #navtopright .exlinksOptionsLink::after, .icons-oneechan #navtopright .exlinksOptionsLink::after,
.icons-oneechan #main-menu, .icons-oneechan #main-menu,
.icons-oneechan .navLinks > a:first-of-type::after, .icons-oneechan .navLinks > a:first-of-type::after,
.icons-oneechan #watcher::after, .icons-oneechan #thread-watcher::after,
.icons-oneechan #globalMessage::after, .icons-oneechan #globalMessage::after,
.icons-oneechan #boardNavDesktopFoot::after, .icons-oneechan #boardNavDesktopFoot::after,
.icons-oneechan #img-controls, .icons-oneechan #img-controls,
@ -751,53 +751,53 @@ th {
display: none; display: none;
} }
/* Watcher */ /* Watcher */
#watcher { #thread-watcher {
position: fixed; position: fixed;
z-index: 14; z-index: 14;
padding: 2px; padding: 2px;
} }
#watcher { #thread-watcher {
width: 200px; width: 200px;
} }
#watcher:not(:hover) { #thread-watcher:not(:hover) {
max-height: 200px; max-height: 200px;
overflow: hidden; overflow: hidden;
} }
.rounded-edges #watcher { .rounded-edges #thread-watcher {
border-radius: 3px; border-radius: 3px;
} }
#watcher > div { #thread-watcher > div {
max-height: 1.3em; max-height: 1.3em;
overflow: hidden; overflow: hidden;
} }
.slideout-watcher #watcher { .slideout-watcher #thread-watcher {
<%= sizing %>: border-box; <%= sizing %>: border-box;
width: 248px; width: 248px;
} }
.slideout-watcher.sidebar-large #boardNavDesktopFoot { .slideout-watcher.sidebar-large #boardNavDesktopFoot {
width: 299px; width: 299px;
} }
.slideout-watcher.sidebar-location-right #watcher { .slideout-watcher.sidebar-location-right #thread-watcher {
left: auto !important; left: auto !important;
right: 2px !important; right: 2px !important;
} }
.slideout-watcher.sidebar-location-left #watcher { .slideout-watcher.sidebar-location-left #thread-watcher {
right: auto !important; right: auto !important;
left: 2px !important; left: 2px !important;
} }
.slideout-watcher #watcher .move { .slideout-watcher #thread-watcher .move {
cursor: default; cursor: default;
} }
.slideout-watcher.underline-links #watcher .move { .slideout-watcher.underline-links #thread-watcher .move {
text-decoration: underline; text-decoration: underline;
} }
.slideout-watcher #watcher > div { .slideout-watcher #thread-watcher > div {
overflow: hidden; overflow: hidden;
} }
.slideout-watcher #watcher:hover { .slideout-watcher #thread-watcher:hover {
overflow-y: auto; overflow-y: auto;
} }
.slideout-watcher #watcher:not(:hover) { .slideout-watcher #thread-watcher:not(:hover) {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
border: 0 none; border: 0 none;

View File

@ -97,10 +97,10 @@ a {
#qr { #qr {
z-index: 30; z-index: 30;
} }
#watcher { #thread-watcher {
z-index: 8; z-index: 8;
} }
:root.fixed-watcher #watcher { :root.fixed-watcher #thread-watcher {
z-index: 20; z-index: 20;
} }
.fixed #header-bar { .fixed #header-bar {
@ -465,10 +465,10 @@ a.hide-announcement {
} }
/* Thread Watcher */ /* Thread Watcher */
#watcher { #thread-watcher {
position: absolute; position: absolute;
} }
#watcher { #thread-watcher {
padding-bottom: 3px; padding-bottom: 3px;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
@ -476,27 +476,27 @@ a.hide-announcement {
max-height: 92%; max-height: 92%;
overflow-y: auto; overflow-y: auto;
} }
:root.fixed-watcher #watcher { :root.fixed-watcher #thread-watcher {
position: fixed; position: fixed;
} }
:root:not(.fixed-watcher) #watcher:not(:hover) { :root:not(.fixed-watcher) #thread-watcher:not(:hover) {
max-height: 210px; max-height: 210px;
overflow-y: hidden; overflow-y: hidden;
} }
#watcher > .move { #thread-watcher > .move {
padding-top: 3px; padding-top: 3px;
} }
#watcher > div { #thread-watcher > div {
max-width: 250px; max-width: 250px;
overflow: hidden; overflow: hidden;
padding-left: 3px; padding-left: 3px;
padding-right: 3px; padding-right: 3px;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#watcher a { #thread-watcher a {
text-decoration: none; text-decoration: none;
} }
#watcher .move>.close { #thread-watcher .move>.close {
position: absolute; position: absolute;
right: 0px; right: 0px;
top: 0px; top: 0px;

View File

@ -165,8 +165,8 @@ textarea.field:focus {
#menu, #menu,
#selectrice, #selectrice,
#themeConf, #themeConf,
#watcher, #thread-watcher,
#watcher:hover, #thread-watcher:hover,
.announcements-slideout #globalMessage, .announcements-slideout #globalMessage,
.dialog, .dialog,
.post-form-style-float #qr, .post-form-style-float #qr,
@ -343,7 +343,7 @@ a .name {
#navtopright .exlinksOptionsLink::after, #navtopright .exlinksOptionsLink::after,
#main-menu, #main-menu,
.navLinks > a:first-of-type::after, .navLinks > a:first-of-type::after,
#watcher::after, #thread-watcher::after,
#globalMessage::after, #globalMessage::after,
#boardNavDesktopFoot::after, #boardNavDesktopFoot::after,
#img-controls, #img-controls,

View File

@ -54,6 +54,6 @@
#{if isOP then '' else fileHTML} #{if isOP then '' else fileHTML}
<blockquote class=postMessage id=m#{postID}>#{comment or ''}#{capcodeReplies}</blockquote>#{" "} <blockquote class=postMessage id=m#{postID}>#{comment or ''}</blockquote>#{" "}
</div>""" </div>"""

View File

@ -0,0 +1,5 @@
<div>
<span class="move">Thread Watcher <span id="watcher-status"></span></span>
<a class="menu-button brackets-wrap" href="javascript:;"><i class=drop-marker></i></a>
</div>
<div id="watched-threads"></div>

View File

@ -54,14 +54,19 @@ $.extend = (object, properties) ->
object[key] = val object[key] = val
return return
$.ajax = (url, options, extra={}) -> $.ajax = do ->
{type, headers, upCallbacks, form, sync} = extra # Status Code 304: Not modified
# With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
# This saves a lot of bandwidth and CPU time for both the users and the servers.
lastModified = {}
(url, options, extra={}) ->
{type, whenModified, upCallbacks, form, sync} = extra
r = new XMLHttpRequest() r = new XMLHttpRequest()
r.overrideMimeType 'text/html'
type or= form and 'post' or 'get' type or= form and 'post' or 'get'
r.open type, url, !sync r.open type, url, !sync
for key, val of headers if whenModified
r.setRequestHeader key, val r.setRequestHeader 'If-Modified-Since', lastModified[url] or '0'
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
$.extend r, options $.extend r, options
$.extend r.upload, upCallbacks $.extend r.upload, upCallbacks
r.send form r.send form

View File

@ -1,10 +1,10 @@
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts'] DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
class DataBoard class DataBoard
constructor: (@key, sync) -> constructor: (@key, sync, dontClean) ->
@data = Conf[key] @data = Conf[key]
$.sync key, @onSync.bind @ $.sync key, @onSync.bind @
@clean() @clean() unless dontClean
return unless sync return unless sync
# Chrome also fires the onChanged callback on the current tab, # Chrome also fires the onChanged callback on the current tab,
# so we only start syncing when we're ready. # so we only start syncing when we're ready.
@ -13,6 +13,9 @@ class DataBoard
@sync = sync @sync = sync
$.on d, '4chanXInitFinished', init $.on d, '4chanXInitFinished', init
save: ->
$.set @key, @data
delete: ({boardID, threadID, postID}) -> delete: ({boardID, threadID, postID}) ->
if postID if postID
delete @data.boards[boardID][threadID][postID] delete @data.boards[boardID][threadID][postID]
@ -22,7 +25,7 @@ class DataBoard
@deleteIfEmpty {boardID} @deleteIfEmpty {boardID}
else else
delete @data.boards[boardID] delete @data.boards[boardID]
$.set @key, @data @save()
deleteIfEmpty: ({boardID, threadID}) -> deleteIfEmpty: ({boardID, threadID}) ->
if threadID if threadID
@ -39,7 +42,7 @@ class DataBoard
(@data.boards[boardID] or= {})[threadID] = val (@data.boards[boardID] or= {})[threadID] = val
else else
@data.boards[boardID] = val @data.boards[boardID] = val
$.set @key, @data @save()
get: ({boardID, threadID, postID, defaultValue}) -> get: ({boardID, threadID, postID, defaultValue}) ->
if board = @data.boards[boardID] if board = @data.boards[boardID]
@ -67,8 +70,7 @@ class DataBoard
@data.lastChecked = now @data.lastChecked = now
for boardID of @data.boards for boardID of @data.boards
@ajaxClean boardID @ajaxClean boardID
@save()
$.set @key, @data
ajaxClean: (boardID) -> ajaxClean: (boardID) ->
$.cache "//api.4chan.org/#{boardID}/threads.json", (e) => $.cache "//api.4chan.org/#{boardID}/threads.json", (e) =>
@ -84,7 +86,7 @@ class DataBoard
threads[thread.no] = board[thread.no] threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads @data.boards[boardID] = threads
@deleteIfEmpty {boardID} @deleteIfEmpty {boardID}
$.set @key, @data @save()
onSync: (data) -> onSync: (data) ->
@data = data or boards: {} @data = data or boards: {}

View File

@ -1,6 +1,16 @@
Polyfill = Polyfill =
init: -> init: ->
Polyfill.toBlob()
Polyfill.visibility() Polyfill.visibility()
toBlob: ->
HTMLCanvasElement::toBlob or= (cb) ->
data = atob @toDataURL()[22..]
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
ui8a[i] = data.charCodeAt i
cb new Blob [ui8a], type: 'image/png'
visibility: -> visibility: ->
# page visibility API # page visibility API
return unless 'webkitHidden' of document return unless 'webkitHidden' of document

View File

@ -173,8 +173,8 @@ ImageExpand =
{createSubEntry} = ImageExpand.menu {createSubEntry} = ImageExpand.menu
subEntries = [] subEntries = []
for key, conf of Config.imageExpansion for name, conf of Config.imageExpansion
subEntries.push createSubEntry key, conf subEntries.push createSubEntry name, conf[1]
$.event 'AddMenuEntry', $.event 'AddMenuEntry',
type: 'header' type: 'header'
@ -182,15 +182,14 @@ ImageExpand =
order: 105 order: 105
subEntries: subEntries subEntries: subEntries
createSubEntry: (type, config) -> createSubEntry: (name, desc) ->
label = $.el 'label', label = $.el 'label',
innerHTML: "<input type=checkbox name='#{type}'> #{type}" innerHTML: "<input type=checkbox name='#{name}'> #{name}"
title: desc
input = label.firstElementChild input = label.firstElementChild
if type in ['Fit width', 'Fit height'] if name in ['Fit width', 'Fit height']
$.on input, 'change', ImageExpand.cb.setFitness $.on input, 'change', ImageExpand.cb.setFitness
if config input.checked = Conf[name]
label.title = config[1]
input.checked = Conf[type]
$.event 'change', null, input $.event 'change', null, input
$.on input, 'change', $.cb.checked $.on input, 'change', $.cb.checked
el: label el: label

View File

@ -4,7 +4,6 @@ Linkify =
@regString = if Conf['Allow False Positives'] @regString = if Conf['Allow False Positives']
///( ///(
\b(
[-a-z]+:// [-a-z]+://
| |
[a-z]{3,}\.[-a-z0-9]+\.[a-z] [a-z]{3,}\.[-a-z0-9]+\.[a-z]
@ -16,11 +15,9 @@ Linkify =
[a-z]{3,}:[a-z0-9?] [a-z]{3,}:[a-z0-9?]
| |
[^\s@]+@[a-z0-9.-]+\.[a-z0-9] [^\s@]+@[a-z0-9.-]+\.[a-z0-9]
) )///i
[^\s'"]+
)///gi
else else
/(((magnet|mailto)\:|(www\.)|(news|(ht|f)tp(s?))\:\/\/){1}\S+)/gi /(((magnet|mailto)\:|(www\.)|(news|(ht|f)tp(s?))\:\/\/){1})/i
if Conf['Comment Expansion'] if Conf['Comment Expansion']
ExpandComment.callbacks.push @node ExpandComment.callbacks.push @node
@ -43,16 +40,44 @@ Linkify =
return return
test = /[^\s'"]+/g
space = /[\s'"]/
snapshot = $.X './/br|.//text()', @nodes.comment snapshot = $.X './/br|.//text()', @nodes.comment
i = 0 i = 0
while node = snapshot.snapshotItem i++ while node = snapshot.snapshotItem i++
continue if node.parentElement.nodeName is "A"
links = [] links = []
{data} = node
continue if node.parentElement.nodeName is "A" or not data
if Linkify.regString.test node.data while result = test.exec data
Linkify.regString.lastIndex = 0 {index} = result
Linkify.gatherLinks snapshot, @, node, links, i endNode = node
if (length = index + result[0].length) is data.length
while (saved = snapshot.snapshotItem i++)
break if saved.nodeName is 'BR'
endNode = saved
{length} = saved.data
if end = space.exec saved.data
length = end.index
i--
break
if length is endNode.data.length then test.lastIndex = 0
range = Linkify.makeRange node, endNode, index, length
if link = Linkify.regString.exec text = range.toString()
if lIndex = link.index
range.setStart node, lIndex + index
links.push [range, text]
break
else
if link = Linkify.regString.exec result[0]
range = Linkify.makeRange node, node, link.index, link.length
links.push [range, link]
for range in links.reverse() for range in links.reverse()
@nodes.links.push Linkify.makeLink range, @ @nodes.links.push Linkify.makeLink range, @
@ -68,66 +93,29 @@ Linkify =
return return
gatherLinks: (snapshot, post, node, links, i) -> makeRange: (startNode, endNode, startOffset, endOffset) ->
{data} = node
len = data.length
while (match = Linkify.regString.exec data)
{index} = match
link = match[0]
len2 = index + link.length
break if len is len2
range = document.createRange(); range = document.createRange();
range.setStart node, index range.setStart startNode, startOffset
range.setEnd node, len2 range.setEnd endNode, endOffset
links.push range
Linkify.regString.lastIndex = 0
if match
links.push Linkify.seek snapshot, post, node, links, match, i
return
seek: (snapshot, post, node, links, match, i) ->
link = match[0]
range = document.createRange()
range.setStart node, match.index
while (next = snapshot.snapshotItem i++) and next.nodeName isnt 'BR'
node = next
data = node.data
if result = /[\s'"]/.exec data
{index} = result
range.setEnd node, index
Linkify.regString.lastIndex = index
Linkify.gatherLinks snapshot, post, node, links, i
return range
if range.collapsed
range.setEnd node, node.data.length
range range
makeLink: (range) -> makeLink: ([range, text]) ->
link = range.toString() text
link = text =
if link.contains ':' if text.contains ':'
link text
else ( else (
if link.contains '@' if text.contains '@'
'mailto:' 'mailto:'
else else
'http://' 'http://'
) + link ) + text
a = $.el 'a', a = $.el 'a',
className: 'linkify' className: 'linkify'
rel: 'nofollow noreferrer' rel: 'nofollow noreferrer'
target: '_blank' target: '_blank'
href: link href: text
$.add a, range.extractContents() $.add a, range.extractContents()
range.insertNode a range.insertNode a
a a

View File

@ -19,8 +19,7 @@ ExpandComment =
cb: (e) -> cb: (e) ->
e.preventDefault() e.preventDefault()
post = Get.postFromNode @ ExpandComment.expand Get.postFromNode @
ExpandComment.expand post
expand: (post) -> expand: (post) ->
if post.nodes.longComment and !post.nodes.longComment.parentNode if post.nodes.longComment and !post.nodes.longComment.parentNode
@ -61,6 +60,11 @@ ExpandComment =
href = quote.getAttribute 'href' href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{post.board}/res/#{href}" # Fix pathnames quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
Build.capcodeReplies
boardID: post.board.ID
threadID: post.thread.ID
bq: clone
capcodeReplies: postObj.capcode_replies
post.nodes.shortComment = comment post.nodes.shortComment = comment
$.replace comment, clone $.replace comment, clone
post.nodes.comment = post.nodes.longComment = clone post.nodes.comment = post.nodes.longComment = clone

View File

@ -18,7 +18,6 @@ ThreadStats =
@postCountEl = $ '#post-count', sc @postCountEl = $ '#post-count', sc
@fileCountEl = $ '#file-count', sc @fileCountEl = $ '#file-count', sc
@pageCountEl = $ '#page-count', sc @pageCountEl = $ '#page-count', sc
@lastModified = '0'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Stats' name: 'Thread Stats'
@ -55,12 +54,10 @@ ThreadStats =
return return
setTimeout ThreadStats.fetchPage, 2 * $.MINUTE setTimeout ThreadStats.fetchPage, 2 * $.MINUTE
$.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad, $.ajax "//api.4chan.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad,
headers: 'If-Modified-Since': ThreadStats.lastModified whenModified: true
onThreadsLoad: -> onThreadsLoad: ->
return if !Conf["Page Count in Stats"] return unless Conf["Page Count in Stats"] and @status is 200
ThreadStats.lastModified = @getResponseHeader 'Last-Modified'
return if @status isnt 200
pages = JSON.parse @response pages = JSON.parse @response
for page in pages for page in pages
for thread in page.threads for thread in page.threads

View File

@ -64,7 +64,6 @@ ThreadUpdater =
ThreadUpdater.root = @OP.nodes.root.parentNode ThreadUpdater.root = @OP.nodes.root.parentNode
ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0] ThreadUpdater.lastPost = +ThreadUpdater.root.lastElementChild.id.match(/\d+/)[0]
ThreadUpdater.outdateCount = 0 ThreadUpdater.outdateCount = 0
ThreadUpdater.lastModified = '0'
ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval'] ThreadUpdater.cb.interval.call $.el 'input', value: Conf['Interval']
@ -137,8 +136,6 @@ ThreadUpdater =
when 200 when 200
g.DEAD = false g.DEAD = false
ThreadUpdater.parse JSON.parse(req.response).posts ThreadUpdater.parse JSON.parse(req.response).posts
ThreadUpdater.lastModified = req.getResponseHeader 'Last-Modified'
if Conf['Auto Update']
ThreadUpdater.set 'timer', ThreadUpdater.getInterval() ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
when 404 when 404
g.DEAD = true g.DEAD = true
@ -150,14 +147,8 @@ ThreadUpdater =
404: true 404: true
thread: ThreadUpdater.thread thread: ThreadUpdater.thread
else else
if Conf['Auto Update']
ThreadUpdater.outdateCount++ ThreadUpdater.outdateCount++
ThreadUpdater.set 'timer', ThreadUpdater.getInterval() ThreadUpdater.set 'timer', ThreadUpdater.getInterval()
###
Status Code 304: Not modified
By sending the `If-Modified-Since` header we get a proper status code, and no response.
This saves bandwidth for both the user and the servers and avoid unnecessary computation.
###
[text, klass] = if req.status is 304 [text, klass] = if req.status is 304
[null, null] [null, null]
else else
@ -219,7 +210,7 @@ ThreadUpdater =
ThreadUpdater.req.abort() ThreadUpdater.req.abort()
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json" url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load, ThreadUpdater.req = $.ajax url, onloadend: ThreadUpdater.cb.load,
headers: 'If-Modified-Since': ThreadUpdater.lastModified whenModified: true
updateThreadStatus: (title, OP) -> updateThreadStatus: (title, OP) ->
titleLC = title.toLowerCase() titleLC = title.toLowerCase()

View File

@ -2,98 +2,282 @@ ThreadWatcher =
init: -> init: ->
return unless Conf['Thread Watcher'] return unless Conf['Thread Watcher']
@dialog = UI.dialog 'watcher', 'top: 50px; left: 0px;',
'<div class=move>Thread Watcher</div>' @db = new DataBoard 'watchedThreads', @refresh, true
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """
<%= grunt.file.read('src/General/html/Monitoring/ThreadWatcher.html').replace(/>\s+</g, '><').trim() %>
"""
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
$.on d, 'QRPostSuccessful', @cb.post $.on d, 'QRPostSuccessful', @cb.post
$.sync 'WatchedThreads', @refresh $.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on d, '4chanXInitFinished', @ready
$.ready -> now = Date.now()
ThreadWatcher.refresh() if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
$.add d.body, ThreadWatcher.dialog @db.data.lastChecked = now
ThreadWatcher.fetchAllStatus()
@db.save()
# XXX tmp conversion from old to new format
$.get 'WatchedThreads', null, ({WatchedThreads}) ->
return unless WatchedThreads
for boardID, threads of ThreadWatcher.convert WatchedThreads
for threadID, data of threads
ThreadWatcher.db.set {boardID, threadID, val: data}
$.delete 'WatchedThreads'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Watcher' name: 'Thread Watcher'
cb: @node cb: @node
node: -> node: ->
favicon = $.el 'a', toggler = $.el 'img',
className: 'watch-thread-link' className: 'watcher-toggler'
href: 'javascript:;' $.on toggler, 'click', ThreadWatcher.cb.toggle
$.on favicon, 'click', ThreadWatcher.cb.toggle $.before $('input', @OP.nodes.post), toggler
$.before $('input', @OP.nodes.post), favicon
return if g.VIEW isnt 'thread' ready: ->
$.get 'AutoWatch', 0, (item) => $.off d, '4chanXInitFinished', ThreadWatcher.ready
return if item['AutoWatch'] isnt @ID return unless Main.isThisPageLegit()
ThreadWatcher.watch @ ThreadWatcher.refresh()
$.add d.body, ThreadWatcher.dialog
return unless Conf['Auto Watch']
$.get 'AutoWatch', 0, ({AutoWatch}) ->
return unless thread = g.BOARD.threads[AutoWatch]
ThreadWatcher.add thread
$.delete 'AutoWatch' $.delete 'AutoWatch'
refresh: (watched) ->
unless watched
$.get 'WatchedThreads', {}, (item) ->
ThreadWatcher.refresh item['WatchedThreads']
return
nodes = [$('.move', ThreadWatcher.dialog)]
for board of watched
for id, props of watched[board]
x = $.el 'a',
textContent: ''
className: 'close'
href: 'javascript:;'
$.on x, 'click', ThreadWatcher.cb.x
link = $.el 'a', props
link.title = link.textContent
div = $.el 'div'
$.add div, [x, $.tn(' '), link]
nodes.push div
$.rmAll ThreadWatcher.dialog
$.add ThreadWatcher.dialog, nodes
watched = watched[g.BOARD] or {}
for ID, thread of g.BOARD.threads
favicon = $ '.watch-thread-link', thread.OP.nodes.post
if ID of watched
$.addClass favicon, 'watched'
else
$.rmClass favicon, 'watched'
return
cb: cb:
openAll: ->
return if $.hasClass @, 'disabled'
for a in $$ 'a[title]', ThreadWatcher.list
$.open a.href
$.event 'CloseMenu'
checkThreads: ->
return if $.hasClass @, 'disabled'
ThreadWatcher.fetchAllStatus()
pruneDeads: ->
return if $.hasClass @, 'disabled'
for {boardID, threadID, data} in ThreadWatcher.getAll() when data.isDead
delete ThreadWatcher.db.data.boards[boardID][threadID]
ThreadWatcher.db.deleteIfEmpty {boardID}
ThreadWatcher.db.save()
ThreadWatcher.refresh()
$.event 'CloseMenu'
toggle: -> toggle: ->
ThreadWatcher.toggle Get.postFromNode(@).thread ThreadWatcher.toggle Get.postFromNode(@).thread
x: -> rm: ->
thread = @nextElementSibling.pathname.split '/' [boardID, threadID] = @parentNode.dataset.fullID.split '.'
ThreadWatcher.unwatch thread[1], thread[3] ThreadWatcher.rm boardID, +threadID
post: (e) -> post: (e) ->
{board, postID, threadID} = e.detail {board, postID, threadID} = e.detail
if postID is threadID if postID is threadID
if Conf['Auto Watch'] if Conf['Auto Watch']
$.set 'AutoWatch', threadID $.set 'AutoWatch', threadID
else if Conf['Auto Watch Reply'] else if Conf['Auto Watch Reply']
ThreadWatcher.watch board.threads[threadID] ThreadWatcher.add board.threads[threadID]
threadUpdate: (e) ->
{thread} = e.detail
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Update 404 status.
ThreadWatcher.add thread
fetchCount:
fetched: 0
fetching: 0
fetchAllStatus: ->
ThreadWatcher.status.textContent = '...'
for thread in ThreadWatcher.getAll()
ThreadWatcher.fetchStatus thread
return
fetchStatus: ({boardID, threadID, data}) ->
return if data.isDead
{fetchCount} = ThreadWatcher
fetchCount.fetching++
$.ajax "//api.4chan.org/#{boardID}/res/#{threadID}.json",
onloadend: ->
fetchCount.fetched++
if fetchCount.fetched is fetchCount.fetching
fetchCount.fetched = 0
fetchCount.fetching = 0
status = ''
else
status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%"
ThreadWatcher.status.textContent = status
return if @status isnt 404
if Conf['Auto Prune']
ThreadWatcher.rm boardID, threadID
else
data.isDead = true
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
,
type: 'head'
getAll: ->
all = []
for boardID, threads of ThreadWatcher.db.data.boards
if Conf['Current Board'] and boardID isnt g.BOARD.ID
continue
for threadID, data of threads
all.push {boardID, threadID, data}
all
makeLine: (boardID, threadID, data) ->
x = $.el 'a',
textContent: ''
href: 'javascript:;'
$.on x, 'click', ThreadWatcher.cb.rm
if data.isDead
href = Redirect.to 'thread', {boardID, threadID}
link = $.el 'a',
href: href or "/#{boardID}/res/#{threadID}"
textContent: data.excerpt
title: data.excerpt
div = $.el 'div'
fullID = "#{boardID}.#{threadID}"
div.dataset.fullID = fullID
$.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
$.addClass div, 'dead-thread' if data.isDead
$.add div, [x, $.tn(' '), link]
div
refresh: ->
nodes = []
for {boardID, threadID, data} in ThreadWatcher.getAll()
nodes.push ThreadWatcher.makeLine boardID, threadID, data
{list} = ThreadWatcher
$.rmAll list
$.add list, nodes
for threadID, thread of g.BOARD.threads
toggler = $ '.watcher-toggler', thread.OP.nodes.post
toggler.src = if ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
Favicon.default
else
Favicon.empty
for refresher in ThreadWatcher.menu.refreshers
refresher()
return
toggle: (thread) -> toggle: (thread) ->
unless $.hasClass $('.watch-thread-link', thread.OP.nodes.post), 'watched' boardID = thread.board.ID
ThreadWatcher.watch thread threadID = thread.ID
if ThreadWatcher.db.get {boardID, threadID}
ThreadWatcher.rm boardID, threadID
else else
ThreadWatcher.unwatch thread.board, thread.ID ThreadWatcher.add thread
add: (thread) ->
data = {}
boardID = thread.board.ID
threadID = thread.ID
if thread.isDead
if Conf['Auto Prune'] and ThreadWatcher.db.get {boardID, threadID}
ThreadWatcher.rm boardID, threadID
return
data.isDead = true
data.excerpt = Get.threadExcerpt thread
ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
rm: (boardID, threadID) ->
ThreadWatcher.db.delete {boardID, threadID}
ThreadWatcher.refresh()
unwatch: (board, threadID) -> convert: (oldFormat) ->
$.get 'WatchedThreads', {}, (item) -> newFormat = {}
watched = item['WatchedThreads'] for boardID, threads of oldFormat
delete watched[board][threadID] for threadID, data of threads
delete watched[board] unless Object.keys(watched[board]).length (newFormat[boardID] or= {})[threadID] = excerpt: data.textContent
ThreadWatcher.refresh watched newFormat
$.set 'WatchedThreads', watched
watch: (thread) -> menu:
$.get 'WatchedThreads', {}, (item) -> refreshers: []
watched = item['WatchedThreads'] init: ->
watched[thread.board] or= {} return if !Conf['Thread Watcher']
watched[thread.board][thread] = menu = new UI.Menu 'thread watcher'
href: "/#{thread.board}/res/#{thread}" $.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) ->
textContent: Get.threadExcerpt thread menu.toggle e, @, ThreadWatcher
ThreadWatcher.refresh watched @addHeaderMenuEntry()
$.set 'WatchedThreads', watched @addMenuEntries()
addHeaderMenuEntry: ->
return if g.VIEW isnt 'thread'
entryEl = $.el 'a',
href: 'javascript:;'
$.event 'AddMenuEntry',
type: 'header'
el: entryEl
order: 60
$.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
@refreshers.push ->
[addClass, rmClass, text] = if $ '.current', ThreadWatcher.list
['unwatch-thread', 'watch-thread', 'Unwatch thread']
else
['watch-thread', 'unwatch-thread', 'Watch thread']
$.addClass entryEl, addClass
$.rmClass entryEl, rmClass
entryEl.textContent = text
addMenuEntries: ->
entries = []
# `Open all` entry
entries.push
cb: ThreadWatcher.cb.openAll
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Open all threads'
refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
# `Check 404'd threads` entry
entries.push
cb: ThreadWatcher.cb.checkThreads
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Check 404\'d threads'
refresh: -> (if $('div:not(.dead-thread)', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Prune 404'd threads` entry
entries.push
cb: ThreadWatcher.cb.pruneDeads
entry:
type: 'thread watcher'
el: $.el 'a',
textContent: 'Prune 404\'d threads'
refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
# `Settings` entries:
subEntries = []
for name, conf of Config.threadWatcher
subEntries.push @createSubEntry name, conf[1]
entries.push
entry:
type: 'thread watcher'
el: $.el 'span',
textContent: 'Settings'
subEntries: subEntries
for {entry, cb, refresh} in entries
entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
$.on entry.el, 'click', cb if cb
@refreshers.push refresh.bind entry if refresh
$.event 'AddMenuEntry', entry
createSubEntry: (name, desc) ->
entry =
type: 'thread watcher'
el: $.el 'label',
innerHTML: "<input type=checkbox name='#{name}'> #{name}"
title: desc
input = entry.el.firstElementChild
input.checked = Conf[name]
$.on input, 'change', $.cb.checked
$.on input, 'change', ThreadWatcher.refresh if name is 'Current Board'
entry

View File

@ -665,21 +665,9 @@ QR =
cv.width = img.width = width cv.width = img.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height cv.getContext('2d').drawImage img, 0, 0, width, height
URL.revokeObjectURL fileURL URL.revokeObjectURL fileURL
applyBlob = (blob) => cv.toBlob (blob) =>
@URL = URL.createObjectURL blob @URL = URL.createObjectURL blob
@nodes.el.style.backgroundImage = "url(#{@URL})" @nodes.el.style.backgroundImage = "url(#{@URL})"
if cv.toBlob
cv.toBlob applyBlob
return
data = atob cv.toDataURL().split(',')[1]
# DataUrl to Binary code from Aeosynth's 4chan X repo
l = data.length
ui8a = new Uint8Array l
for i in [0...l]
ui8a[i] = data.charCodeAt i
applyBlob new Blob [ui8a], type: 'image/png'
fileURL = URL.createObjectURL @file fileURL = URL.createObjectURL @file
img.src = fileURL img.src = fileURL

View File

@ -16,7 +16,6 @@ QuoteYou =
cb: @node cb: @node
node: -> node: ->
# Stop there if it's a clone.
return if @isClone return if @isClone
if @info.yours if @info.yours