mirror of
https://github.com/zulip/zulip.git
synced 2026-07-12 21:04:41 +08:00
redesign: Change /#settings and /#administration to an overlay.
This also adds a box-shadow to the #deactivate_self_modal so that it looks similar to the old backdrop.
This commit is contained in:
parent
7a76f3dcc8
commit
1143ed7219
@ -22,9 +22,9 @@ casper.then(function () {
|
||||
});
|
||||
|
||||
casper.then(function () {
|
||||
casper.waitUntilVisible("#settings-change-box", function () {
|
||||
casper.waitUntilVisible("#settings_content .account-settings-form", function () {
|
||||
casper.test.assertUrlMatch(/^http:\/\/[^/]+\/#settings/, 'URL suggests we are on settings page');
|
||||
casper.test.assertExists('#settings.tab-pane.active', 'Settings page is active');
|
||||
casper.test.assertVisible('.account-settings-form', 'Settings page is active');
|
||||
|
||||
casper.test.assertNotVisible("#pw_change_controls");
|
||||
|
||||
@ -56,6 +56,7 @@ casper.then(function () {
|
||||
casper.waitUntilVisible('#settings-status', function () {
|
||||
casper.test.assertSelectorHasText('#settings-status', 'Updated settings!');
|
||||
|
||||
casper.click('[data-section="your-bots"]');
|
||||
casper.click('#api_key_button');
|
||||
});
|
||||
});
|
||||
@ -196,6 +197,7 @@ casper.waitForSelector('#create_alert_word_form', function () {
|
||||
|
||||
casper.then(function change_default_language() {
|
||||
casper.test.info('Changing the default language');
|
||||
casper.click('[data-section="display-settings"]');
|
||||
casper.waitForSelector('#default_language');
|
||||
});
|
||||
|
||||
@ -236,6 +238,7 @@ casper.waitForSelector("#settings-change-box", function check_url_preference() {
|
||||
return document.documentElement.lang;
|
||||
}, 'de');
|
||||
casper.test.info("English is now the default language");
|
||||
casper.click('[data-section="display-settings"]');
|
||||
});
|
||||
|
||||
casper.thenClick('#default_language');
|
||||
|
||||
@ -17,7 +17,7 @@ function wait_for_tab(tab) {
|
||||
function then_navigate_to(click_target, tab) {
|
||||
casper.then(function () {
|
||||
casper.test.info('Visiting #' + click_target);
|
||||
casper.click('a[href^="#' + click_target + '"]');
|
||||
casper.click("a[href='#" + click_target + "']");
|
||||
wait_for_tab(tab);
|
||||
});
|
||||
}
|
||||
@ -30,7 +30,10 @@ function then_navigate_to_settings() {
|
||||
casper.click(menu_selector);
|
||||
casper.waitUntilVisible('a[href^="#settings"]', function () {
|
||||
casper.click('a[href^="#settings"]');
|
||||
wait_for_tab('settings');
|
||||
casper.waitForSelector('#settings_page', function () {
|
||||
casper.test.assertExists('#settings_page', "Settings page is active");
|
||||
casper.click("#settings_page .exit");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -53,7 +56,6 @@ function then_navigate_to_subscriptions() {
|
||||
|
||||
// Take a navigation tour of the app.
|
||||
// Entries are (click target, tab that should be active after clicking).
|
||||
|
||||
then_navigate_to_settings();
|
||||
then_navigate_to('narrow/stream/Verona', 'home');
|
||||
then_navigate_to('home', 'home');
|
||||
|
||||
@ -14,7 +14,7 @@ casper.then(function () {
|
||||
casper.click('a[href^="#administration"]');
|
||||
});
|
||||
|
||||
casper.waitForSelector('#administration.tab-pane.active', function () {
|
||||
casper.waitForSelector('#settings_overlay_container.show', function () {
|
||||
casper.test.info('Administration page is active');
|
||||
casper.test.assertUrlMatch(/^http:\/\/[^/]+\/#administration/, 'URL suggests we are on administration page');
|
||||
});
|
||||
@ -23,7 +23,6 @@ casper.waitForSelector('#administration.tab-pane.active', function () {
|
||||
casper.waitForSelector('input[type="checkbox"][id="id_realm_create_stream_by_admins_only"]', function () {
|
||||
casper.click('input[type="checkbox"][id="id_realm_create_stream_by_admins_only"]');
|
||||
casper.click('form.admin-realm-form input.button');
|
||||
|
||||
});
|
||||
|
||||
casper.then(function () {
|
||||
@ -64,14 +63,15 @@ casper.then(function () {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
casper.then(function () {
|
||||
// Test custom realm emoji
|
||||
casper.click("li[data-section='emoji-settings']");
|
||||
casper.waitForSelector('.admin-emoji-form', function () {
|
||||
casper.fill('form.admin-emoji-form', {
|
||||
name: 'MouseFace',
|
||||
url: 'http://zulipdev.com:9991/static/images/integrations/logos/jenkins.png',
|
||||
});
|
||||
casper.click('form.admin-emoji-form input.button');
|
||||
}, true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -97,6 +97,7 @@ casper.then(function () {
|
||||
|
||||
// Test custom realm filters
|
||||
casper.then(function () {
|
||||
casper.click("li[data-section='filter-settings']");
|
||||
casper.waitForSelector('.admin-filter-form', function () {
|
||||
casper.fill('form.admin-filter-form', {
|
||||
pattern: '#(?P<id>[0-9]+)',
|
||||
@ -168,10 +169,7 @@ function select_from_suggestions(item) {
|
||||
|
||||
// Test default stream creation and addition
|
||||
casper.then(function () {
|
||||
casper.click('#settings-dropdown');
|
||||
casper.click('a[href^="#subscriptions"]');
|
||||
casper.click('#settings-dropdown');
|
||||
casper.click('a[href^="#administration"]');
|
||||
casper.click("li[data-section='default-streams-list']");
|
||||
casper.waitUntilVisible(".create_default_stream", function () {
|
||||
// It matches with all the stream names which has 'O' as a substring (Rome, Scotland, Verona
|
||||
// etc). 'O' is used to make sure that it works even if there are multiple suggestions.
|
||||
@ -201,9 +199,11 @@ casper.then(function () {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// TODO: Test stream deletion
|
||||
|
||||
casper.then(function () {
|
||||
casper.click("li[data-section='organization-settings']");
|
||||
casper.waitUntilVisible('#id_realm_default_language', function () {
|
||||
casper.test.info("Changing realm default language");
|
||||
casper.evaluate(function () {
|
||||
@ -222,6 +222,7 @@ casper.then(function () {
|
||||
|
||||
// Test authentication methods setting
|
||||
casper.then(function () {
|
||||
casper.click("li[data-section='auth-methods']");
|
||||
casper.waitForSelector(".method_row[data-method='Email'] input[type='checkbox']", function () {
|
||||
casper.click(".method_row[data-method='Email'] input[type='checkbox']");
|
||||
casper.click('form.admin-realm-form input.button');
|
||||
|
||||
@ -96,7 +96,7 @@ casper.then(function () {
|
||||
|
||||
// go back to home page
|
||||
casper.then(function () {
|
||||
casper.click('.global-filter[data-name="home"]');
|
||||
casper.click('.settings-header .exit');
|
||||
});
|
||||
|
||||
// Commented out due to Issue #1243
|
||||
@ -156,7 +156,7 @@ casper.waitForSelector('input[type="checkbox"][id="id_realm_allow_message_editin
|
||||
// Commented out due to Issue #1243
|
||||
// go back home
|
||||
// casper.then(function () {
|
||||
// casper.click('.global-filter[data-name="home"]');
|
||||
// casper.click('.settings-header .exit');
|
||||
// });
|
||||
|
||||
// // save our edit
|
||||
@ -192,8 +192,8 @@ casper.waitForSelector('input[type="checkbox"][id="id_realm_allow_message_editin
|
||||
casper.then(function () {
|
||||
casper.test.info('Administration page');
|
||||
casper.click('a[href^="#administration"]');
|
||||
casper.test.assertUrlMatch(/^http:\/\/[^/]+\/#administration/, 'URL suggests we are on administration page');
|
||||
casper.test.assertExists('#administration.tab-pane.active', 'Administration page is active');
|
||||
casper.test.assertUrlMatch(/^http:\/\/[^\/]+\/#administration/, 'URL suggests we are on administration page');
|
||||
casper.test.assertExists('#settings_overlay_container.show', 'Administration page is active');
|
||||
});
|
||||
|
||||
casper.waitForSelector('form.admin-realm-form input.button');
|
||||
|
||||
@ -10,6 +10,12 @@ casper.then(function () {
|
||||
});
|
||||
});
|
||||
|
||||
casper.then(function () {
|
||||
casper.waitForSelector('#settings_overlay_container.show', function () {
|
||||
casper.click("li[data-section='user-list-admin']");
|
||||
});
|
||||
});
|
||||
|
||||
// Test user deactivation and reactivation
|
||||
casper.then(function () {
|
||||
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]', function () {
|
||||
@ -52,8 +58,8 @@ casper.then(function () {
|
||||
casper.click('#settings-dropdown');
|
||||
casper.click('a[href^="#administration"]');
|
||||
|
||||
casper.test.assertSelectorHasText("#administration a[aria-controls='deactivated-users']", "Deactivated users");
|
||||
casper.click("#administration a[aria-controls='deactivated-users']");
|
||||
casper.test.assertSelectorHasText("li[data-section='deactivated-users-admin']", "Deactivated users");
|
||||
casper.click("li[data-section='deactivated-users-admin']");
|
||||
|
||||
|
||||
casper.waitForSelector('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"] .reactivate', function () {
|
||||
@ -66,13 +72,15 @@ casper.then(function () {
|
||||
casper.waitForSelector('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"] button:not(.reactivate)', function () {
|
||||
casper.test.assertSelectorHasText('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
|
||||
});
|
||||
});
|
||||
|
||||
casper.test.assertSelectorHasText("#administration a[aria-controls='organization']", "Organization");
|
||||
casper.click("#administration a[aria-controls='organization']");
|
||||
// Test bot deactivation and reactivation
|
||||
casper.then(function () {
|
||||
casper.test.assertSelectorHasText("li[data-section='organization-settings']", "Organization settings");
|
||||
casper.click("li[data-section='bot-list-admin']");
|
||||
});
|
||||
|
||||
casper.then(function () {
|
||||
// Test bot deactivation and reactivation
|
||||
casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"]', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Deactivate');
|
||||
casper.click('.user_row[id="user_new-user-bot@zulip.com"] .deactivate');
|
||||
|
||||
@ -12,6 +12,8 @@ exports.show_or_hide_menu_item = function () {
|
||||
item.show();
|
||||
} else {
|
||||
item.hide();
|
||||
$(".ind-tab[data-name='admin']").addClass("disabled");
|
||||
$(".settings-list li.admin").hide();
|
||||
}
|
||||
};
|
||||
|
||||
@ -136,11 +138,8 @@ function get_non_default_streams_names(streams_data) {
|
||||
}
|
||||
|
||||
exports.update_default_streams_table = function () {
|
||||
if (!meta.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#administration').hasClass('active')) {
|
||||
if (/#*administration/.test(window.location.hash) ||
|
||||
/#*settings/.test(window.location.hash)) {
|
||||
$("#admin_default_streams_table").expectOne().find("tr.default_stream_row").remove();
|
||||
populate_default_streams(page_params.realm_default_streams);
|
||||
}
|
||||
@ -270,8 +269,9 @@ function _setup_page() {
|
||||
realm_default_language: page_params.realm_default_language,
|
||||
realm_waiting_period_threshold: page_params.realm_waiting_period_threshold,
|
||||
};
|
||||
|
||||
var admin_tab = templates.render('admin_tab', options);
|
||||
$("#administration").html(admin_tab);
|
||||
$("#settings_content .administration-box").html(admin_tab);
|
||||
$("#administration-status").expectOne().hide();
|
||||
$("#admin-realm-name-status").expectOne().hide();
|
||||
$("#admin-realm-restricted-to-domain-status").expectOne().hide();
|
||||
@ -288,6 +288,20 @@ function _setup_page() {
|
||||
$('#admin-filter-pattern-status').expectOne().hide();
|
||||
$('#admin-filter-format-status').expectOne().hide();
|
||||
|
||||
var tab = (function () {
|
||||
var tab = false;
|
||||
var hash_sequence = window.location.hash.split(/\//);
|
||||
if (/#*(administration)/.test(hash_sequence[0])) {
|
||||
tab = hash_sequence[1];
|
||||
return tab || "organization-settings";
|
||||
}
|
||||
return tab;
|
||||
}());
|
||||
|
||||
if (tab) {
|
||||
exports.launch_page(tab);
|
||||
}
|
||||
|
||||
$("#id_realm_default_language").val(page_params.realm_default_language);
|
||||
|
||||
// create loading indicators
|
||||
@ -1014,6 +1028,17 @@ function _setup_page() {
|
||||
|
||||
}
|
||||
|
||||
exports.launch_page = function (tab) {
|
||||
var $active_tab = $("#settings_overlay_container li[data-section='" + tab + "']");
|
||||
|
||||
if ($active_tab.hasClass("admin")) {
|
||||
$(".sidebar .ind-tab[data-name='admin']").click();
|
||||
}
|
||||
|
||||
$("#settings_overlay_container").addClass("show");
|
||||
$active_tab.click();
|
||||
};
|
||||
|
||||
exports.setup_page = function () {
|
||||
i18n.ensure_i18n(_setup_page);
|
||||
};
|
||||
|
||||
@ -294,9 +294,10 @@ $(function () {
|
||||
popovers.register_click_handlers();
|
||||
notifications.register_click_handlers();
|
||||
|
||||
$('.logout_button').click(function () {
|
||||
$('body').on('click', '.logout_button', function () {
|
||||
$('#logout_form').submit();
|
||||
});
|
||||
|
||||
$('.restart_get_events_button').click(function () {
|
||||
server_events.restart_get_events({dont_block: true});
|
||||
});
|
||||
@ -558,6 +559,58 @@ $(function () {
|
||||
$('a.dropdown-toggle, .dropdown-menu a').on('touchstart', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
$("#settings_overlay_container .sidebar").on("click", "li[data-section]", function () {
|
||||
var $this = $(this);
|
||||
|
||||
$("#settings_overlay_container .sidebar li").removeClass("active no-border");
|
||||
$this.addClass("active");
|
||||
$this.prev().addClass("no-border");
|
||||
});
|
||||
|
||||
$("#settings_overlay_container .sidebar").on("click", "li[data-section]", function () {
|
||||
var $this = $(this);
|
||||
var section = $this.data("section");
|
||||
var sel = "[data-name='" + section + "']";
|
||||
|
||||
$("#settings_overlay_container .sidebar li").removeClass("active no-border");
|
||||
$this.addClass("active");
|
||||
$this.prev().addClass("no-border");
|
||||
|
||||
if ($this.hasClass("admin")) {
|
||||
window.location.hash = "administration/" + section;
|
||||
} else {
|
||||
window.location.hash = "settings/" + section;
|
||||
}
|
||||
|
||||
$(".settings-section, .settings-wrapper").removeClass("show");
|
||||
$(".settings-section" + sel + ", .settings-wrapper" + sel).addClass("show");
|
||||
});
|
||||
|
||||
$("#settings_overlay_container").on("click", function (e) {
|
||||
var $target = $(e.target);
|
||||
if ($target.is(".exit-sign, .exit")) {
|
||||
hashchange.exit_settings();
|
||||
}
|
||||
});
|
||||
|
||||
(function () {
|
||||
var $parent = $("#settings_overlay_container .sidebar .tab-switcher");
|
||||
var $tabs = $parent.find(".ind-tab");
|
||||
$tabs.click(function () {
|
||||
$tabs.removeClass("selected");
|
||||
$(this).addClass("selected");
|
||||
|
||||
$(".sidebar li").hide();
|
||||
if ($(this).data("name") === "admin") {
|
||||
$("li.admin").show();
|
||||
$("li[data-section='organization-settings']").click();
|
||||
} else {
|
||||
$("li:not(.admin)").show();
|
||||
$("li[data-section='your-account']").click();
|
||||
}
|
||||
});
|
||||
}());
|
||||
});
|
||||
|
||||
return exports;
|
||||
|
||||
@ -48,12 +48,6 @@ exports.initialize = function () {
|
||||
|
||||
// The admin and settings pages are generated client-side through
|
||||
// templates.
|
||||
|
||||
var admin_link = $('#gear-menu a[href="#administration"]');
|
||||
admin_link.on('shown', admin.setup_page);
|
||||
|
||||
var settings_link = $('#gear-menu a[href="#settings"]');
|
||||
settings_link.on('shown', settings.setup_page);
|
||||
};
|
||||
|
||||
return exports;
|
||||
|
||||
@ -199,9 +199,9 @@ function do_hashchange(from_reload) {
|
||||
// When going from a normal view (eg. `narrow/is/private`) to a settings panel
|
||||
// (eg. `settings/your-bots`) it should trigger the `should_ignore` function and
|
||||
// return `true` for the current state -- we want to ignore hash changes from
|
||||
// within the settings page, as they will be handled by the settings page itself.
|
||||
//
|
||||
// There is then an `exit_settings` function that allows the hash to change exactly
|
||||
// within the settings page. The previous hash however should return `false` as it
|
||||
// was outside of the scope of settings.
|
||||
// there is then an `exit_settings` function that allows the hash to change exactly
|
||||
// once without triggering any events. This allows the hash to reset back from
|
||||
// a settings page to the previous view available before the settings page
|
||||
// (eg. narrow/is/private). This saves the state, scroll position, and makes the
|
||||
@ -211,24 +211,44 @@ var ignore = {
|
||||
flag: false,
|
||||
prev: null,
|
||||
old_hash: typeof window !== "undefined" ? window.location.hash : "#",
|
||||
group: null,
|
||||
};
|
||||
|
||||
function get_main_hash(hash) {
|
||||
return hash.replace(/^#/, "").split(/\//)[0];
|
||||
return hash ? hash.replace(/^#/, "").split(/\//)[0] : "";
|
||||
}
|
||||
|
||||
// different groups require different reloads. The grouped elements don't
|
||||
// require a reload or overlay change to run.
|
||||
var get_hash_group = (function () {
|
||||
var groups = [
|
||||
["subscriptions"],
|
||||
["settings", "administration"],
|
||||
];
|
||||
|
||||
return function (value) {
|
||||
var idx = null;
|
||||
|
||||
_.find(groups, function (o, i) {
|
||||
if (o.indexOf(value) !== -1) {
|
||||
idx = i;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return idx;
|
||||
};
|
||||
}());
|
||||
|
||||
function should_ignore(hash) {
|
||||
// an array of hashes to ignore (eg. ["subscriptions", "settings", "administration"]).
|
||||
var ignore_list = ["subscriptions"];
|
||||
var ignore_list = ["subscriptions", "settings", "administration"];
|
||||
var main_hash = get_main_hash(hash);
|
||||
|
||||
return (ignore_list.indexOf(main_hash) > -1);
|
||||
}
|
||||
|
||||
function hide_overlays() {
|
||||
subs.close();
|
||||
}
|
||||
|
||||
function hashchanged(from_reload, e) {
|
||||
var old_hash;
|
||||
if (e) {
|
||||
@ -238,16 +258,32 @@ function hashchanged(from_reload, e) {
|
||||
}
|
||||
|
||||
var base = get_main_hash(window.location.hash);
|
||||
|
||||
if (should_ignore(window.location.hash)) {
|
||||
if (!should_ignore(old_hash || "#")) {
|
||||
if (base === "subscriptions") {
|
||||
subs.launch();
|
||||
// if the old has was a standard non-ignore hash OR the ignore hash
|
||||
// base has changed, something needs to run again.
|
||||
|
||||
if (!should_ignore(old_hash || "#") || ignore.group !== get_hash_group(base)) {
|
||||
if (ignore.group !== get_hash_group(base)) {
|
||||
exports.close_modals();
|
||||
}
|
||||
|
||||
ignore.prev = old_hash;
|
||||
// now only if the previous one should not have been ignored.
|
||||
if (!should_ignore(old_hash || "#")) {
|
||||
ignore.prev = old_hash;
|
||||
}
|
||||
|
||||
if (base === "subscriptions") {
|
||||
subs.launch();
|
||||
} else if (/settings|administration/.test(base)) {
|
||||
settings.setup_page();
|
||||
admin.setup_page();
|
||||
}
|
||||
|
||||
ignore.group = get_hash_group(base);
|
||||
}
|
||||
} else if (!should_ignore(window.location.hash) && !ignore.flag) {
|
||||
hide_overlays();
|
||||
exports.close_modals();
|
||||
changing_hash = true;
|
||||
var ret = do_hashchange(from_reload);
|
||||
changing_hash = false;
|
||||
@ -270,6 +306,10 @@ exports.initialize = function () {
|
||||
hashchanged(true);
|
||||
};
|
||||
|
||||
exports.close_modals = function () {
|
||||
$("[data-overlay]").removeClass("show");
|
||||
};
|
||||
|
||||
exports.exit_settings = function (callback) {
|
||||
if (should_ignore(window.location.hash)) {
|
||||
ui.blur_active_element();
|
||||
@ -278,6 +318,8 @@ exports.exit_settings = function (callback) {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
|
||||
exports.close_modals();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -7,6 +7,10 @@ function do_narrow_action(action) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function is_settings_page() {
|
||||
return (/^#*(settings|administration)/g).test(window.location.hash);
|
||||
}
|
||||
|
||||
var actions_dropdown_hotkeys = [
|
||||
'down_arrow',
|
||||
'up_arrow',
|
||||
@ -213,6 +217,28 @@ function process_hotkey(e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (is_settings_page()) {
|
||||
if (event_name === 'up_arrow') {
|
||||
var prev = e.target.previousElementSibling;
|
||||
|
||||
if ($(prev).css("display") !== "none") {
|
||||
$(prev).focus().click();
|
||||
}
|
||||
return true;
|
||||
} else if (event_name === 'down_arrow') {
|
||||
var next = e.target.nextElementSibling;
|
||||
|
||||
if ($(next).css("display") !== "none") {
|
||||
$(next).focus().click();
|
||||
}
|
||||
return true;
|
||||
} else if (event_name === 'escape') {
|
||||
$("#settings_overlay_container .exit").click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process hotkeys specially when in an input, select, textarea, or send button
|
||||
if ($('input:focus,select:focus,textarea:focus,#compose-send-button:focus').length > 0) {
|
||||
if (event_name === 'escape') {
|
||||
@ -256,7 +282,10 @@ function process_hotkey(e) {
|
||||
}
|
||||
|
||||
if (event_name === 'enter') {
|
||||
if (activity.searching()) {
|
||||
if (is_settings_page()) {
|
||||
$(e.target).click();
|
||||
return true;
|
||||
} else if (activity.searching()) {
|
||||
activity.blur_search();
|
||||
return true;
|
||||
} else if (stream_list.searching()) {
|
||||
@ -353,11 +382,17 @@ function process_hotkey(e) {
|
||||
navigate.to_end();
|
||||
return true;
|
||||
case 'page_up':
|
||||
navigate.page_up();
|
||||
return true;
|
||||
if (!is_settings_page()) {
|
||||
navigate.page_up();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'page_down':
|
||||
navigate.page_down();
|
||||
return true;
|
||||
if (!is_settings_page()) {
|
||||
navigate.page_down();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Shortcuts that operate on a message
|
||||
|
||||
@ -149,6 +149,17 @@ function _setup_page() {
|
||||
// at page load. This promise will be resolved with a list of streams after
|
||||
// the first settings page load. build_stream_list then adds a callback to
|
||||
// the promise, which in most cases will already be resolved.
|
||||
|
||||
var tab = (function () {
|
||||
var tab = false;
|
||||
var hash_sequence = window.location.hash.split(/\//);
|
||||
if (/#*(settings)/.test(hash_sequence[0])) {
|
||||
tab = hash_sequence[1];
|
||||
return tab || "your-account";
|
||||
}
|
||||
return tab;
|
||||
}());
|
||||
|
||||
if (_streams_deferred.state() !== "resolved") {
|
||||
channel.get({
|
||||
url: '/json/streams',
|
||||
@ -173,7 +184,7 @@ function _setup_page() {
|
||||
zuliprc: 'zuliprc',
|
||||
});
|
||||
|
||||
$("#settings").html(settings_tab);
|
||||
$(".settings-box").html(settings_tab);
|
||||
$("#settings-status").hide();
|
||||
$("#notify-settings-status").hide();
|
||||
$("#display-settings-status").hide();
|
||||
@ -186,6 +197,10 @@ function _setup_page() {
|
||||
$("#show_api_key_box").hide();
|
||||
$("#api_key_button_box").show();
|
||||
|
||||
if (tab) {
|
||||
exports.launch_page(tab);
|
||||
}
|
||||
|
||||
function clear_password_change() {
|
||||
// Clear the password boxes so that passwords don't linger in the DOM
|
||||
// for an XSS attacker to find.
|
||||
@ -463,10 +478,14 @@ function _setup_page() {
|
||||
});
|
||||
});
|
||||
|
||||
$("#default_language_modal [data-dismiss]").click(function () {
|
||||
$("#default_language_modal").fadeOut(300);
|
||||
});
|
||||
|
||||
$("#default_language_modal .language").click(function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$('#default_language_modal').modal('hide');
|
||||
$('#default_language_modal').fadeOut(300);
|
||||
|
||||
var data = {};
|
||||
var $link = $(e.target).closest("a[data-code]");
|
||||
@ -495,7 +514,7 @@ function _setup_page() {
|
||||
$('#default_language').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$('#default_language_modal').modal('show');
|
||||
$('#default_language_modal').show().attr('aria-hidden', false);
|
||||
});
|
||||
|
||||
$("#user_deactivate_account_button").on('click', function (e) {
|
||||
@ -835,6 +854,17 @@ exports.update_page = function () {
|
||||
i18n.ensure_i18n(_update_page);
|
||||
};
|
||||
|
||||
exports.launch_page = function (tab) {
|
||||
var $active_tab = $("#settings_overlay_container li[data-section='" + tab + "']");
|
||||
|
||||
if (!$active_tab.hasClass("admin")) {
|
||||
$(".sidebar .ind-tab[data-name='settings']").click();
|
||||
}
|
||||
|
||||
$("#settings_overlay_container").addClass("show");
|
||||
$active_tab.click();
|
||||
};
|
||||
|
||||
return exports;
|
||||
}());
|
||||
|
||||
|
||||
@ -111,6 +111,7 @@ td .button {
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: none;
|
||||
background-color: #fafafa;
|
||||
border-radius: 2px;
|
||||
margin: 20px;
|
||||
@ -119,6 +120,20 @@ td .button {
|
||||
border-top: 5px solid #dfdfdf;
|
||||
}
|
||||
|
||||
#administration .settings-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-section.show,
|
||||
.settings-wrapper.show,
|
||||
.settings-wrapper.show .settings-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-section .settings-section-title {
|
||||
font-size: 1.2em;
|
||||
font-weight: 300;
|
||||
@ -485,6 +500,247 @@ input[type=checkbox].inline-block {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
/* -- new settings overlay -- */
|
||||
#settings_overlay_container {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
|
||||
background-color: rgba(32,32,32,0.8);
|
||||
z-index: 105;
|
||||
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#settings_overlay_container.show {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#settings_overlay_container.show #settings_page {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
#settings_page {
|
||||
-webkit-transform: scale(0.5);
|
||||
transform: scale(0.5);
|
||||
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
z-index: 102;
|
||||
min-height: 550px;
|
||||
}
|
||||
|
||||
#settings_page {
|
||||
height: 95vh;
|
||||
width: 97vw;
|
||||
margin: 2.5vh 1.5vw;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#settings_page .sidebar {
|
||||
float: left;
|
||||
position: relative;
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
min-height: 550px;
|
||||
overflow-y: auto;
|
||||
border-top-left-radius: 4px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
#settings_page .sidebar .tab-container {
|
||||
background-color: #fff;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#settings_page .sidebar .settings-list {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#settings_page .content-wrapper {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: calc(100% - 250px - 1px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#settings_page .content-wrapper .settings-header {
|
||||
width: 100%;
|
||||
height: 43px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#settings_page .content-wrapper #settings_content {
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
|
||||
float: left;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#settings_page .sidebar .header {
|
||||
height: auto;
|
||||
position: relative;
|
||||
width: calc(100% - 20px);
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
|
||||
background-color: #edefef;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
#settings_page .settings-header {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
#settings_page .settings-header h1 {
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
margin: 15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#settings_page .settings-header .exit {
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
color: #AAA;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#settings_page .settings-header .exit-sign {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-left: 3px;
|
||||
font-size: 1.7em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
#deactivation_user_modal.fade.in {
|
||||
top: calc(50% - 120px);
|
||||
}
|
||||
|
||||
#deactivate_self_modal {
|
||||
box-shadow: 0px 0px 75px rgba(0,0,0,0.5);
|
||||
outline: 10000px solid rgba(0,0,0,0.3);
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
input[type=text]#settings_search {
|
||||
width: calc(100% - 10px - 2px);
|
||||
margin: 0px;
|
||||
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
padding: 3px 5px;
|
||||
outline: none;
|
||||
border: 1px solid #DDD;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input[type=text]:focus#settings_search {
|
||||
box-shadow: none;
|
||||
border: 1px solid #BBB;
|
||||
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
input[type=text]#settings_search {
|
||||
width: calc(100% - 10px - 2px);
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#settings_page .sidebar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li {
|
||||
padding: 5px 0px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li.no-border {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li.active {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li.active {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li .text,
|
||||
#settings_page .sidebar li .icon {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li .text {
|
||||
width: calc(100% - 55px);
|
||||
padding: 10px 12px 10px 0px;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li .icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 10px 10px;
|
||||
|
||||
font-size: 1.4em;
|
||||
color: #888;
|
||||
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li.admin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li.admin .icon {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#settings_page .sidebar li:last-of-type .text {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#settings_page .sidebar .sidebar-bottom-anchor {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
}
|
||||
/* -- end new settings overlay -- */
|
||||
|
||||
@media (max-width: 1215px) {
|
||||
.user-avatar-section {
|
||||
float: none;
|
||||
@ -505,6 +761,12 @@ input[type=checkbox].inline-block {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 953px) {
|
||||
#settings_content .warning {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#show_api_key_box {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
@ -1962,11 +1962,6 @@ div.floating_recipient {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.administration {
|
||||
margin-top: 55px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.conversation-partners {
|
||||
display: block;
|
||||
line-height: 1.2em;
|
||||
|
||||
@ -1,55 +1,23 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<div class="administration">
|
||||
<div class="alert" id="administration-status"></div>
|
||||
<h1><i class="icon-vector-bolt administration-icon"></i>{{t "Administration" }}</h1>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#organization" aria-controls="organization" role="tab" data-toggle="tab"><i class="icon-vector-gear settings-section-icon"></i> {{t "Organization settings" }}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#users" aria-controls="users" role="tab" data-toggle="tab"><i class="icon-vector-user settings-section-icon"></i> {{t "Users" }}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#deactivated-users" aria-controls="deactivated-users" role="tab" data-toggle="tab"><i class="icon-vector-trash settings-section-icon"></i> {{t "Deactivated users" }}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#bots" aria-controls="bots" role="tab" data-toggle="tab"><i class="icon-vector-github settings-section-icon"></i> {{t "Bots" }}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#streams" aria-controls="streams" role="tab" data-toggle="tab"><i class="icon-vector-exchange settings-section-icon"></i> {{t "Delete streams" }}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#default-streams" aria-controls="default-streams" role="tab" data-toggle="tab"><i class="icon-vector-exchange settings-section-icon"></i> {{t "Default streams" }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="organization">
|
||||
{{ partial "organization-settings-admin" }}
|
||||
{{ partial "emoji-settings-admin" }}
|
||||
{{ partial "realm-filter-settings-admin" }}
|
||||
{{ partial "auth-methods-settings-admin" }}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="users">
|
||||
{{ partial "user-list-admin" }}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="deactivated-users">
|
||||
{{ partial "deactivated-users-admin" }}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="bots">
|
||||
{{ partial "bot-list-admin" }}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="streams">
|
||||
{{ partial "streams-list-admin" }}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="default-streams">
|
||||
{{ partial "default-streams-list-admin" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert" id="administration-status"></div>
|
||||
|
||||
{{ partial "deactivation-user-modal" }}
|
||||
{{ partial "deactivation-stream-modal" }}
|
||||
{{ partial "realm-domains-modal" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ partial "deactivation-user-modal" }}
|
||||
{{ partial "deactivation-stream-modal" }}
|
||||
{{ partial "realm-domains-modal" }}
|
||||
|
||||
{{ partial "organization-settings-admin" }}
|
||||
|
||||
{{ partial "emoji-settings-admin" }}
|
||||
|
||||
{{ partial "user-list-admin" }}
|
||||
|
||||
{{ partial "deactivated-users-admin" }}
|
||||
|
||||
{{ partial "bot-list-admin" }}
|
||||
|
||||
{{ partial "streams-list-admin" }}
|
||||
|
||||
{{ partial "default-streams-list-admin" }}
|
||||
|
||||
{{ partial "auth-methods-settings-admin" }}
|
||||
|
||||
{{ partial "realm-filter-settings-admin" }}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
<h1><i class="icon-vector-wrench settings-icon"></i>{{t "Settings" }}</h1>
|
||||
|
||||
<div id="settings-change-box" class="new-style">
|
||||
<div class="alert" id="settings-status"></div>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div id="subscription_overlay" class="new-style">
|
||||
<div id="subscription_overlay" class="new-style" data-overlay="subscriptions">
|
||||
<div class="flex">
|
||||
<div class="subscriptions-container">
|
||||
<div class="subscriptions-header">
|
||||
|
||||
2
static/third/bootstrap/css/bootstrap.css
vendored
2
static/third/bootstrap/css/bootstrap.css
vendored
@ -3774,7 +3774,7 @@ button.close {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1040;
|
||||
z-index: 101;
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
|
||||
@ -60,6 +60,9 @@ var page_params = {{ page_params }};
|
||||
<div id="right-screen" class="screen"></div>
|
||||
<div id="clear-screen" class="screen"></div>
|
||||
|
||||
<div id="settings_overlay_container" data-overlay="settings">
|
||||
{% include "zerver/settings_overlay.html" %}
|
||||
</div>
|
||||
{% include "zerver/navbar.html" %}
|
||||
|
||||
<div class="fixed-app">
|
||||
|
||||
@ -55,12 +55,12 @@
|
||||
</a>
|
||||
</li>
|
||||
<li title="{{ _('Settings') }}">
|
||||
<a href="#settings" data-toggle="tab">
|
||||
<a href="#settings">
|
||||
<i class="icon-vector-wrench"></i> {{ _('Settings') }}
|
||||
</a>
|
||||
</li>
|
||||
<li title="{{ _('Administration') }}" class="admin-menu-item">
|
||||
<a href="#administration" role="button" data-toggle="tab">
|
||||
<a href="#administration" role="button">
|
||||
<i class="icon-vector-bolt"></i> {{ _('Administration') }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
100
templates/zerver/settings_overlay.html
Normal file
100
templates/zerver/settings_overlay.html
Normal file
@ -0,0 +1,100 @@
|
||||
<div id="settings_page" class="new-style">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-list dark-grey small-text">
|
||||
<div class="center tab-container">
|
||||
<div class="tab-switcher">
|
||||
<div class="ind-tab first selected" data-name="settings">{{ _('Settings') }}</div>
|
||||
<div class="ind-tab second" data-name="admin">{{ _('Administration') }} </div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="settings-list">
|
||||
<li tabindex="1" class="active" data-section="your-account">
|
||||
<div class="icon icon-vector-user"></div>
|
||||
<div class="text">{{ _('Your account') }}</div>
|
||||
</li>
|
||||
<li tabindex="1" data-section="display-settings">
|
||||
<div class="icon icon-vector-time"></div>
|
||||
<div class="text">{{ _('Display settings') }}</div>
|
||||
</li>
|
||||
<li tabindex="1" data-section="notifications">
|
||||
<div class="icon icon-vector-warning-sign"></div>
|
||||
<div class="text">{{ _('Notifications') }}</div>
|
||||
</li>
|
||||
<li tabindex="1" data-section="your-bots">
|
||||
<div class="icon icon-vector-github"></div>
|
||||
<div class="text">{{ _('Your bots') }}</div>
|
||||
</li>
|
||||
<li tabindex="1" data-section="custom-alert-words">
|
||||
<div class="icon icon-vector-book"></div>
|
||||
<div class="text">{{ _('Custom alert words') }}</div>
|
||||
</li>
|
||||
<li tabindex="1" data-section="zulip-labs">
|
||||
<i class="icon icon-vector-beaker"></i>
|
||||
<div class="text">{{ _('Zulip labs') }}</div>
|
||||
</li>
|
||||
|
||||
<li class="admin" tabindex="1" data-section="organization-settings">
|
||||
<i class="icon icon-vector-beaker"></i>
|
||||
<div class="text">{{ _('Organization settings') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="emoji-settings">
|
||||
<i class="icon icon-vector-smile"></i>
|
||||
<div class="text">{{ _('Custom emoji') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="auth-methods">
|
||||
<i class="icon icon-vector-lock"></i>
|
||||
<div class="text">{{ _('Authentication methods') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="user-list-admin">
|
||||
<i class="icon icon-vector-user"></i>
|
||||
<div class="text">{{ _('Users') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="deactivated-users-admin">
|
||||
<i class="icon icon-vector-trash"></i>
|
||||
<div class="text">{{ _('Deactivated users') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="bot-list-admin">
|
||||
<i class="icon icon-vector-github"></i>
|
||||
<div class="text">{{ _('Bots') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="streams-list-admin">
|
||||
<i class="icon icon-vector-exchange"></i>
|
||||
<div class="text">{{ _('Delete streams') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="default-streams-list">
|
||||
<i class="icon icon-vector-exchange"></i>
|
||||
<div class="text">{{ _('Default streams') }}</div>
|
||||
</li>
|
||||
<li class="admin" tabindex="1" data-section="filter-settings">
|
||||
<i class="icon icon-vector-font"></i>
|
||||
<div class="text">{{ _('Filter settings') }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-bottom-anchor dark-grey small-text">
|
||||
<ul>
|
||||
<li>
|
||||
<div class="icon icon-vector-off"></div>
|
||||
<a href="#logout" class="no-style logout_button"><div class="text">Log Out</div></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="settings-header">
|
||||
<h1>{{ _('Settings') }}</h1>
|
||||
<div class="exit">
|
||||
<span class="exit-sign">×</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settings_content">
|
||||
<div class="administration-box administration">
|
||||
|
||||
</div>
|
||||
<div class="settings-box">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -18,6 +18,7 @@ GENERIC_KEYWORDS = [
|
||||
'show',
|
||||
'notdisplayed',
|
||||
'popover',
|
||||
'no-border',
|
||||
'success',
|
||||
'text-error',
|
||||
'warning',
|
||||
@ -25,6 +26,7 @@ GENERIC_KEYWORDS = [
|
||||
'zoom-out',
|
||||
'first',
|
||||
'second',
|
||||
'selected',
|
||||
]
|
||||
|
||||
def raise_error(fn, i, line):
|
||||
|
||||
@ -72,6 +72,7 @@ class TemplateTestCase(ZulipTestCase):
|
||||
'zerver/navbar.html',
|
||||
'zerver/right-sidebar.html',
|
||||
'zerver/search_operators.html',
|
||||
'zerver/settings_overlay.html',
|
||||
'zerver/stream_creation_prompt.html',
|
||||
'zerver/subscriptions.html',
|
||||
'zerver/tutorial_finale.html',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user