[ overboard / sfw / alt / cytube] [ leftypol / b / WRK / hobby / tech / edu / ga / ent / 777 / posad / i / a / R9K / dead ] [ meta ]

/tech/ - Technology

"Technology reveals the active relation of man to nature"
Name
Email
Subject
Comment
Flag
File
Embed
Password (For file deletion.)

Matrix   IRC Chat   Mumble   Telegram   Discord


File: 1612129656526.gif ( 2.28 MB , 224x240 , 1608608621350.gif )

 No.6724[View All]

This thread is only for feedback related to technical issues(bug reports, suggestions). Otherwise use
>>>/leftypol/30356
Public Repo: https://github.com/towards-a-new-leftypol/leftypol_lainchan
If you have any grievances you can make a PR.

Mobile Support: https://github.com/PietroCarrara/Clover/releases/latest
Thread For Mobile Feedback: >>>/tech/6316

Onion Link: http://wz6bnwwtwckltvkvji6vvgmjrfspr3lstz66rusvtczhsgvwdcixgbyd.onion
Cytube: https://tv.leftychan.net
Matrix: https://matrix.to/#/#Leftypol:matrix.org
Once you enter, consider joining the lefty technology room.

We are currently working on improvements to the site, subject to the need of the tech team to sleep and go to their day jobs. If you need more immediate feedback please join the matrix room[s] and ask around. Feel free to leave comments, concerns, and suggestions about the tech side of the site here and we will try to get to it as soon as possible

Archived thread:
>>>/leftypol_archive/903
151 posts and 41 image replies omitted. Click reply to view.
>>

 No.10247

When the reply count drops and a recent version of the thread is available, the ids can be diffed:
>> Array.from (document.querySelectorAll ("div.post.reply")).map (e => e.getAttribute ('id').replace (/^reply_/, '')).join (' ')
"6727 ..."

$ diffids () { diff <(echo "$1" | tr ' ' '\n') <(echo "$2" | tr ' ' '\n'); }
$ diffids "6727 ..." "6727 ..."
140d139
< 9986
151a151
> 10226

In this case the logs https://leftypol.org/log.php?board=tech suggest 9986 was probably in "Deleted all posts by IP address".
>>

 No.10389

File: 1626955991857.jpg ( 1.7 MB , 1493x2500 , 04c5e4db7974bb4ced191f0085….jpg )

Duplicate scripts:
>> console.log (Object.entries (Array.from (document.getElementsByTagName ("script")).filter (e => e.hasAttribute ("src")).map (e => e.getAttribute ("src")).reduce ((acc, name, idx) => {
  if (name in acc) {
    acc [name].push (idx)
  } else {
    acc [name] = [idx]
  }
  return acc
}, {})).filter (([name, indices]) => indices.length > 1).sort ((a, b) => a [1] [0] - b [1] [0]).map (([name, indices]) => name + ' ' + indices.toString ()).join ('\n'))

/js/jquery.min.js 1,3
/js/inline-expanding.js 2,5

The source of the problem is this duplication:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/5faa622303f7a3d9568a7c89728c3095a664b5c7/inc/config.php#L1039
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/5faa622303f7a3d9568a7c89728c3095a664b5c7/inc/instance-config.php#L375

While two jquery.min only slow page loads, the two inline-expanding cause double registration of listeners and duplicate options gui spans.
>>

 No.10393

https://github.com/towards-a-new-leftypol/leftypol_lainchan/issues/326
> Multiple features in the Options dialog appear to be broken #326
> The first three options appear to have no effect and do not store a value when activated.
> Show relative time

