mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1210616 - have remote (sync) tabs appear in awesomebar suggestions. r=mak
This commit is contained in:
parent
587e99a62e
commit
225e172a8e
@ -157,6 +157,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
if (action) {
|
||||
switch (action.type) {
|
||||
case "switchtab": // Fall through.
|
||||
case "remotetab": // Fall through.
|
||||
case "visiturl": {
|
||||
returnValue = action.params.url;
|
||||
break;
|
||||
@ -1490,6 +1491,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
];
|
||||
break;
|
||||
case "switchtab":
|
||||
case "remotetab":
|
||||
parts = [
|
||||
item.getAttribute("title"),
|
||||
action.params.url,
|
||||
|
@ -575,6 +575,7 @@ BrowserGlue.prototype = {
|
||||
switchtab: 6,
|
||||
tag: 7,
|
||||
visiturl: 8,
|
||||
remotetab: 9,
|
||||
};
|
||||
if (actionType in buckets) {
|
||||
Services.telemetry
|
||||
|
@ -1176,7 +1176,7 @@ richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-t
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
|
||||
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
@ -1854,13 +1854,13 @@ richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-t
|
||||
color: -moz-nativehyperlinktext;
|
||||
}
|
||||
|
||||
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
|
||||
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
|
||||
-moz-image-region: rect(0, 16px, 11px, 0);
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
-moz-image-region: rect(11px, 16px, 22px, 0);
|
||||
}
|
||||
|
||||
@ -1879,13 +1879,13 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-
|
||||
list-style-image: url("chrome://browser/skin/places/tag@2x.png");
|
||||
}
|
||||
|
||||
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
|
||||
list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
|
||||
-moz-image-region: rect(0, 32px, 22px, 0);
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
-moz-image-region: rect(22px, 32px, 44px, 0);
|
||||
}
|
||||
}
|
||||
|
@ -1537,7 +1537,7 @@ richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-si
|
||||
}
|
||||
}
|
||||
|
||||
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
|
||||
list-style-image: url("chrome://browser/skin/actionicon-tab.png");
|
||||
-moz-image-region: rect(0, 16px, 11px, 0);
|
||||
padding: 0 3px;
|
||||
@ -1546,7 +1546,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
|
||||
list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
|
||||
-moz-image-region: rect(0, 32px, 22px, 0);
|
||||
}
|
||||
@ -1556,12 +1556,12 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
|
||||
not all and (-moz-windows-default-theme) {
|
||||
@media not all and (-moz-os-version: windows-win7),
|
||||
not all and (-moz-windows-default-theme) {
|
||||
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
-moz-image-region: rect(11px, 16px, 22px, 0);
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
|
||||
-moz-image-region: rect(22px, 32px, 44px, 0);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,120 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*
|
||||
* Provides functions to handle remote tabs (ie, tabs known by Sync) in
|
||||
* the awesomebar.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PlacesRemoteTabsAutocompleteProvider"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://services-sync/main.js");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "weaveXPCService", function() {
|
||||
return Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
});
|
||||
|
||||
// from MDN...
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
// Build the in-memory structure we use.
|
||||
function buildItems() {
|
||||
let clients = new Map(); // keyed by client guid, value is client
|
||||
let tabs = new Map(); // keyed by string URL, value is {clientId, tab}
|
||||
|
||||
// If Sync isn't initialized (either due to lag at startup or due to no user
|
||||
// being signed in), don't reach in to Weave.Service as that may initialize
|
||||
// Sync unnecessarily - we'll get an observer notification later when it
|
||||
// becomes ready and has synced a list of tabs.
|
||||
if (weaveXPCService.ready) {
|
||||
let engine = Weave.Service.engineManager.get("tabs");
|
||||
|
||||
for (let [guid, client] in Iterator(engine.getAllClients())) {
|
||||
clients.set(guid, client);
|
||||
for (let tab of client.tabs) {
|
||||
let url = tab.urlHistory[0];
|
||||
tabs.set(url, { clientId: guid, tab });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { clients, tabs };
|
||||
}
|
||||
|
||||
// Manage the cache of the items we use.
|
||||
// The cache itself.
|
||||
let _items = null;
|
||||
|
||||
// Ensure the cache is good.
|
||||
function ensureItems() {
|
||||
if (!_items) {
|
||||
_items = buildItems();
|
||||
}
|
||||
return _items;
|
||||
}
|
||||
|
||||
// An observer to invalidate _items.
|
||||
function observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "weave:engine:sync:finish":
|
||||
if (data == "tabs") {
|
||||
// The tabs engine just finished syncing, so may have a different list
|
||||
// of tabs then we previously cached.
|
||||
_items = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case "weave:service:start-over":
|
||||
// Sync is being reset due to the user disconnecting - we must invalidate
|
||||
// the cache so we don't supply tabs from a different user.
|
||||
_items = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Services.obs.addObserver(observe, "weave:engine:sync:finish", false);
|
||||
Services.obs.addObserver(observe, "weave:service:start-over", false);
|
||||
|
||||
// This public object is a static singleton.
|
||||
this.PlacesRemoteTabsAutocompleteProvider = {
|
||||
// a promise that resolves with an array of matching remote tabs.
|
||||
getMatches(searchString) {
|
||||
// If Sync isn't configured we bail early.
|
||||
if (!Services.prefs.prefHasUserValue("services.sync.username")) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
let re = new RegExp(escapeRegExp(searchString), "i");
|
||||
let matches = [];
|
||||
let { tabs, clients } = ensureItems();
|
||||
for (let [url, { clientId, tab }] of tabs) {
|
||||
let title = tab.title;
|
||||
if (url.match(re) || (title && title.match(re))) {
|
||||
// lookup the client record.
|
||||
let client = clients.get(clientId);
|
||||
// create the record we return for auto-complete.
|
||||
let record = {
|
||||
url, title,
|
||||
icon: tab.icon,
|
||||
deviceClass: Weave.Service.clientsEngine.isMobile(clientId) ? "mobile" : "desktop",
|
||||
deviceName: client.clientName,
|
||||
};
|
||||
matches.push(record);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(matches);
|
||||
},
|
||||
}
|
@ -263,6 +263,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
|
||||
"resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesRemoteTabsAutocompleteProvider",
|
||||
"resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
|
||||
"@mozilla.org/intl/texttosuburi;1",
|
||||
@ -585,6 +587,42 @@ function makeActionURL(action, params) {
|
||||
return NetUtil.newURI(url).spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key to be used for a URL in a map for the purposes of removing
|
||||
* duplicate entries - any 2 URLs that should be considered the same should
|
||||
* return the same key. For some moz-action URLs this will unwrap the params
|
||||
* and return a key based on the wrapped URL.
|
||||
*/
|
||||
function makeKeyForURL(actionUrl) {
|
||||
// At this stage we only consider moz-action URLs.
|
||||
if (!actionUrl.startsWith("moz-action:")) {
|
||||
return stripHttpAndTrim(actionUrl);
|
||||
}
|
||||
let [, type, params] = actionUrl.match(/^moz-action:([^,]+),(.*)$/);
|
||||
try {
|
||||
params = JSON.parse(params);
|
||||
} catch (ex) {
|
||||
// This is unexpected in this context, so just return the input.
|
||||
return stripHttpAndTrim(actionUrl);
|
||||
}
|
||||
// For now we only handle these 2 action types and treat them as the same.
|
||||
switch (type) {
|
||||
case "remotetab":
|
||||
case "switchtab":
|
||||
if (params.url) {
|
||||
return "moz-action:tab:" + stripHttpAndTrim(params.url);
|
||||
}
|
||||
break;
|
||||
// TODO (bug 1222435) - "switchtab" should be handled as an "autofill"
|
||||
// entry.
|
||||
default:
|
||||
// do nothing.
|
||||
// TODO (bug 1222436) - extend this method so it can be used instead of
|
||||
// the |placeId| that's also used to remove duplicate entries.
|
||||
}
|
||||
return stripHttpAndTrim(actionUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the passed in string looks like a url.
|
||||
*/
|
||||
@ -864,6 +902,9 @@ Search.prototype = {
|
||||
yield this._matchFirstHeuristicResult(conn);
|
||||
this._addingHeuristicFirstMatch = false;
|
||||
|
||||
// We sleep a little between adding the heuristicFirstMatch and matching
|
||||
// any other searches so we aren't kicking off potentially expensive
|
||||
// searches on every keystroke.
|
||||
yield this._sleep(Prefs.delay);
|
||||
if (!this.pending)
|
||||
return;
|
||||
@ -878,6 +919,12 @@ Search.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._enableActions && this.hasBehavior("openpage")) {
|
||||
yield this._matchRemoteTabs();
|
||||
if (!this.pending)
|
||||
return;
|
||||
}
|
||||
|
||||
// If we do not have enough results, and our match type is
|
||||
// MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
|
||||
// results.
|
||||
@ -1188,6 +1235,35 @@ Search.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
*_matchRemoteTabs() {
|
||||
let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
|
||||
for (let {url, title, icon, deviceClass, deviceName} of matches) {
|
||||
// It's rare that Sync supplies the icon for the page (but if it does, it
|
||||
// is a string URL)
|
||||
if (!icon) {
|
||||
try {
|
||||
let favicon = yield PlacesUtils.promiseFaviconLinkUrl(url);
|
||||
if (favicon) {
|
||||
icon = favicon.spec;
|
||||
}
|
||||
} catch (ex) {} // no favicon for this URL.
|
||||
}
|
||||
|
||||
let match = {
|
||||
// We include the deviceName in the action URL so we can render it in
|
||||
// the URLBar.
|
||||
value: makeActionURL("remotetab", { url, deviceName }),
|
||||
comment: title || url,
|
||||
style: "action",
|
||||
// we want frecency > FRECENCY_DEFAULT so it doesn't get pushed out
|
||||
// by "remote" matches.
|
||||
frecency: FRECENCY_DEFAULT + 1,
|
||||
icon,
|
||||
}
|
||||
this._addMatch(match);
|
||||
}
|
||||
},
|
||||
|
||||
// TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
|
||||
// scheme isn't specificed.
|
||||
_matchUnknownUrl: function* () {
|
||||
@ -1307,7 +1383,7 @@ Search.prototype = {
|
||||
return;
|
||||
|
||||
// Must check both id and url, cause keywords dynamically modify the url.
|
||||
let urlMapKey = stripHttpAndTrim(match.value);
|
||||
let urlMapKey = makeKeyForURL(match.value);
|
||||
if ((match.placeId && this._usedPlaceIds.has(match.placeId)) ||
|
||||
this._usedURLs.has(urlMapKey)) {
|
||||
return;
|
||||
|
@ -65,6 +65,7 @@ if CONFIG['MOZ_PLACES']:
|
||||
'History.jsm',
|
||||
'PlacesBackups.jsm',
|
||||
'PlacesDBUtils.jsm',
|
||||
'PlacesRemoteTabsAutocompleteProvider.jsm',
|
||||
'PlacesSearchAutocompleteProvider.jsm',
|
||||
'PlacesTransactions.jsm',
|
||||
'PlacesUtils.jsm',
|
||||
|
@ -0,0 +1,175 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
* vim:set ts=2 sw=2 sts=2 et:
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://services-sync/main.js");
|
||||
|
||||
Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com");
|
||||
|
||||
// A mock "Tabs" engine which autocomplete will use instead of the real
|
||||
// engine. We pass a constructor that Sync creates.
|
||||
function MockTabsEngine() {
|
||||
this.clients = null; // We'll set this dynamically
|
||||
}
|
||||
|
||||
MockTabsEngine.prototype = {
|
||||
name: "tabs",
|
||||
|
||||
getAllClients() {
|
||||
return this.clients;
|
||||
},
|
||||
}
|
||||
|
||||
// A clients engine that doesn't need to be a constructor.
|
||||
let MockClientsEngine = {
|
||||
isMobile(guid) {
|
||||
Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile"));
|
||||
return guid.endsWith("mobile");
|
||||
},
|
||||
}
|
||||
|
||||
// Tell Sync about the mocks.
|
||||
Weave.Service.engineManager.register(MockTabsEngine);
|
||||
Weave.Service.clientsEngine = MockClientsEngine;
|
||||
|
||||
// Tell the Sync XPCOM service it is initialized.
|
||||
let weaveXPCService = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
weaveXPCService.ready = true;
|
||||
|
||||
// Configure the singleton engine for a test.
|
||||
function configureEngine(clients) {
|
||||
// Configure the instance Sync created.
|
||||
let engine = Weave.Service.engineManager.get("tabs");
|
||||
engine.clients = clients;
|
||||
// Send an observer that pretends the engine just finished a sync.
|
||||
Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
|
||||
}
|
||||
|
||||
// Make a match object suitable for passing to check_autocomplete.
|
||||
function makeRemoteTabMatch(url, deviceName, extra = {}) {
|
||||
return {
|
||||
uri: makeActionURI("remotetab", {url, deviceName}),
|
||||
title: extra.title || url,
|
||||
style: [ "action" ],
|
||||
}
|
||||
}
|
||||
|
||||
// The tests.
|
||||
add_task(function* test_nomatch() {
|
||||
// Nothing matches.
|
||||
configureEngine({
|
||||
guid_desktop: {
|
||||
clientName: "My Desktop",
|
||||
tabs: [{
|
||||
urlHistory: ["http://foo.com/"],
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
// No remote tabs match here, so we only expect search results.
|
||||
yield check_autocomplete({
|
||||
search: "ex",
|
||||
searchParam: "enable-actions",
|
||||
matches: [ makeSearchMatch("ex", { heuristic: true }) ],
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_minimal() {
|
||||
// The minimal client and tabs info we can get away with.
|
||||
configureEngine({
|
||||
guid_desktop: {
|
||||
clientName: "My Desktop",
|
||||
tabs: [{
|
||||
urlHistory: ["http://example.com/"],
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: "ex",
|
||||
searchParam: "enable-actions",
|
||||
matches: [ makeSearchMatch("ex", { heuristic: true }),
|
||||
makeRemoteTabMatch("http://example.com/", "My Desktop") ],
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_maximal() {
|
||||
// Every field that could possibly exist on a remote record.
|
||||
configureEngine({
|
||||
guid_mobile: {
|
||||
clientName: "My Phone",
|
||||
tabs: [{
|
||||
urlHistory: ["http://example.com/"],
|
||||
title: "An Example",
|
||||
icon: "http://favicon",
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: "ex",
|
||||
searchParam: "enable-actions",
|
||||
matches: [ makeSearchMatch("ex", { heuristic: true }),
|
||||
makeRemoteTabMatch("http://example.com/", "My Phone",
|
||||
{ title: "An Example",
|
||||
icon: "http://favicon"
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_matches_title() {
|
||||
// URL doesn't match search expression, should still match the title.
|
||||
configureEngine({
|
||||
guid_mobile: {
|
||||
clientName: "My Phone",
|
||||
tabs: [{
|
||||
urlHistory: ["http://foo.com/"],
|
||||
title: "An Example",
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
yield check_autocomplete({
|
||||
search: "ex",
|
||||
searchParam: "enable-actions",
|
||||
matches: [ makeSearchMatch("ex", { heuristic: true }),
|
||||
makeRemoteTabMatch("http://foo.com/", "My Phone",
|
||||
{ title: "An Example" }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* test_localtab_matches_override() {
|
||||
// We have an open tab to the same page on a remote device, only "switch to
|
||||
// tab" should appear as duplicate detection removed the remote one.
|
||||
|
||||
// First setup Sync to have the page as a remote tab.
|
||||
configureEngine({
|
||||
guid_mobile: {
|
||||
clientName: "My Phone",
|
||||
tabs: [{
|
||||
urlHistory: ["http://foo.com/"],
|
||||
title: "An Example",
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
// Setup Places to think the tab is open locally.
|
||||
let uri = NetUtil.newURI("http://foo.com/");
|
||||
yield PlacesTestUtils.addVisits([
|
||||
{ uri: uri, title: "An Example" },
|
||||
]);
|
||||
addOpenPages(uri, 1);
|
||||
|
||||
yield check_autocomplete({
|
||||
search: "ex",
|
||||
searchParam: "enable-actions",
|
||||
matches: [ makeSearchMatch("ex", { heuristic: true }),
|
||||
makeSwitchToTabMatch("http://foo.com/", { title: "An Example" }),
|
||||
],
|
||||
});
|
||||
});
|
@ -30,6 +30,7 @@ support-files =
|
||||
[test_match_beginning.js]
|
||||
[test_multi_word_search.js]
|
||||
[test_queryurl.js]
|
||||
[test_remotetabmatches.js]
|
||||
[test_searchEngine_alias.js]
|
||||
[test_searchEngine_current.js]
|
||||
[test_searchEngine_host.js]
|
||||
|
@ -1719,6 +1719,10 @@ extends="chrome://global/content/bindings/popup.xml#popup">
|
||||
displayUrl = action.params.url;
|
||||
let desc = this._stringBundle.GetStringFromName("switchToTab");
|
||||
this._setUpDescription(this._action, desc, true);
|
||||
} else if (action.type == "remotetab") {
|
||||
displayUrl = action.params.url;
|
||||
let desc = action.params.deviceName;
|
||||
this._setUpDescription(this._action, desc, true);
|
||||
} else if (action.type == "searchengine") {
|
||||
emphasiseUrl = false;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user