Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db5f07f164 | ||
|
|
e050e69a43 | ||
|
|
27cb1d4fc7 | ||
|
|
5d6a740947 | ||
|
|
da3f68c363 | ||
|
|
d7d1c3685c |
13
README.md
13
README.md
@@ -59,13 +59,16 @@ launch either of them and it'll unpack and run copyparty, assuming you have pyth
|
||||
|
||||
pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `copyparty.py` and keep it in the same folder because `sys.path` is funky
|
||||
|
||||
if you don't need all the features you can repack the sfx and save a bunch of space, tho currently the only removable feature is the opus/vorbis javascript decoder which is needed by apple devices to play foss audio files
|
||||
if you don't need all the features you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except for either msys2 or WSL if you're on windows)
|
||||
* `724K` original size as of v0.4.0
|
||||
* `256K` after `./scripts/make-sfx.sh re no-ogv`
|
||||
* `164K` after `./scripts/make-sfx.sh re no-ogv no-cm`
|
||||
|
||||
steps to reduce the sfx size from `720 kB` to `250 kB` roughly:
|
||||
* run one of the sfx'es once to unpack it
|
||||
* `./scripts/make-sfx.sh re no-ogv` creates a new pair of sfx
|
||||
the features you can opt to drop are
|
||||
* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files
|
||||
* `cm`/easymde, the "fancy" markdown editor
|
||||
|
||||
no internet connection needed, just download an sfx and the repo zip (also if you're on windows use msys2)
|
||||
for the `re`pack to work, first run one of the sfx'es once to unpack it
|
||||
|
||||
|
||||
# install on android
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (0, 4, 0)
|
||||
VERSION = (0, 4, 1)
|
||||
CODENAME = "NIH"
|
||||
BUILD_DT = (2020, 5, 13)
|
||||
BUILD_DT = (2020, 5, 14)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -83,6 +83,7 @@ h3 {
|
||||
h1 a, h3 a, h5 a,
|
||||
h2 a, h4 a, h6 a {
|
||||
color: inherit;
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
@@ -239,7 +240,7 @@ blink {
|
||||
}
|
||||
#mn.undocked {
|
||||
position: fixed;
|
||||
padding: 1.2em 0 1em 1em;
|
||||
padding: 1.7em 0 1.5em 1em;
|
||||
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.3);
|
||||
background: #f7f7f7;
|
||||
}
|
||||
@@ -424,6 +425,16 @@ blink {
|
||||
html.dark #mw {
|
||||
scrollbar-color: #b80 #282828;
|
||||
}
|
||||
html.dark #toc::-webkit-scrollbar-track {
|
||||
background: #282828;
|
||||
}
|
||||
html.dark #toc::-webkit-scrollbar {
|
||||
background: #282828;
|
||||
width: .8em;
|
||||
}
|
||||
html.dark #toc::-webkit-scrollbar-thumb {
|
||||
background: #eb0;
|
||||
}
|
||||
html.dark #mn.undocked {
|
||||
box-shadow: 0 0 .5em #555;
|
||||
border: none;
|
||||
|
||||
@@ -6,10 +6,30 @@ var dom_pre = document.getElementById('mp');
|
||||
var dom_src = document.getElementById('mt');
|
||||
var dom_navtgl = document.getElementById('navtoggle');
|
||||
|
||||
|
||||
// chrome 49 needs this
|
||||
var chromedbg = function () { console.log(arguments); }
|
||||
|
||||
// null-logger
|
||||
var dbg = function () { };
|
||||
|
||||
// replace dbg with the real deal here or in the console:
|
||||
// dbg = chromedbg
|
||||
// dbg = console.log
|
||||
|
||||
|
||||
function hesc(txt) {
|
||||
return txt.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
|
||||
function cls(dom, name, add) {
|
||||
var re = new RegExp('(^| )' + name + '( |$)');
|
||||
var lst = (dom.getAttribute('class') + '').replace(re, "$1$2").replace(/ /, "");
|
||||
dom.setAttribute('class', lst + (add ? ' ' + name : ''));
|
||||
}
|
||||
|
||||
|
||||
// add navbar
|
||||
(function () {
|
||||
var n = document.location + '';
|
||||
@@ -28,17 +48,105 @@ function hesc(txt) {
|
||||
dom_nav.innerHTML = nav.join('');
|
||||
})();
|
||||
|
||||
|
||||
// faster than replacing the entire html (chrome 1.8x, firefox 1.6x)
|
||||
function copydom(src, dst, lv) {
|
||||
var sc = src.childNodes,
|
||||
dc = dst.childNodes;
|
||||
|
||||
if (sc.length !== dc.length) {
|
||||
dbg("replace L%d (%d/%d) |%d|",
|
||||
lv, sc.length, dc.length, src.innerHTML.length);
|
||||
|
||||
dst.innerHTML = src.innerHTML;
|
||||
return;
|
||||
}
|
||||
|
||||
var rpl = [];
|
||||
for (var a = sc.length - 1; a >= 0; a--) {
|
||||
var st = sc[a].tagName,
|
||||
dt = dc[a].tagName;
|
||||
|
||||
if (st !== dt) {
|
||||
dbg("replace L%d (%d/%d) type %s/%s", lv, a, sc.length, st, dt);
|
||||
rpl.push(a);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sa = sc[a].attributes || [],
|
||||
da = dc[a].attributes || [];
|
||||
|
||||
if (sa.length !== da.length) {
|
||||
dbg("replace L%d (%d/%d) attr# %d/%d",
|
||||
lv, a, sc.length, sa.length, da.length);
|
||||
|
||||
rpl.push(a);
|
||||
continue;
|
||||
}
|
||||
|
||||
var dirty = false;
|
||||
for (var b = sa.length - 1; b >= 0; b--) {
|
||||
var name = sa[b].name,
|
||||
sv = sa[b].value,
|
||||
dv = dc[a].getAttribute(name);
|
||||
|
||||
if (name == "data-ln" && sv !== dv) {
|
||||
dc[a].setAttribute(name, sv);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sv !== dv) {
|
||||
dbg("replace L%d (%d/%d) attr %s [%s] [%s]",
|
||||
lv, a, sc.length, name, sv, dv);
|
||||
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dirty)
|
||||
rpl.push(a);
|
||||
}
|
||||
|
||||
// TODO pure guessing
|
||||
if (rpl.length > sc.length / 3) {
|
||||
dbg("replace L%d fully, %s (%d/%d) |%d|",
|
||||
lv, rpl.length, sc.length, src.innerHTML.length);
|
||||
|
||||
dst.innerHTML = src.innerHTML;
|
||||
return;
|
||||
}
|
||||
|
||||
// repl is reversed; build top-down
|
||||
var nbytes = 0;
|
||||
for (var a = rpl.length - 1; a >= 0; a--) {
|
||||
var html = sc[rpl[a]].outerHTML;
|
||||
dc[rpl[a]].outerHTML = html;
|
||||
nbytes += html.length;
|
||||
}
|
||||
if (nbytes > 0)
|
||||
dbg("replaced %d bytes L%d", nbytes, lv);
|
||||
|
||||
for (var a = 0; a < sc.length; a++)
|
||||
copydom(sc[a], dc[a], lv + 1);
|
||||
|
||||
if (src.innerHTML !== dst.innerHTML) {
|
||||
dbg("setting %d bytes L%d", src.innerHTML.length, lv);
|
||||
dst.innerHTML = src.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function convert_markdown(md_text) {
|
||||
marked.setOptions({
|
||||
//headerPrefix: 'h-',
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
var html = marked(md_text);
|
||||
dom_pre.innerHTML = html;
|
||||
var md_html = marked(md_text);
|
||||
var md_dom = new DOMParser().parseFromString(md_html, "text/html").body;
|
||||
|
||||
// todo-lists (should probably be a marked extension)
|
||||
var nodes = dom_pre.getElementsByTagName('input');
|
||||
var nodes = md_dom.getElementsByTagName('input');
|
||||
for (var a = nodes.length - 1; a >= 0; a--) {
|
||||
var dom_box = nodes[a];
|
||||
if (dom_box.getAttribute('type') !== 'checkbox')
|
||||
@@ -58,9 +166,10 @@ function convert_markdown(md_text) {
|
||||
html.substr(html.indexOf('>') + 1);
|
||||
}
|
||||
|
||||
var manip_nodes = dom_pre.getElementsByTagName('*');
|
||||
for (var a = manip_nodes.length - 1; a >= 0; a--) {
|
||||
var el = manip_nodes[a];
|
||||
// separate <code> for each line in <pre>
|
||||
var nodes = md_dom.getElementsByTagName('pre');
|
||||
for (var a = nodes.length - 1; a >= 0; a--) {
|
||||
var el = nodes[a];
|
||||
|
||||
var is_precode =
|
||||
el.tagName == 'PRE' &&
|
||||
@@ -77,7 +186,36 @@ function convert_markdown(md_text) {
|
||||
|
||||
el.innerHTML = lines.join('');
|
||||
}
|
||||
|
||||
// self-link headers
|
||||
var id_seen = {},
|
||||
dyn = md_dom.getElementsByTagName('*');
|
||||
|
||||
nodes = [];
|
||||
for (var a = 0, aa = dyn.length; a < aa; a++)
|
||||
if (/^[Hh]([1-6])/.exec(dyn[a].tagName) !== null)
|
||||
nodes.push(dyn[a]);
|
||||
|
||||
for (var a = 0; a < nodes.length; a++) {
|
||||
el = nodes[a];
|
||||
var id = el.getAttribute('id'),
|
||||
orig_id = id;
|
||||
|
||||
if (id_seen[id]) {
|
||||
for (var n = 1; n < 4096; n++) {
|
||||
id = orig_id + '-' + n;
|
||||
if (!id_seen[id])
|
||||
break;
|
||||
}
|
||||
el.setAttribute('id', id);
|
||||
}
|
||||
id_seen[id] = 1;
|
||||
el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
|
||||
}
|
||||
|
||||
copydom(md_dom, dom_pre, 0);
|
||||
}
|
||||
|
||||
|
||||
function init_toc() {
|
||||
var loader = document.getElementById('ml');
|
||||
@@ -85,10 +223,8 @@ function init_toc() {
|
||||
|
||||
var anchors = []; // list of toc entries, complex objects
|
||||
var anchor = null; // current toc node
|
||||
var id_seen = {}; // taken IDs
|
||||
var html = []; // generated toc html
|
||||
var lv = 0; // current indentation level in the toc html
|
||||
var re = new RegExp('^[Hh]([1-3])');
|
||||
|
||||
var manip_nodes_dyn = dom_pre.getElementsByTagName('*');
|
||||
var manip_nodes = [];
|
||||
@@ -97,7 +233,7 @@ function init_toc() {
|
||||
|
||||
for (var a = 0, aa = manip_nodes.length; a < aa; a++) {
|
||||
var elm = manip_nodes[a];
|
||||
var m = re.exec(elm.tagName);
|
||||
var m = /^[Hh]([1-6])/.exec(elm.tagName);
|
||||
var is_header = m !== null;
|
||||
if (is_header) {
|
||||
var nlv = m[1];
|
||||
@@ -110,23 +246,7 @@ function init_toc() {
|
||||
lv--;
|
||||
}
|
||||
|
||||
var orig_id = elm.getAttribute('id');
|
||||
var id = orig_id;
|
||||
if (id_seen[id]) {
|
||||
for (var n = 1; n < 4096; n++) {
|
||||
id = orig_id + '-' + n;
|
||||
if (!id_seen[id])
|
||||
break;
|
||||
}
|
||||
elm.setAttribute('id', id);
|
||||
}
|
||||
id_seen[id] = 1;
|
||||
|
||||
var ahref = '<a href="#' + id + '">' +
|
||||
elm.innerHTML + '</a>';
|
||||
|
||||
html.push('<li>' + ahref + '</li>');
|
||||
elm.innerHTML = ahref;
|
||||
html.push('<li>' + elm.innerHTML + '</li>');
|
||||
|
||||
if (anchor != null)
|
||||
anchors.push(anchor);
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
}
|
||||
#mw {
|
||||
left: calc(100% - 57em);
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +24,17 @@
|
||||
}
|
||||
#mw.preview,
|
||||
#mtw.editor {
|
||||
z-index: 3;
|
||||
z-index: 5;
|
||||
}
|
||||
#mtw.single,
|
||||
#mw.single {
|
||||
left: calc((100% - 58em) / 2);
|
||||
margin: 0;
|
||||
left: 1em;
|
||||
left: max(1em, calc((100% - 58em) / 2));
|
||||
}
|
||||
#mtw.single {
|
||||
width: 57em;
|
||||
width: min(57em, calc(100% - 2em));
|
||||
}
|
||||
|
||||
|
||||
@@ -42,25 +47,30 @@
|
||||
color: #444;
|
||||
background: #f7f7f7;
|
||||
border: 1px solid #999;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'consolas', monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word; /*ie*/
|
||||
overflow-y: scroll;
|
||||
line-height: 1.3em;
|
||||
font-size: .9em;
|
||||
position: relative;
|
||||
scrollbar-color: #eb0 #f7f7f7;
|
||||
}
|
||||
html.dark #mt {
|
||||
color: #eee;
|
||||
background: #222;
|
||||
border: 1px solid #777;
|
||||
scrollbar-color: #b80 #282828;
|
||||
}
|
||||
#mtr {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#save.force-save {
|
||||
color: #400;
|
||||
@@ -95,8 +105,4 @@ html.dark #helpbox {
|
||||
border-width: 1px 0;
|
||||
}
|
||||
|
||||
/* dbg:
|
||||
#mt {
|
||||
opacity: .5;
|
||||
}
|
||||
*/
|
||||
# mt {opacity: .5;top:1px}
|
||||
|
||||
@@ -18,16 +18,12 @@ var dom_ref = (function () {
|
||||
})();
|
||||
|
||||
|
||||
// replace it with the real deal in the console
|
||||
var dbg = function () { };
|
||||
// dbg = console.log
|
||||
|
||||
|
||||
// line->scrollpos maps
|
||||
var map_src = [];
|
||||
var map_pre = [];
|
||||
function genmap(dom) {
|
||||
var ret = [];
|
||||
var last_y = -1;
|
||||
var parent_y = 0;
|
||||
var parent_n = null;
|
||||
var nodes = dom.querySelectorAll('*[data-ln]');
|
||||
@@ -53,7 +49,14 @@ function genmap(dom) {
|
||||
while (ln > ret.length)
|
||||
ret.push(null);
|
||||
|
||||
ret.push(parent_y + n.offsetTop);
|
||||
var y = parent_y + n.offsetTop;
|
||||
if (y <= last_y)
|
||||
//console.log('awawa');
|
||||
continue;
|
||||
|
||||
//console.log('%d %d (%d+%d)', a, y, parent_y, n.offsetTop);
|
||||
ret.push(y);
|
||||
last_y = y;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -62,8 +65,10 @@ function genmap(dom) {
|
||||
// input handler
|
||||
var action_stack = null;
|
||||
var nlines = 0;
|
||||
(function () {
|
||||
dom_src.oninput = function (e) {
|
||||
var draw_md = (function () {
|
||||
var delay = 1;
|
||||
function draw_md() {
|
||||
var t0 = new Date().getTime();
|
||||
var src = dom_src.value;
|
||||
convert_markdown(src);
|
||||
|
||||
@@ -77,16 +82,22 @@ var nlines = 0;
|
||||
map_src = genmap(dom_ref);
|
||||
map_pre = genmap(dom_pre);
|
||||
|
||||
var sb = document.getElementById('save');
|
||||
var cl = (sb.getAttribute('class') + '').replace(/ disabled/, "");
|
||||
if (src == server_md)
|
||||
cl += ' disabled';
|
||||
cls(document.getElementById('save'), 'disabled', src == server_md);
|
||||
|
||||
sb.setAttribute('class', cl);
|
||||
var t1 = new Date().getTime();
|
||||
delay = t1 - t0 > 150 ? 25 : 1;
|
||||
}
|
||||
|
||||
var timeout = null;
|
||||
dom_src.oninput = function (e) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(draw_md, delay);
|
||||
if (action_stack)
|
||||
action_stack.push();
|
||||
}
|
||||
dom_src.oninput();
|
||||
};
|
||||
|
||||
draw_md();
|
||||
return draw_md;
|
||||
})();
|
||||
|
||||
|
||||
@@ -96,7 +107,7 @@ redraw = (function () {
|
||||
var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';
|
||||
dom_wrap.style.top = y;
|
||||
dom_swrap.style.top = y;
|
||||
dom_ref.style.width = (dom_src.offsetWidth - 4) + 'px';
|
||||
dom_ref.style.width = getComputedStyle(dom_src).offsetWidth + 'px';
|
||||
map_src = genmap(dom_ref);
|
||||
map_pre = genmap(dom_pre);
|
||||
dbg(document.body.clientWidth + 'x' + document.body.clientHeight);
|
||||
@@ -296,7 +307,7 @@ function save_chk() {
|
||||
|
||||
last_modified = this.lastmod;
|
||||
server_md = this.txt;
|
||||
dom_src.oninput();
|
||||
draw_md();
|
||||
|
||||
var ok = document.createElement('div');
|
||||
ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1');
|
||||
@@ -365,11 +376,8 @@ function setsel(s) {
|
||||
}
|
||||
dom_src.value = [s.pre, s.sel, s.post].join('');
|
||||
dom_src.setSelectionRange(s.car, s.cdr);
|
||||
try {
|
||||
dom_src.oninput();
|
||||
}
|
||||
catch (ex) { }
|
||||
}
|
||||
|
||||
|
||||
// indent/dedent
|
||||
@@ -426,7 +434,7 @@ function md_home(shift) {
|
||||
function md_newline() {
|
||||
var s = linebounds(true),
|
||||
ln = s.md.substring(s.n1, s.n2),
|
||||
m = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/.exec(ln);
|
||||
m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(ln);
|
||||
|
||||
s.pre = s.md.substring(0, s.car) + '\n' + m[0];
|
||||
s.sel = '';
|
||||
@@ -501,12 +509,12 @@ document.getElementById('help').onclick = function (e) {
|
||||
action_stack = (function () {
|
||||
var undos = [];
|
||||
var redos = [];
|
||||
var sched_txt = '';
|
||||
var sched_cpos = 0;
|
||||
var sched_timer = null;
|
||||
var ignore = false;
|
||||
var ref = dom_src.value;
|
||||
|
||||
function diff(from, to) {
|
||||
function diff(from, to, cpos) {
|
||||
if (from === to)
|
||||
return null;
|
||||
|
||||
@@ -532,14 +540,15 @@ action_stack = (function () {
|
||||
return {
|
||||
car: car,
|
||||
cdr: ++p2,
|
||||
txt: txt
|
||||
txt: txt,
|
||||
cpos: cpos
|
||||
};
|
||||
}
|
||||
|
||||
function undiff(from, change) {
|
||||
return {
|
||||
txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
|
||||
cursor: change.car + change.txt.length
|
||||
cpos: change.cpos
|
||||
};
|
||||
}
|
||||
|
||||
@@ -549,19 +558,21 @@ action_stack = (function () {
|
||||
if (src.length === 0)
|
||||
return false;
|
||||
|
||||
var state = undiff(ref, src.pop()),
|
||||
change = diff(ref, state.txt);
|
||||
var patch = src.pop(),
|
||||
applied = undiff(ref, patch),
|
||||
cpos = patch.cpos - (patch.cdr - patch.car) + patch.txt.length,
|
||||
reverse = diff(ref, applied.txt, cpos);
|
||||
|
||||
if (change === null)
|
||||
if (reverse === null)
|
||||
return false;
|
||||
|
||||
dst.push(change);
|
||||
ref = state.txt;
|
||||
dst.push(reverse);
|
||||
ref = applied.txt;
|
||||
ignore = true; // just some browsers
|
||||
dom_src.value = ref;
|
||||
dom_src.setSelectionRange(state.cursor, state.cursor);
|
||||
dom_src.setSelectionRange(cpos, cpos);
|
||||
ignore = true; // all browsers
|
||||
dom_src.oninput();
|
||||
draw_md();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -571,12 +582,16 @@ action_stack = (function () {
|
||||
return;
|
||||
}
|
||||
redos = [];
|
||||
sched_txt = dom_src.value;
|
||||
clearTimeout(sched_timer);
|
||||
sched_cpos = dom_src.selectionEnd;
|
||||
sched_timer = setTimeout(push, 500);
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (redos.length == 0) {
|
||||
clearTimeout(sched_timer);
|
||||
push();
|
||||
}
|
||||
return apply(undos, redos);
|
||||
}
|
||||
|
||||
@@ -585,11 +600,12 @@ action_stack = (function () {
|
||||
}
|
||||
|
||||
function push() {
|
||||
var change = diff(ref, sched_txt, dom_src.selectionStart);
|
||||
var newtxt = dom_src.value;
|
||||
var change = diff(ref, newtxt, sched_cpos);
|
||||
if (change !== null)
|
||||
undos.push(change);
|
||||
|
||||
ref = sched_txt;
|
||||
ref = newtxt;
|
||||
dbg('undos(%d) redos(%d)', undos.length, redos.length);
|
||||
if (undos.length > 0)
|
||||
dbg(undos.slice(-1)[0]);
|
||||
@@ -607,3 +623,14 @@ action_stack = (function () {
|
||||
_ref: ref
|
||||
}
|
||||
})();
|
||||
|
||||
/*
|
||||
document.getElementById('help').onclick = function () {
|
||||
var c1 = getComputedStyle(dom_src).cssText.split(';');
|
||||
var c2 = getComputedStyle(dom_ref).cssText.split(';');
|
||||
var max = Math.min(c1.length, c2.length);
|
||||
for (var a = 0; a < max; a++)
|
||||
if (c1[a] !== c2[a])
|
||||
console.log(c1[a] + '\n' + c2[a]);
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user