After the page has loaded with scripts enabled the inspector can be used to verify that '#show-relative-time>input' has no listener. The listener is added in:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/5faa622303f7a3d9568a7c89728c3095a664b5c7/js/local-time.js#L91
> $('#show-relative-time>input').on('change', function() {

Setting a breakpoint on that line shows that $('#show-relative-time>input') is empty, so the listener is added to nothing. One way to fix this is to retrieve the input from the options tab instead of the document:
< Options.get_tab ('general').content.find ('#show-relative-time>input').on('change', function() {

I don't know whether this is the only fix needed for js/local-time.js, that needs to be retested after the listener is in place. A similar consideration applies to every script that has been moved from onready to $(document).ready and attempts to retrieve options gui elements after an Options.extend_tab or equivalent call.

https://github.com/towards-a-new-leftypol/leftypol_lainchan/commit/88f6088a429fb73d12805377b43c6b567d03a5db
> fix Relative Time and Image Throttler
> Author: marktaiwan <[email protected]>
> Date: Fri Jan 23 00:03:37 2015 +0800
> marktaiwan authored and czaks committed May 5, 2016
>>

 No.10407

The reason $('#show-relative-time>input') is empty >>10393 is that js/options.js adds its gui to the document at $(document).ready
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/5faa622303f7a3d9568a7c89728c3095a664b5c7/js/options.js#L105
but it's placed after some scripts that add options in $config['additional_javascript']
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/5faa622303f7a3d9568a7c89728c3095a664b5c7/inc/instance-config.php#L393

The only reason js/local-time.js and its friends even find a general tab is that js/options/general.js adds it at the time of head:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/5faa622303f7a3d9568a7c89728c3095a664b5c7/js/options/general.js#L15
>>

 No.10414

https://github.com/towards-a-new-leftypol/leftypol_lainchan/issues/326
> Number of simultaneous image downloads (0 to disable):

Besides being run >>10389 twice, js/inline-expanding.js has the same no listener issue >>10393 because $('#inline-expand-max input') is empty.
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/5faa622303f7a3d9568a7c89728c3095a664b5c7/js/inline-expanding.js#L196

One fix is:
< Options.get_tab ('general').content.find ('#inline-expand-max input')
>>

 No.10420

File: 1627085901011.jpg ( 589.83 KB , 720x1080 , 9a51c2ff7b9880dbcfd1af649e….jpg )

When testing with scripts enabled, cloudflare's injected malware can be avoided by blocking leftypol URLs that contain "/cdn-cgi/".
>>

 No.10421

Thanks for all your help with this, I'm going to start making pull requests over the next few days.

>>10109
Would you consider this an ideal solution for us to implement, or a simple and effective bandaid for our short-sighted $(document).ready changes? Is it sane for us to be in this situation where it is called multiple times?
>>

 No.10429

>>10421
That is certainly only a quickfix, but a very cheap one because after the first call it will immediately return on a boolean test. A more pleasing long-term solution would be for the rememberStuff chain not to trigger 'cite' that early and instead cause a delayed 'cite' to fire at the time of $(document).ready, but this way great care must be taken with the order in which things run.

Also, I see that PR #330 has been approved and its commit merged but the duplicate scripts are still being served as I'm writing this.
>>

 No.10430

>>10429
>>10429
We're testing bb. One sec.
>>

 No.10432

File: 1627126740264.png ( 9.16 KB , 454x122 , ClipboardImage.png )

>>10429
>Also, I see that PR #330 has been approved and its commit merged but the duplicate scripts are still being served as I'm writing this.
Yep, merge happens before final testing, before they go live.
I can confirm it removes the duplication in the Option form.

>That is certainly only a quickfix [snip]

I'm happy with adding that, it's safe and effective. I might aim for the better solution once we've finished fixing the fallout from our speed-improvement changes and downstreaming the years of vichan changes that lainchan ignored.
>>

 No.10441

File: 1627172785657.jpg ( 566.96 KB , 1605x2500 , 0f92b8330c700a6981361b0b7d….jpg )

https://github.com/towards-a-new-leftypol/leftypol_lainchan/issues/318
> Syncronize spoiler button state on quick reply and main post form #318

While looking into this the synchronization section of js/quick-reply.js turns out to be quite peculiar.
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/dcf92dfef5f5ab900dc879d7dc4c3f04fab0aed8/js/quick-reply.js#L296
Every time the quick reply form is manually closed and reopened, new listeners are installed on the forms and the window, but the old listeners are never cleared in the .close-btn click handler.
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/dcf92dfef5f5ab900dc879d7dc4c3f04fab0aed8/js/quick-reply.js#L348

The effect can be observed on elements of the top form, such as the textarea, for which the inspector shows an ever increasing number of listeners. These keep the closed quick reply forms reachable and therefore uncollectable, leaking memory. They also transfer the top form text to each closed quick reply form, and after a sufficient number typing becomes sluggish. The .close-btn click handler should deregister all listeners that only served the closed quick reply form.

Keeping in mind that the current way of doing things leaks memory with abandon, the spoiler checkboxes can be synced by extending the synchronization section:
const cbsync = (form1, form2, name) => {
  const sel = 'input[type="checkbox"][name="' + name + '"]'
  const cb1 = form1.find (sel)
  const cb2 = form2.find (sel)
  cb1.on ('change', () => {
    cb2.prop ('checked', cb1.prop ('checked'))
  })
  cb2.on ('change', () => {
    cb1.prop ('checked', cb2.prop ('checked'))
  })
}

cbsync ($origPostForm, $postForm, 'spoiler')
>>

 No.10444

>>10441
>Every time the quick reply form is manually closed and reopened, new listeners are installed on the forms and the window, but the old listeners are never cleared in the .close-btn click handler.
It looks adding these two .off() lines fixes it correctly:
		$postForm.find('th .close-btn').click(function() {
			// Remove origPostForm listeners
			$origPostForm.find('textarea[name="body"]').off('change input propertychange focus');
			$origPostForm.find('input[type="text"],select').off('change input propertychange');
>>

 No.10455

>>10444
That removes every listener for those events from those elements, not just those added by js/quick-reply.js. Currently there are no others, but in the future that might remove some other script's listeners. To avoid this the events can be namespaced with something like .quickreply in both on and off calls.
https://api.jquery.com/on/#event-names

> > new listeners are installed on the forms and the window >>10441

This scroll handler on the window
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/dcf92dfef5f5ab900dc879d7dc4c3f04fab0aed8/js/quick-reply.js#L363
needs to be deregistered as well on .close-btn click because it keeps the old $postForm reachable and therefore uncollectable.

This stylesheet handler on the window
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/dcf92dfef5f5ab900dc879d7dc4c3f04fab0aed8/js/quick-reply.js#L375
needs the same treatment. Alternatively it could be added exactly once >>10109, since it doesn't depend on the $postForm, but in that case it must be moved out of show_quick_reply.

This 'quick-reply' handler on the window
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/dcf92dfef5f5ab900dc879d7dc4c3f04fab0aed8/js/quick-reply.js#L414
is added anew on every floating_link call, which occurs on every .close-btn click. It needs to be added at most once. >>10109
>>

 No.10478

Here's a cleanup_jquery_listeners utility function:
const cleanup_jquery_listeners = cleanupspec => {
  // cleanupspec is a list of triples of
  // [jquery sets, selector or null, event string]
  // example: [
  //   [[$origPostForm, $postForm], 'input[type="text"],select', '.quickreply'],
  //   [[$(window)], null, 'scroll.quickreply']
  // ]
  for (const [sets, sel, events] of cleanupspec) {
    for (let oneset of sets) {
      if (sel != null) {
        oneset = oneset.find (sel)
      }
      oneset.off (events)
    }
  }
}

Then in .close-btn click:
const spec = [
  [[$origPostForm, $postForm], 'textarea[name="body"]', '.quickreply'],
  [[$origPostForm, $postForm], 'input[type="text"],select', '.quickreply'],
  [[$(window)], null, 'scroll.quickreply'],
  [[$postForm], 'th .close-btn', 'click.quickreply'],
]

cleanup_jquery_listeners (spec)

The $postForm is cleaned up for completeness. When spoilers are synced >>10441 they can be added to the spec list in the obvious way. All on calls need .quickreply on their events. The click, focus and scroll calls become .on('click.quickreply', …) and equivalent. The exceptions are the last two listeners of >>10455 which can be dealt with using callonce_factory >>10109.
>>

 No.10486

>>10109
Should the callonce factory be placed in main.js?
>>

 No.10487

For the last two listeners of >>10455, inside the outermost function of js/quick-reply.js:
const stylesheet_handler_once = callonce_factory (() => {
  $(window).on('stylesheet', function() {
    do_css();
    if ($('link#stylesheet').attr('href')) {
      $('link#stylesheet')[0].onload = do_css;
    }
  });
})

const qr_handler_once = callonce_factory (() => {
  $(window).on('quick-reply', function() {
    $('.quick-reply-btn').remove();
  });
})

Then replace
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/dcf92dfef5f5ab900dc879d7dc4c3f04fab0aed8/js/quick-reply.js#L375
with
< stylesheet_handler_once ()
and replace
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/dcf92dfef5f5ab900dc879d7dc4c3f04fab0aed8/js/quick-reply.js#L414
with
< qr_handler_once ()

>>10486
Either templates/main.js or a new js/functools.js, entirely at the techs' option.
>>

 No.10493

File: 1627341198437.jpg ( 5.79 MB , 3002x4988 , ebe6d32cbc673dba0151ea29f3….jpg )

Temporary fix for spoiler sync #318 in Options -> User JS until the backend is fixed >>10441.
(() => {
  const cbsync = (form1, form2, name) => {
    const sel = 'input[type="checkbox"][name="' + name + '"]'
    const cb1 = form1.find (sel)
    const cb2 = form2.find (sel)
    cb1.on ('change', () => {
      cb2.prop ('checked', cb1.prop ('checked'))
    })
    cb2.on ('change', () => {
      cb1.prop ('checked', cb2.prop ('checked'))
    })
  }
  const spoilersync = () => {
    const ftop   = $('form[name="post"]:first')
    const fquick = $('#quick-reply')
    if ((ftop.length != 1) || (fquick.length != 1)) { return; }
    if (fquick.attr ('data-userjs-spoilersync') == 'spoilersync') { return; }
    cbsync (ftop, fquick, 'spoiler')
    fquick.attr ('data-userjs-spoilersync', 'spoilersync')
  }
  spoilersync ()
  $(window).on('quick-reply', spoilersync)
})()
>>

 No.10494

Temporary fix for >>10079 in Options -> User JS until the backend is fixed >>10104 >>10109.
$('#quick-reply input[name="file"]').remove ()
>>

 No.10495

File: 1627344936998.txt ( 7.71 KB , userjs.txt )

Updated Options -> User JS >>10101 with the current fixes.
① catalog links in div.boardlist >>9483
② thread stats and Unique IPs >>6744
③ individual post hiding >>6753
④ batch loop/once WebM setting >>6819
⑤ top/bottom navlinks in the top bar >>6835
⑥ generic file thumbs in catalog >>6843
⑦ catalog link above OP >>6916
⑧ quick reply spoiler sync >>10493
⑨ quick reply before init_file_selector >>10494
>>

 No.10656

links like https://leftychan.net/leftypol don't work anymore
>>

 No.10664

>extremely basic pull requests in queue for weeks
>one of three devs remaining
._.
>>

 No.10698

>>10664
Zer0 is aware of the backlog of Pr's
>>

 No.10728

https://github.com/towards-a-new-leftypol/leftypol_lainchan/issues/353
>Embeding can only be done at the top of the page. #353
see >>7392
>>

 No.10767

File: 1630180285468.jpg ( 797.3 KB , 720x1080 , 6ae8c364d9d95333de9fc023e0….jpg )

autism scoredb links, preferring thumbnails to full images.
((getthumb, getfull, getdest, getthumb2, getfull2, realthumb, formatok, speclist, showft) => Array.from (document.querySelectorAll ("div.files > div.file")).map (e => [getthumb (e), getfull (e), getdest (e)]).map (([ethumb, efull, edest]) => [ethumb ? getthumb2 (ethumb) : null, efull ? getfull2 (efull) : null, edest]).map (([thumb, full, edest]) => [thumb, full, edest, thumb && realthumb (thumb) && formatok (thumb), full && formatok (full)]).filter (([thumb, full, edest, thumbok, fullok]) => thumbok || fullok).forEach (([thumb, full, edest, thumbok, fullok]) => {
  const url  = "https://leftypol.org" + (thumbok ? thumb : full)
  const span = document.createElement ("span")
  span.setAttribute ("class", "iqdb")
  span.innerHTML = ' ' + (showft ? ((thumbok ? 'T' : 'F') + ':') : "") + speclist.map (([label, urlfun]) => '<a href="' + urlfun (url) + '" target="_blank">' + label + '</a>').join (' ')
  edest.appendChild (span)
})) (
  e => e.querySelector ("img.post-image"),
  e => e.querySelector ('p.fileinfo a[target="_blank"][href*="/src/"]'),
  e => e.querySelector ("span.details"),
  e => e.getAttribute ("src"),
  e => e.getAttribute ("href"),
  s => /^\/[^\/]+\/thumb\//.test (s),
  s => /[.](jpe?g|png|gif)$/i.test (s),
  [
    ["iqdb", s => "https://iqdb.org/?url=" + s],
 // ["nao",  s => "https://saucenao.com/search.php?db=999&dbmaski=32768&url=" + s]
  ],
  false
)
>>

 No.10768

>>

 No.10769

Forgot to updste >>10767 the site URL, my mistake.
((getthumb, getfull, getdest, getthumb2, getfull2, realthumb, formatok, speclist, showft) => Array.from (document.querySelectorAll ("div.files > div.file")).map (e => [getthumb (e), getfull (e), getdest (e)]).map (([ethumb, efull, edest]) => [ethumb ? getthumb2 (ethumb) : null, efull ? getfull2 (efull) : null, edest]).map (([thumb, full, edest]) => [thumb, full, edest, thumb && realthumb (thumb) && formatok (thumb), full && formatok (full)]).filter (([thumb, full, edest, thumbok, fullok]) => thumbok || fullok).forEach (([thumb, full, edest, thumbok, fullok]) => {
  const url  = "https://leftychan.net" + (thumbok ? thumb : full)
  const span = document.createElement ("span")
  span.setAttribute ("class", "iqdb")
  span.innerHTML = ' ' + (showft ? ((thumbok ? 'T' : 'F') + ':') : "") + speclist.map (([label, urlfun]) => '<a href="' + urlfun (url) + '" target="_blank">' + label + '</a>').join (' ')
  edest.appendChild (span)
})) (
  e => e.querySelector ("img.post-image"),
  e => e.querySelector ('p.fileinfo a[target="_blank"][href*="/src/"]'),
  e => e.querySelector ("span.details"),
  e => e.getAttribute ("src"),
  e => e.getAttribute ("href"),
  s => /^\/[^\/]+\/thumb\//.test (s),
  s => /[.](jpe?g|png|gif)$/i.test (s),
  [
    ["iqdb", s => "https://iqdb.org/?url=" + s],
 // ["nao",  s => "https://saucenao.com/search.php?db=999&dbmaski=32768&url=" + s]
  ],
  false
)
>>

 No.10775

Multiupload with the site's scripts disabled but JS enabled in the dev tools console:
(count => {
  const dest = document.querySelector ("tr#upload > td.upload-area")
  if (dest == null) { return; }
  const have = dest.querySelectorAll ('input[type="file"]').length
  if (have >= count) { return; }
  const make = n => '<br class="file_separator"/><input type="file" name="file' + n + '" id="upload_file' + n + '">'
  const add  = []
  for (let k = 2; k <= count; k++) {
    add.push (make (k))
  }
  dest.innerHTML += add.join ("")
}) (5)
>>

 No.10787

Some harmless fun with the original filenames. If the filename looks like a timestamp from a board download >>6724 the UTC date is provided. If it looks like an md5 hash >>6841 an md5 search link is provided to r34. Other matchers might be added later. This is merely a demo, not anything serious.
((getfilesbody, getorig, decorate, providers) => getfilesbody ().forEach (([files, body]) => {
  const infolist = Array.from (files.querySelectorAll ("div.file")).map ((f, index) => [getorig (f), index + 1]).filter (p => p [0] != null).map (([name, index]) => providers.map (p => p (name, index)).filter (s => s != null)).flat (1)
  if (infolist.length > 0) {
    decorate (body, infolist)
  }
})) (
  () => Array.from (document.querySelectorAll ("div.thread > div.files")).map (f => [f, f.parentNode.querySelector ("div.post.op > div.body")]).concat (Array.from (document.querySelectorAll ("div.post.reply")).map (r => [r.querySelector ("div.files"), r.querySelector ("div.body")]).filter (p => p [0] != null)),
  f => {
    const s = f.querySelector ('span.details > span.postfilename')
    return s == null ? null : (s.hasAttribute ("title") ? s.getAttribute ("title") : s.innerText)
  },
  (body, infolist) => {
    const p = document.createElement ("p")
    p.setAttribute ("class", "miscfilesbodyinfo")
    p.innerHTML = infolist.join ("<br/>")
    if (body.firstChild) {
       body.insertBefore (p, body.firstChild)
       const hr = document.createElement ("hr")
       hr.setAttribute ("style", "clear: none;")
       body.insertBefore (hr, p.nextSibling)
    } else {
       body.appendChild (p)
    }
  },
  [
 // (name, index) => "file " + index + " name " + name,
    (name, index) => {
      const m = name.match (/^([0-9a-fA-F]{32})[.]/)
      if (m == null) { return null; }
      const hash = m [1]
      return "file " + index + " md5 " + hash + ' search <a href="https://rule34.xxx/index.php?page=post&s=list&tags=md5%3a' + hash + '" target="_blank">r34</a>'
    },
    (name, index) => {
      const m = name.match (/^([0-9]{13})[.]/)
      if (m == null) { return null; }
      const time = parseInt (m [1], 10)
      return "file " + index + " timestamp " + time + " date " + new Date (time).toUTCString ()
    }
  ]
)
>>

 No.10799

To move towards per-file spoilers >>7390 >>7435 exemptions for spoiler1 through spoiler9 should be added to $config['spam']['valid_inputs'], to allow per-file booleans to pass through the post form. Their number is simply for consistency with the existing file_url9.
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/d5bbcc205d4e357de180fa19d8d20e688d6153cb/inc/config.php#L279

The names of the file fields of the post form are file, file2, file3 and so on.
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/d5bbcc205d4e357de180fa19d8d20e688d6153cb/js/file-selector.js#L73

Those names can be used to select corresponding booleans sent by checkboxes, and are available as $_FILES keys when processing uploads. The $key can be temporarily stored in $file, for example as $file['formparametername'].
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/d5bbcc205d4e357de180fa19d8d20e688d6153cb/post.php#L793
https://www.php.net/manual/en/features.file-upload.post-method.php

The spoiler test
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/d5bbcc205d4e357de180fa19d8d20e688d6153cb/post.php#L1064
can then be modified to check per-file booleans:
< if ($config['spoiler_images'] && isset($_POST[str_replace('file', 'spoiler', $file['formparametername'])])) {
>>

 No.10800

File: 1630800257046.png ( 33.66 KB , 447x420 , image.png )

For per-file spoilers for the non-JS case the way to have multiple file controls can be found in multi-image.js:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/d5bbcc205d4e357de180fa19d8d20e688d6153cb/js/multi-image.js#L21
> var new_file = '<br class="file_separator"/><input type="file" name="file'+(images_len+1)+'" id="upload_file'+(images_len+1)+'">';

The original spoiler checkbox is in post_form.html:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/d5bbcc205d4e357de180fa19d8d20e688d6153cb/templates/post_form.html#L51
> {% if config.spoiler_images %}<div id="spoilercontainer"> <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label></div>{% endif %}

as is the file input:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/d5bbcc205d4e357de180fa19d8d20e688d6153cb/templates/post_form.html#L149
> <input type="file" name="file" id="upload_file">

The div#spoilercontainer can be dropped and the file input replaced with:
{% for counter in 1..config.max_images %}
    {% if counter > 1 %}<br class="file_separator"/>{% endif %}
    {% set countersuffix = counter == 1 ? '' : counter %}
    {% if config.spoiler_images %}<span class="spoilercontainer"><input id="spoiler{{ countersuffix }}" name="spoiler{{ countersuffix }}" type="checkbox"><label for="spoiler{{ countersuffix }}">S{{ counter }}</label></span>{% endif %}
    <input type="file" name="file{{ countersuffix }}" id="upload_file{{ countersuffix }}">
{% endfor %}


A sample result is attached. With >>10799 this will enable per-file spoilers for the non-JS case. I'll leave the necessary frontend changes for the JS case to those who regularly browse with remote code execution enabled.
>>

 No.10803

Upgrade of >>10787 to unify handling of the two sites. It'll work on some other vichan-based boards as well if their URLs are added.
(regexlisttest => ((sitespec, providers) => {
  const site = sitespec.find (spec => spec ["test"] ())
  if (!site) { return; }
  const postfiles = site ["postfiles"]
  const original  = site ["original" ]
  const decorate  = site ["decorate" ]
  site ["filesbody"] ().forEach (([files, body]) => {
    const infolist = postfiles (files).map ((f, index) => [original (f), index + 1]).filter (p => p [0] != null).map (([name, index]) => providers.map (p => p (name, index)).filter (s => s != null)).flat (1)
    if (infolist.length > 0) {
      decorate (body, infolist)
    }
  })
}) (
  [
    {
      test:      regexlisttest ([
     // /^https?:\/\//,
        /^https?:\/\/leftychan\.net\//,
        /^https?:\/\/leftypol\.org\//,
      ]),
      filesbody: () => Array.from (document.querySelectorAll ("div.thread > div.files")).map (f => [f, f.parentNode.querySelector ("div.post.op > div.body")]).concat (Array.from (document.querySelectorAll ("div.post.reply")).map (r => [r.querySelector ("div.files"), r.querySelector ("div.body")]).filter (p => p [0] != null)),
      postfiles: files => Array.from (files.querySelectorAll ("div.file")),
      original:  f => {
        let e = f.querySelector ('span.postfilename')
        if (e != null) { return e.hasAttribute ("title") ? e.getAttribute ("title") : e.innerText; }
        e = f.querySelector ('span.details > a[download][title*="original filename"]')
        if (e != null) { return e.getAttribute ("download"); }
        return null
      },
      decorate:  (body, infolist) => {
        const p = document.createElement ("p")
        p.setAttribute ("class", "miscfilesbodyinfo")
        p.innerHTML = infolist.join ("<br/>")
        if (body.firstChild) {
          body.insertBefore (p, body.firstChild)
          const hr = document.createElement ("hr")
          hr.setAttribute ("style", "clear: none;")
          body.insertBefore (hr, p.nextSibling)
        } else {
          body.appendChild (p)
        }
      }
    }
  ], [
 // (name, index) => "file " + index + " name " + name,
    ((link, speclist) => (name, index) => {
      const m = name.match (/^([0-9a-fA-F]{32})[.]/)
      if (m == null) { return null; }
      const hash = m [1]
      return "file " + index + " md5 " + hash + ' search ' + speclist.map (e => link (e ["href"] (hash), e ["name"])).join (" ")
    }) (
      (h, t) => '<a href="' + h + '" target="_blank">' + t + '</a>',
      [
        {
          name: "r3",
          href: s => "https://rule34.xxx/index.php?page=post&s=list&tags=md5%3a" + s,
        }
      ]
    ),
    (name, index) => {
      const m = name.match (/^([0-9]{13})[-.]/)
      if (m == null) { return null; }
      const time = parseInt (m [1], 10)
      return "file " + index + " timestamp " + time + " date " + new Date (time).toUTCString ()
    }
  ]
)) (
  regexlist => () => (url => regexlist.some (rx => rx.test (url))) (document.location.href)
)
>>

 No.10864

A backend version of >>6835.

The thread view's top/bottom targets are #top and #bottom:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/templates/thread.html#L61
> <span class="threadlink"><a href="#bottom" style="padding-left: 10px"> {% trans %}Go to bottom{% endtrans %}</a> ]</span>
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/templates/thread.html#L73
> <a id="thread-top" href="#top">[{% trans %}Go to top{% endtrans %}]</a>

The corresponding anchors are placed just after the boardlist and just before the end of the body:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/templates/thread.html#L32
> <a name="top"></a>
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/templates/thread.html#L117
> <a href="#" id="bottom"></a>

The boardlist assembly:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/inc/display.php#L108
> 'top' => '<div class="boardlist">' . $body . '</div>' . $top,
This could host a span with two links with titles, some classes and some default styling such as floating in style.css. Inclusion could be controlled by a new optional boolean passed to createBoardlist that defaults to false.
// after trim
if ($topbottomlinks) {
    $body .= ' <span class="topbottomlinks"><a href="#top" title="' . _('Go to top') . '">&#x25B2;</a> <a href="#bottom" title="' . _('Go to bottom') . '">&#x25BC;</a></span>';
}

// in style.css
.topbottomlinks {
    ... style to taste ...
}
.topbottomlinks a {
    ... style to taste ...
}


The createBoardlist invocations that would need the new boolean set to true:
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/inc/functions.php#L1405
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/inc/functions.php#L2316
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/inc/functions.php#L2419
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/templates/themes/catalog/theme.php#L460
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/bd737669957948b72deb1ef974f8132a7cd99b13/templates/themes/overboards/theme.php#L216

Plenty of views have boardlists, most of them lacking top/bottom anchors:
$ grep -e '{{ *boardlist\.top *}}' -r .
Out of those the ones that matter and need top/bottom anchors are:
templates/index.html
templates/themes/catalog/catalog.html

The unicode arrows used above:
▲=&#x25B2;
▼=&#x25BC;
>>

 No.10877

File: 1633954221730.png ( 113.83 KB , 1059x840 , lcimage.png )

Here's a graph of relative PPD for the last 30 days for thread views.
((pagetest, install, getdata, draw) => {
  const back   = 'rgba(  0,   0,  60, 1)'
  const bars   = 'rgba(  0,   0, 255, 1)'
  const grid   = 'rgba(128, 128, 128, 1)'
  const count  = 30, step = 15
  const width  = count * step
  const height = 100

  if (!pagetest ()) { return; }
  context = install ({
    width:  width,
    height: height,
  })
  if (!context.ok) { return; }

  context.back   = back
  context.bars   = bars
  context.grid   = grid
  context.count  = count
  context.step   = step
  context.width  = width
  context.height = height
  context.data   = getdata (count)
  draw (context)
}) (
  () => (document.getElementById ('uniqueip') != null) && (document.querySelectorAll ('div.post.op').length == 1),
  config => {
    const holder = document.createElement ("div")
    holder.innerHTML = '<div style="text-align: center;"><canvas id="canvasid" width="' + config.width + '" height="' + config.height + '" style="border: 1px solid; z-index: 100; position: relative;">canvas</canvas></div>'
    const target = document.getElementById ('thread-interactions')
    if (target == null) { return { ok: false }; }
    target.parentNode.insertBefore (holder, target)
    const canvas = document.getElementById ('canvasid')

    if (canvas.getContext) {
      const ctx = canvas.getContext ('2d')
      return {
        ok:      true,
        holder:  holder,
        canvas:  canvas,
        context: ctx,
      }
    } else {
      holder.remove ()
      return { ok: false }
    }
  },
  count => {
    const all = Array.from (document.querySelectorAll ('p.intro time')).map (e => e.innerText.match (/^\d{4}-\d{2}-\d{2}/)).filter (m => m != null).reduce ((acc, m) => {
      const key = m [0]
      if (key in acc) {
        acc [key] += 1
      } else {
        acc [key]  = 1
      }
      return acc
    }, {})
    const now = new Date ()
    const key = k => {
      const when = new Date (now)
      when.setUTCDate (now.getUTCDate () - k)
      return when.getUTCFullYear ().toString ().padStart (4, '0') + '-' + (when.getUTCMonth () + 1).toString ().padStart (2, '0') + '-' + when.getUTCDate ().toString ().padStart (2, '0')
    }
    const ret = Array.from ({length: count}, (x, k) => all [key (k)] ?? 0)
    return ret
  },
  context => {
    const ctx   = context.context
    const data  = context.data
    const max   = data.reduce ((a, b) => Math.max (a, b), 0) || 1
    const w     = context.width
    const h     = context.height
    const count = context.count
    const step  = context.step

    ctx.fillStyle = context.back
    ctx.fillRect (0, 0, w, h)

    ctx.strokeStyle = context.grid
    for (let k = 0; k < count; k++) {
      ctx.beginPath ()
      ctx.moveTo (step * k, 0)
      ctx.lineTo (step * k, h)
      ctx.stroke ()
    }

    ctx.fillStyle = context.bars
    for (let k = 0; k < count; k++) {
      const y = data [k] * h / max
      ctx.fillRect (w - step - k * step, h - y, step, y)
    }
  }
)

Just a demo.
>>

 No.10880

File: 1634121485828.png ( 326.99 KB , 3177x800 , lcimage.png )

Here's a plot for catalog views that shows the relative age of threads, growing to the right, the relative reply counts, growing to the top, and the relative bump freshness, active in red, dormant in blue. The sample images are for /tech/, /leftypol/ and /b/.
(tools => ((install, sitespec) => {
  const site   = sitespec.find (spec => spec.pagetest ())
  if (!site) { return; }
  const config = site.getconfig ()
  if (!config.ok) { return; }
  install (config)
  if (!config.ok) { return; }
  site.getdata (config)
  if (!config.ok) {
    config.holder.remove ()
    return
  }
  site.draw (config)
}) (
  config => {
    // > width height insertmode inserttarget
    // < holder canvas context
    const modes = {
      after:  (holder, target) => { target.parentNode.insertBefore (holder, target.nextSibling); },
      before: (holder, target) => { target.parentNode.insertBefore (holder, target); },
      first:  (holder, target) => { target.insertBefore (holder, target.firstChild); },
      last:   (holder, target) => { target.appendChild (holder); },
    }

    const holder = document.createElement ("div")
    holder.innerHTML = '<div style="text-align: center;"><canvas id="canvasid" width="' + config.width + '" height="' + config.height + '" style="border: 1px solid; z-index: 100; position: relative;">canvas</canvas></div>'
    modes [config.insertmode] (holder, config.inserttarget)
    const canvas = document.getElementById ('canvasid')

    if (canvas.getContext) {
      config.holder  = holder
      config.canvas  = canvas
      config.context = canvas.getContext ('2d')
    } else {
      holder.remove ()
      config.ok = false
    }
  }, [{
    pagetest:  () => /^https?:\/\/(leftypol\.org|leftychan\.net)\//.test (document.location.href) && (document.getElementById ('uniqueip') != null) && (document.querySelectorAll ('div.post.op').length == 1),
    getconfig: () => {
      const target = document.getElementById ('thread-interactions')
      if (target == null) { return { ok: false }; }
      const count  = 60, step = 10

      return {
        ok:     true,
        width:  count * step,
        height: 100,
        count:  count,
        step:   step,
        back:   'rgba(  0,   0,  60, 1)',
        bars:   'rgba(  0,   0, 255, 1)',
        grid:   'rgba(128, 128, 128, 1)',
        insertmode:   'before',
        inserttarget: target,
      }
    },
    getdata: config => {
      const dates = Array.from (document.querySelectorAll ('p.intro time')).map (e => e.innerText.match (/^\d{4}-\d{2}-\d{2}/)).filter (m => m != null).map (m => m [0])
      tools.getdatadatecount (config, dates)
    },
    draw: tools.drawrelative
  }, {
    pagetest:  () => /^https?:\/\/(leftypol\.org|leftychan\.net)\//.test (document.location.href) && (document.getElementById ('Grid') != null),
    getconfig: () => {
      const target = document.getElementById ('Grid')
      if (target == null) { return { ok: false }; }

      return {
        ok:     true,
        width:  400,
        height: 400,
        radius: 8,
        insertmode:   'after',
        inserttarget: target,
      }
    },
    getdata: config => {
      const now  = Date.now () / 1000
      const data = Array.from (document.querySelectorAll ('div.mix[data-bump][data-reply][data-time]')).map (e => ["data-bump", "data-reply", "data-time"].map (s => parseInt (e.getAttribute (s), 10))).map (([b, r, t]) => [now - b, r, now - t])
      config.data = data
    },
    draw: config => {
      const ctx    = config.context
      const w      = config.width
      const h      = config.height
      const radius = config.radius
      const data   = config.data
      const [maxb, maxr, maxt] = Array.from ({length: 3}, (x, k) => data.reduce ((a, b) => Math.max (a, b [k]), 0) || 1)

      data.reverse ()
      for (const [b, r, t] of data) {
        const x = t * w / maxt
        const y = h - r * h / maxr
        const c = Math.floor (b * 255 / maxb)
        const f = 'rgba(' + (255 - c) + ', 0, ' + c + ', 1)'

        ctx.fillStyle = f
        ctx.beginPath ()
        ctx.arc (x, y, radius, 0, 2 * Math.PI, true)
        ctx.fill ()
      }
    }
  }]
)) ({
  drawrelative: config => {
    const ctx   = config.context
    const data  = config.data
    const max   = data.reduce ((a, b) => Math.max (a, b), 0) || 1
    const w     = config.width
    const h     = config.height
    const count = config.count
    const step  = config.step

    ctx.fillStyle = config.back
    ctx.fillRect (0, 0, w, h)

    ctx.strokeStyle = config.grid
    for (let k = 0; k < count; k++) {
      ctx.beginPath ()
      ctx.moveTo (step * k, 0)
      ctx.lineTo (step * k, h)
      ctx.stroke ()
    }

    ctx.fillStyle = config.bars
    for (let k = 0; k < count; k++) {
      const y = data [k] * h / max
      ctx.fillRect (w - step - k * step, h - y, step, y)
    }
  },
  getdatadatecount: (config, datestrings) => {
    // YYYY-MM-DD
    const all = datestrings.reduce ((acc, key) => {
      if (key in acc) {
        acc [key] += 1
      } else {
        acc [key]  = 1
      }
      return acc
    }, {})
    const now = new Date ()
    const key = k => {
      const when = new Date (now)
      when.setUTCDate (now.getUTCDate () - k)
      return when.getUTCFullYear ().toString ().padStart (4, '0') + '-' + (when.getUTCMonth () + 1).toString ().padStart (2, '0') + '-' + when.getUTCDate ().toString ().padStart (2, '0')
    }
    const ret = Array.from ({length: config.count}, (x, k) => all [key (k)] ?? 0)
    config.data = ret
  }
})

Just a demo.
>>

 No.10904

Last50 links on catalog pages:
((count, min) => Array.from (document.querySelectorAll ("div#Grid div.thread")).map (t => [t.querySelector ('a[href*="/res/"]'), t.querySelector ("div.replies > strong")]).filter (([a, s]) => parseInt (s.innerText.replace (/^R: (\d+) .+$/, "$1"), 10) >= min).forEach (([a, s]) => { s.innerHTML += ' <a href="' + a.getAttribute ("href").replace (/^(.+)([.]html)$/, "$1+50$2") + '">L' + count + '</a>'; })) (50, 100)
>>

 No.10933

Update of js/show-own-posts.js to allow users to opt out of (You) tracking.
>>

 No.10934

>>

 No.10937

Here's some harmless fun with localStorage.own_posts:
((sortkeys, link, divide, cites, install, slice, groups) => {
  const own    = JSON.parse (localStorage.own_posts || '{}')
  const boards = sortkeys (Object.keys (own))
  const count  = boards.reduce ((acc, item) => acc + own [item].length, 0)
  const html   = ['own posts: ' + count + ' boards: ' + boards.length]
  boards.forEach (b => {
    const ownb = own [b].reverse ()
    const have = ownb.length
    const [div, mod, left] = divide (have, slice, groups)
    html.push ('board: ' + link ('/' + b + '/index.html', '/' + b + '/') + ' ' + link ('/' + b + '/catalog.html', '&copy;') + ' count: ' + have + ' = ' + div + ' * ' + slice + ' + ' + mod + ' + ' + left)
    for (let k = 0; k < div; k++) {
      html.push (cites (b, ownb, slice, k, slice))
    }
    if (mod) {
      html.push (cites (b, ownb, slice, div, mod))
    }
  })
  install (html)
}) (
  arr => arr.map (s => [s, s.toUpperCase ()]).sort ((a, b) => a [1] < b [1] ? -1 : a [1] > b [1] ? 1 : 0).map (p => p [0]),
  (href, text) => '<a href="' + href + '">' + text + '</a>',
  (have, slice, groups) => {
    const take = Math.min (have, slice * groups)
    const mod  = take % slice
    return [(take - mod) / slice, mod, have - take]
  },
  (board, arr, slice, skip, now) => arr.slice (skip * slice, skip * slice + now).map (s => '>>>/' + board + '/' + s).join (' '),
  html => {
    const old  = document.getElementById ("ownposts-report")
    if (old) { old.remove (); }
    const node = document.createElement ("div")
    node.setAttribute ("id", "ownposts-report")
    node.setAttribute ("style", "border: thin solid;")
    node.innerHTML = html.map (s => '<p>' + s + '</p>').join ("")
    const foot = document.querySelector ('footer')
    if (foot) {
      foot.parentNode.insertBefore (node, foot)
    } else {
      document.body.appendChild (node)
    }
  },
  45, 2
)
>>

 No.10966

to deal with tripiots:
Array.from (document.querySelectorAll ("span.trip")).forEach (e => { e.parentNode.parentNode.parentNode.querySelector ("div.body").innerHTML = "I am a child seeking attention."; });

idea from: https://textboard.org/prog/34#t34p105
>>

 No.11019

Fix for
> Undefined index: id
when reporting a post that gets deleted just before the report is submitted, without discarding the user's reporting effort on the remaining posts of a multipost report.
>>

 No.11035

Here's a patch to make auto-reload.js mark deleted posts. Theme maintainers can customize the marking using the div.post.reply.auto-reload-removed selector. JS users can do the same in User CSS. Deleted posts are not considered important enough to reset the update timer, but if this is desired the removed_posts counter is available for this purpose. To test, inspect and edit a reply id to a non-existent one. Keep in mind that this is narrowly aimed at marking deleted posts, and auto-reload.js has other bugs >>>/leftypol_archive/1759 with which this patch does not concern itself.
>>

 No.11139

File: 1662638998597.jpg ( 638.47 KB , 848x1200 , 969ba5628d2f34bf1575d4be64….jpg )

… U+2026 Po HORIZONTAL ELLIPSIS &hellip;

space >>11090
no space >>11090…

See >>11090 for cite markup.

To fix add the colon and the ampersand to the character class.
< ((?=[\s,.:)?!&])|$)
>>

 No.11140

>>11139
Thank you for your work anon.
>>

 No.11151

File: 1663581149363.jpg ( 366.99 KB , 1392x2048 , nitter.jpg )

User JS snippet for twitter → nitter link conversion

Array.from (document.querySelectorAll ('a[href^="https://twitter.com/"]')).forEach (e => { e.outerHTML = e.outerHTML + ' <a target="_blank" href="' + e.getAttribute ("href").replace (/^https:\/\/twitter\.com\//, "https://nitter.net/") + '">[nitter]</a>'; })
>>

 No.11270

>>11139:

Test
>>

 No.11283

>>11139
A pull request has been submitted for this bug.
>>

 No.11303

>>11283
>A pull request has been submitted for this bug.
I must be looking in the wrong place because I can only find one for the colon which would fix >>11090 but not for the ampersand which would fix >>11139

colon test >>11283 : versus >>11283:
ellipsis test >>11283 … versus >>11283…
>>

 No.12030

File: 1678937328590.png ( 1.88 MB , 1536x1024 , 608bfd6724eb6e62e9ae5dc04b….png )

>>>/meta/11075
>We don't really know where it is located in the source code because lainchan is spaghetti code
The bug is caused by instantiating the index template
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/3396999a17b4c67473a5b1739329a9d08b992c84/templates/themes/overboards/theme.php#L204
using the $config of the openBoard call of the buildOne call of whichever thread happens to be last in the $top_threads loop.

Unfortunately there doesn't seem to be any clean way to access the board-independent global config once openBoard has been run at least once. So a general solution may need to save a reference to the board-independent global config into a new global variable as soon as it is available
https://github.com/towards-a-new-leftypol/leftypol_lainchan/blob/3396999a17b4c67473a5b1739329a9d08b992c84/inc/functions.php#L38
and pass that to the instantiation of the index template.

Unique IPs: 14

[Return][Catalog][Top][Home][Post a Reply]
Delete Post [ ]
[ overboard / sfw / alt / cytube] [ leftypol / b / WRK / hobby / tech / edu / ga / ent / 777 / posad / i / a / R9K / dead ] [ meta ]
ReturnCatalogTopBottomHome