Release 4chan X v1.11.17.2.
This commit is contained in:
parent
d6f8d1ec2d
commit
7b00c00de3
@ -4,6 +4,9 @@ Sometimes the changelog has notes (not comprehensive) acknowledging people's wor
|
|||||||
|
|
||||||
### v1.11.17
|
### v1.11.17
|
||||||
|
|
||||||
|
**v1.11.17.2** *(2015-11-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.17.2/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.17.2/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||||
|
- Merge v1.11.16.9: Bring back scrolling prevention when captcha is focused.
|
||||||
|
|
||||||
**v1.11.17.1** *(2015-11-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.17.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.17.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
**v1.11.17.1** *(2015-11-15)* - [[Firefox](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.17.1/builds/4chan-X-noupdate.user.js "Firefox version")] [[Chromium](https://raw.githubusercontent.com/ccd0/4chan-x/1.11.17.1/builds/4chan-X-noupdate.crx "Chromium version")]
|
||||||
- Merge v1.11.16.8: Remove obsolete captcha bug workaround that may be causing scrolling to top.
|
- Merge v1.11.16.8: Remove obsolete captcha bug workaround that may be causing scrolling to top.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 4chan X beta
|
// @name 4chan X beta
|
||||||
// @version 1.11.17.1
|
// @version 1.11.17.2
|
||||||
// @minGMVer 1.14
|
// @minGMVer 1.14
|
||||||
// @minFFVer 26
|
// @minFFVer 26
|
||||||
// @namespace 4chan-X
|
// @namespace 4chan-X
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Generated by CoffeeScript
|
// Generated by CoffeeScript
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 4chan X beta
|
// @name 4chan X beta
|
||||||
// @version 1.11.17.1
|
// @version 1.11.17.2
|
||||||
// @minGMVer 1.14
|
// @minGMVer 1.14
|
||||||
// @minFFVer 26
|
// @minFFVer 26
|
||||||
// @namespace 4chan-X
|
// @namespace 4chan-X
|
||||||
@ -433,7 +433,7 @@
|
|||||||
doc = d.documentElement;
|
doc = d.documentElement;
|
||||||
|
|
||||||
g = {
|
g = {
|
||||||
VERSION: '1.11.17.1',
|
VERSION: '1.11.17.2',
|
||||||
NAMESPACE: '4chan X.',
|
NAMESPACE: '4chan X.',
|
||||||
boards: {}
|
boards: {}
|
||||||
};
|
};
|
||||||
@ -7115,7 +7115,15 @@
|
|||||||
return $.queueTask(function() {
|
return $.queueTask(function() {
|
||||||
if (!QR.inBubble()) {
|
if (!QR.inBubble()) {
|
||||||
QR.hasFocus = d.activeElement && QR.nodes.el.contains(d.activeElement);
|
QR.hasFocus = d.activeElement && QR.nodes.el.contains(d.activeElement);
|
||||||
return QR.nodes.el.classList.toggle('focus', QR.hasFocus);
|
QR.nodes.el.classList.toggle('focus', QR.hasFocus);
|
||||||
|
}
|
||||||
|
if (QR.captcha.isEnabled && QR.captcha === Captcha.v2 && !QR.captcha.noscript) {
|
||||||
|
if (QR.inCaptcha()) {
|
||||||
|
QR.scrollY = window.scrollY;
|
||||||
|
return $.on(d, 'scroll', QR.scrollLock);
|
||||||
|
} else {
|
||||||
|
return $.off(d, 'scroll', QR.scrollLock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -7126,6 +7134,17 @@
|
|||||||
return getComputedStyle(el).visibility !== 'hidden' && el.getBoundingClientRect().bottom > 0;
|
return getComputedStyle(el).visibility !== 'hidden' && el.getBoundingClientRect().bottom > 0;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
inCaptcha: function() {
|
||||||
|
var ref;
|
||||||
|
return (((ref = d.activeElement) != null ? ref.nodeName : void 0) === 'IFRAME' && QR.nodes.el.contains(d.activeElement)) || (QR.hasFocus && QR.inBubble());
|
||||||
|
},
|
||||||
|
scrollLock: function() {
|
||||||
|
if (QR.inCaptcha()) {
|
||||||
|
return window.scroll(window.scrollX, QR.scrollY);
|
||||||
|
} else {
|
||||||
|
return $.off(d, 'scroll', QR.scrollLock);
|
||||||
|
}
|
||||||
|
},
|
||||||
hide: function() {
|
hide: function() {
|
||||||
d.activeElement.blur();
|
d.activeElement.blur();
|
||||||
$.addClass(QR.nodes.el, 'autohide');
|
$.addClass(QR.nodes.el, 'autohide');
|
||||||
@ -8758,7 +8777,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watchBubbles: function(mutations) {
|
watchBubbles: function(mutations) {
|
||||||
var k, len1, mutation, node, results;
|
var k, len1, mutation, node, overlay, results;
|
||||||
results = [];
|
results = [];
|
||||||
for (k = 0, len1 = mutations.length; k < len1; k++) {
|
for (k = 0, len1 = mutations.length; k < len1; k++) {
|
||||||
mutation = mutations[k];
|
mutation = mutations[k];
|
||||||
@ -8769,9 +8788,17 @@
|
|||||||
for (q = 0, len2 = ref.length; q < len2; q++) {
|
for (q = 0, len2 = ref.length; q < len2; q++) {
|
||||||
node = ref[q];
|
node = ref[q];
|
||||||
if ($('iframe[src^="https://www.google.com/recaptcha/api2/frame"]', node)) {
|
if ($('iframe[src^="https://www.google.com/recaptcha/api2/frame"]', node)) {
|
||||||
results1.push(new MutationObserver(this.fixBubble.bind(this, node)).observe(node, {
|
new MutationObserver(this.fixBubble.bind(this, node)).observe(node, {
|
||||||
attributes: true
|
attributes: true
|
||||||
}));
|
});
|
||||||
|
if (overlay = $('div[style*="position: fixed;"]', node)) {
|
||||||
|
results1.push($.on(overlay, 'click', function() {
|
||||||
|
var ref1;
|
||||||
|
return (ref1 = $('#qr iframe')) != null ? ref1.blur() : void 0;
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
results1.push(void 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
results1.push(void 0);
|
results1.push(void 0);
|
||||||
}
|
}
|
||||||
@ -19803,18 +19830,8 @@
|
|||||||
"#qr .captcha-root {\n" +
|
"#qr .captcha-root {\n" +
|
||||||
" position: relative;\n" +
|
" position: relative;\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"#qr .captcha-container > div {\n" +
|
"#qr .captcha-container > div > div {\n" +
|
||||||
" margin: auto;\n" +
|
" margin: auto;\n" +
|
||||||
" width: 304px;\n" +
|
|
||||||
"}\n" +
|
|
||||||
"/* scrollable with scroll bar hidden; prevents scroll on space press */\n" +
|
|
||||||
":root.blink #qr .captcha-container > div {\n" +
|
|
||||||
" overflow: hidden;\n" +
|
|
||||||
"}\n" +
|
|
||||||
":root.blink #qr .captcha-container > div > div:first-of-type {\n" +
|
|
||||||
" overflow-y: scroll;\n" +
|
|
||||||
" overflow-x: hidden;\n" +
|
|
||||||
" padding-right: 15px;\n" +
|
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"#qr .captcha-counter {\n" +
|
"#qr .captcha-counter {\n" +
|
||||||
" display: block;\n" +
|
" display: block;\n" +
|
||||||
|
|||||||
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
// Generated by CoffeeScript
|
// Generated by CoffeeScript
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 4chan X
|
// @name 4chan X
|
||||||
// @version 1.11.17.1
|
// @version 1.11.17.2
|
||||||
// @minGMVer 1.14
|
// @minGMVer 1.14
|
||||||
// @minFFVer 26
|
// @minFFVer 26
|
||||||
// @namespace 4chan-X
|
// @namespace 4chan-X
|
||||||
@ -433,7 +433,7 @@
|
|||||||
doc = d.documentElement;
|
doc = d.documentElement;
|
||||||
|
|
||||||
g = {
|
g = {
|
||||||
VERSION: '1.11.17.1',
|
VERSION: '1.11.17.2',
|
||||||
NAMESPACE: '4chan X.',
|
NAMESPACE: '4chan X.',
|
||||||
boards: {}
|
boards: {}
|
||||||
};
|
};
|
||||||
@ -7115,7 +7115,15 @@
|
|||||||
return $.queueTask(function() {
|
return $.queueTask(function() {
|
||||||
if (!QR.inBubble()) {
|
if (!QR.inBubble()) {
|
||||||
QR.hasFocus = d.activeElement && QR.nodes.el.contains(d.activeElement);
|
QR.hasFocus = d.activeElement && QR.nodes.el.contains(d.activeElement);
|
||||||
return QR.nodes.el.classList.toggle('focus', QR.hasFocus);
|
QR.nodes.el.classList.toggle('focus', QR.hasFocus);
|
||||||
|
}
|
||||||
|
if (QR.captcha.isEnabled && QR.captcha === Captcha.v2 && !QR.captcha.noscript) {
|
||||||
|
if (QR.inCaptcha()) {
|
||||||
|
QR.scrollY = window.scrollY;
|
||||||
|
return $.on(d, 'scroll', QR.scrollLock);
|
||||||
|
} else {
|
||||||
|
return $.off(d, 'scroll', QR.scrollLock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -7126,6 +7134,17 @@
|
|||||||
return getComputedStyle(el).visibility !== 'hidden' && el.getBoundingClientRect().bottom > 0;
|
return getComputedStyle(el).visibility !== 'hidden' && el.getBoundingClientRect().bottom > 0;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
inCaptcha: function() {
|
||||||
|
var ref;
|
||||||
|
return (((ref = d.activeElement) != null ? ref.nodeName : void 0) === 'IFRAME' && QR.nodes.el.contains(d.activeElement)) || (QR.hasFocus && QR.inBubble());
|
||||||
|
},
|
||||||
|
scrollLock: function() {
|
||||||
|
if (QR.inCaptcha()) {
|
||||||
|
return window.scroll(window.scrollX, QR.scrollY);
|
||||||
|
} else {
|
||||||
|
return $.off(d, 'scroll', QR.scrollLock);
|
||||||
|
}
|
||||||
|
},
|
||||||
hide: function() {
|
hide: function() {
|
||||||
d.activeElement.blur();
|
d.activeElement.blur();
|
||||||
$.addClass(QR.nodes.el, 'autohide');
|
$.addClass(QR.nodes.el, 'autohide');
|
||||||
@ -8758,7 +8777,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watchBubbles: function(mutations) {
|
watchBubbles: function(mutations) {
|
||||||
var k, len1, mutation, node, results;
|
var k, len1, mutation, node, overlay, results;
|
||||||
results = [];
|
results = [];
|
||||||
for (k = 0, len1 = mutations.length; k < len1; k++) {
|
for (k = 0, len1 = mutations.length; k < len1; k++) {
|
||||||
mutation = mutations[k];
|
mutation = mutations[k];
|
||||||
@ -8769,9 +8788,17 @@
|
|||||||
for (q = 0, len2 = ref.length; q < len2; q++) {
|
for (q = 0, len2 = ref.length; q < len2; q++) {
|
||||||
node = ref[q];
|
node = ref[q];
|
||||||
if ($('iframe[src^="https://www.google.com/recaptcha/api2/frame"]', node)) {
|
if ($('iframe[src^="https://www.google.com/recaptcha/api2/frame"]', node)) {
|
||||||
results1.push(new MutationObserver(this.fixBubble.bind(this, node)).observe(node, {
|
new MutationObserver(this.fixBubble.bind(this, node)).observe(node, {
|
||||||
attributes: true
|
attributes: true
|
||||||
}));
|
});
|
||||||
|
if (overlay = $('div[style*="position: fixed;"]', node)) {
|
||||||
|
results1.push($.on(overlay, 'click', function() {
|
||||||
|
var ref1;
|
||||||
|
return (ref1 = $('#qr iframe')) != null ? ref1.blur() : void 0;
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
results1.push(void 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
results1.push(void 0);
|
results1.push(void 0);
|
||||||
}
|
}
|
||||||
@ -19803,18 +19830,8 @@
|
|||||||
"#qr .captcha-root {\n" +
|
"#qr .captcha-root {\n" +
|
||||||
" position: relative;\n" +
|
" position: relative;\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"#qr .captcha-container > div {\n" +
|
"#qr .captcha-container > div > div {\n" +
|
||||||
" margin: auto;\n" +
|
" margin: auto;\n" +
|
||||||
" width: 304px;\n" +
|
|
||||||
"}\n" +
|
|
||||||
"/* scrollable with scroll bar hidden; prevents scroll on space press */\n" +
|
|
||||||
":root.blink #qr .captcha-container > div {\n" +
|
|
||||||
" overflow: hidden;\n" +
|
|
||||||
"}\n" +
|
|
||||||
":root.blink #qr .captcha-container > div > div:first-of-type {\n" +
|
|
||||||
" overflow-y: scroll;\n" +
|
|
||||||
" overflow-x: hidden;\n" +
|
|
||||||
" padding-right: 15px;\n" +
|
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"#qr .captcha-counter {\n" +
|
"#qr .captcha-counter {\n" +
|
||||||
" display: block;\n" +
|
" display: block;\n" +
|
||||||
|
|||||||
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 4chan X
|
// @name 4chan X
|
||||||
// @version 1.11.17.1
|
// @version 1.11.17.2
|
||||||
// @minGMVer 1.14
|
// @minGMVer 1.14
|
||||||
// @minFFVer 26
|
// @minFFVer 26
|
||||||
// @namespace 4chan-X
|
// @namespace 4chan-X
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Generated by CoffeeScript
|
// Generated by CoffeeScript
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 4chan X
|
// @name 4chan X
|
||||||
// @version 1.11.17.1
|
// @version 1.11.17.2
|
||||||
// @minGMVer 1.14
|
// @minGMVer 1.14
|
||||||
// @minFFVer 26
|
// @minFFVer 26
|
||||||
// @namespace 4chan-X
|
// @namespace 4chan-X
|
||||||
@ -433,7 +433,7 @@
|
|||||||
doc = d.documentElement;
|
doc = d.documentElement;
|
||||||
|
|
||||||
g = {
|
g = {
|
||||||
VERSION: '1.11.17.1',
|
VERSION: '1.11.17.2',
|
||||||
NAMESPACE: '4chan X.',
|
NAMESPACE: '4chan X.',
|
||||||
boards: {}
|
boards: {}
|
||||||
};
|
};
|
||||||
@ -7115,7 +7115,15 @@
|
|||||||
return $.queueTask(function() {
|
return $.queueTask(function() {
|
||||||
if (!QR.inBubble()) {
|
if (!QR.inBubble()) {
|
||||||
QR.hasFocus = d.activeElement && QR.nodes.el.contains(d.activeElement);
|
QR.hasFocus = d.activeElement && QR.nodes.el.contains(d.activeElement);
|
||||||
return QR.nodes.el.classList.toggle('focus', QR.hasFocus);
|
QR.nodes.el.classList.toggle('focus', QR.hasFocus);
|
||||||
|
}
|
||||||
|
if (QR.captcha.isEnabled && QR.captcha === Captcha.v2 && !QR.captcha.noscript) {
|
||||||
|
if (QR.inCaptcha()) {
|
||||||
|
QR.scrollY = window.scrollY;
|
||||||
|
return $.on(d, 'scroll', QR.scrollLock);
|
||||||
|
} else {
|
||||||
|
return $.off(d, 'scroll', QR.scrollLock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -7126,6 +7134,17 @@
|
|||||||
return getComputedStyle(el).visibility !== 'hidden' && el.getBoundingClientRect().bottom > 0;
|
return getComputedStyle(el).visibility !== 'hidden' && el.getBoundingClientRect().bottom > 0;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
inCaptcha: function() {
|
||||||
|
var ref;
|
||||||
|
return (((ref = d.activeElement) != null ? ref.nodeName : void 0) === 'IFRAME' && QR.nodes.el.contains(d.activeElement)) || (QR.hasFocus && QR.inBubble());
|
||||||
|
},
|
||||||
|
scrollLock: function() {
|
||||||
|
if (QR.inCaptcha()) {
|
||||||
|
return window.scroll(window.scrollX, QR.scrollY);
|
||||||
|
} else {
|
||||||
|
return $.off(d, 'scroll', QR.scrollLock);
|
||||||
|
}
|
||||||
|
},
|
||||||
hide: function() {
|
hide: function() {
|
||||||
d.activeElement.blur();
|
d.activeElement.blur();
|
||||||
$.addClass(QR.nodes.el, 'autohide');
|
$.addClass(QR.nodes.el, 'autohide');
|
||||||
@ -8758,7 +8777,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watchBubbles: function(mutations) {
|
watchBubbles: function(mutations) {
|
||||||
var k, len1, mutation, node, results;
|
var k, len1, mutation, node, overlay, results;
|
||||||
results = [];
|
results = [];
|
||||||
for (k = 0, len1 = mutations.length; k < len1; k++) {
|
for (k = 0, len1 = mutations.length; k < len1; k++) {
|
||||||
mutation = mutations[k];
|
mutation = mutations[k];
|
||||||
@ -8769,9 +8788,17 @@
|
|||||||
for (q = 0, len2 = ref.length; q < len2; q++) {
|
for (q = 0, len2 = ref.length; q < len2; q++) {
|
||||||
node = ref[q];
|
node = ref[q];
|
||||||
if ($('iframe[src^="https://www.google.com/recaptcha/api2/frame"]', node)) {
|
if ($('iframe[src^="https://www.google.com/recaptcha/api2/frame"]', node)) {
|
||||||
results1.push(new MutationObserver(this.fixBubble.bind(this, node)).observe(node, {
|
new MutationObserver(this.fixBubble.bind(this, node)).observe(node, {
|
||||||
attributes: true
|
attributes: true
|
||||||
}));
|
});
|
||||||
|
if (overlay = $('div[style*="position: fixed;"]', node)) {
|
||||||
|
results1.push($.on(overlay, 'click', function() {
|
||||||
|
var ref1;
|
||||||
|
return (ref1 = $('#qr iframe')) != null ? ref1.blur() : void 0;
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
results1.push(void 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
results1.push(void 0);
|
results1.push(void 0);
|
||||||
}
|
}
|
||||||
@ -19803,18 +19830,8 @@
|
|||||||
"#qr .captcha-root {\n" +
|
"#qr .captcha-root {\n" +
|
||||||
" position: relative;\n" +
|
" position: relative;\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"#qr .captcha-container > div {\n" +
|
"#qr .captcha-container > div > div {\n" +
|
||||||
" margin: auto;\n" +
|
" margin: auto;\n" +
|
||||||
" width: 304px;\n" +
|
|
||||||
"}\n" +
|
|
||||||
"/* scrollable with scroll bar hidden; prevents scroll on space press */\n" +
|
|
||||||
":root.blink #qr .captcha-container > div {\n" +
|
|
||||||
" overflow: hidden;\n" +
|
|
||||||
"}\n" +
|
|
||||||
":root.blink #qr .captcha-container > div > div:first-of-type {\n" +
|
|
||||||
" overflow-y: scroll;\n" +
|
|
||||||
" overflow-x: hidden;\n" +
|
|
||||||
" padding-right: 15px;\n" +
|
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"#qr .captcha-counter {\n" +
|
"#qr .captcha-counter {\n" +
|
||||||
" display: block;\n" +
|
" display: block;\n" +
|
||||||
|
|||||||
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||||
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
||||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.11.17.1' />
|
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X-beta.crx' version='1.11.17.2' />
|
||||||
</app>
|
</app>
|
||||||
</gupdate>
|
</gupdate>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||||
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
<app appid='lacclbnghgdicfifcamcmcnilckjamag'>
|
||||||
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.11.17.1' />
|
<updatecheck codebase='https://www.4chan-x.net/builds/4chan-X.crx' version='1.11.17.2' />
|
||||||
</app>
|
</app>
|
||||||
</gupdate>
|
</gupdate>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "1.11.17.1",
|
"version": "1.11.17.2",
|
||||||
"date": "2015-11-16T06:41:09.771Z"
|
"date": "2015-11-16T07:50:49.812Z"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user