mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 16:14:02 +00:00
stream_settings: Display archived channels.
By default, archived channels will be hidden.
This commit is contained in:
@@ -256,18 +256,13 @@ export function validate_channels_settings_hash(hash: string): string {
|
|||||||
const stream_id = Number.parseInt(section, 10);
|
const stream_id = Number.parseInt(section, 10);
|
||||||
const sub = sub_store.get(stream_id);
|
const sub = sub_store.get(stream_id);
|
||||||
// There are a few situations where we can't display stream settings:
|
// There are a few situations where we can't display stream settings:
|
||||||
// 1. This is a stream that's been archived. (sub.is_archived=true)
|
// 1. The stream ID is invalid. (sub=undefined)
|
||||||
// 2. The stream ID is invalid. (sub=undefined)
|
// 2. The current user is a guest, and was unsubscribed from the stream
|
||||||
// 3. The current user is a guest, and was unsubscribed from the stream
|
|
||||||
// stream in the current session. (In future sessions, the stream will
|
// stream in the current session. (In future sessions, the stream will
|
||||||
// not be in sub_store).
|
// not be in sub_store).
|
||||||
//
|
//
|
||||||
// In all these cases we redirect the user to 'subscribed' tab.
|
// In both cases we redirect the user to 'subscribed' tab.
|
||||||
if (
|
if (sub === undefined || (page_params.is_guest && !stream_data.is_subscribed(stream_id))) {
|
||||||
sub === undefined ||
|
|
||||||
sub.is_archived ||
|
|
||||||
(page_params.is_guest && !stream_data.is_subscribed(stream_id))
|
|
||||||
) {
|
|
||||||
return channels_settings_section_url();
|
return channels_settings_section_url();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -355,6 +355,10 @@ export function subscribed_stream_ids(): number[] {
|
|||||||
return subscribed_subs().map((sub) => sub.stream_id);
|
return subscribed_subs().map((sub) => sub.stream_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function get_archived_subs(): StreamSubscription[] {
|
||||||
|
return [...stream_info.values()].filter((sub) => sub.is_archived);
|
||||||
|
}
|
||||||
|
|
||||||
export function muted_stream_ids(): number[] {
|
export function muted_stream_ids(): number[] {
|
||||||
return subscribed_subs()
|
return subscribed_subs()
|
||||||
.filter((sub) => sub.is_muted)
|
.filter((sub) => sub.is_muted)
|
||||||
|
@@ -28,6 +28,12 @@ export type SettingsSubscription = StreamSubscription & {
|
|||||||
subscriber_count: number;
|
subscriber_count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FILTERS = {
|
||||||
|
ALL_CHANNELS: "all_channels",
|
||||||
|
NON_ARCHIVED_CHANNELS: "non_archived_channels",
|
||||||
|
ARCHIVED_CHANNELS: "archived_channels",
|
||||||
|
};
|
||||||
|
|
||||||
export function get_sub_for_settings(sub: StreamSubscription): SettingsSubscription {
|
export function get_sub_for_settings(sub: StreamSubscription): SettingsSubscription {
|
||||||
return {
|
return {
|
||||||
...sub,
|
...sub,
|
||||||
@@ -66,7 +72,7 @@ function get_subs_for_settings(subs: StreamSubscription[]): SettingsSubscription
|
|||||||
// delegating, so that we can more efficiently compute subscriber counts
|
// delegating, so that we can more efficiently compute subscriber counts
|
||||||
// (in bulk). If that plan appears to have been aborted, feel free to
|
// (in bulk). If that plan appears to have been aborted, feel free to
|
||||||
// inline this.
|
// inline this.
|
||||||
return subs.filter((sub) => !sub.is_archived).map((sub) => get_sub_for_settings(sub));
|
return subs.map((sub) => get_sub_for_settings(sub));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_updated_unsorted_subs(): SettingsSubscription[] {
|
export function get_updated_unsorted_subs(): SettingsSubscription[] {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import assert from "minimalistic-assert";
|
import assert from "minimalistic-assert";
|
||||||
|
import type * as tippy from "tippy.js";
|
||||||
|
|
||||||
import render_stream_creation_confirmation_banner from "../templates/modal_banner/stream_creation_confirmation_banner.hbs";
|
import render_stream_creation_confirmation_banner from "../templates/modal_banner/stream_creation_confirmation_banner.hbs";
|
||||||
import render_stream_info_banner from "../templates/modal_banner/stream_info_banner.hbs";
|
import render_stream_info_banner from "../templates/modal_banner/stream_info_banner.hbs";
|
||||||
@@ -15,6 +16,7 @@ import type {Toggle} from "./components.ts";
|
|||||||
import * as compose_banner from "./compose_banner.ts";
|
import * as compose_banner from "./compose_banner.ts";
|
||||||
import * as compose_recipient from "./compose_recipient.ts";
|
import * as compose_recipient from "./compose_recipient.ts";
|
||||||
import * as compose_state from "./compose_state.ts";
|
import * as compose_state from "./compose_state.ts";
|
||||||
|
import * as dropdown_widget from "./dropdown_widget.ts";
|
||||||
import * as hash_parser from "./hash_parser.ts";
|
import * as hash_parser from "./hash_parser.ts";
|
||||||
import * as hash_util from "./hash_util.ts";
|
import * as hash_util from "./hash_util.ts";
|
||||||
import {$t} from "./i18n.ts";
|
import {$t} from "./i18n.ts";
|
||||||
@@ -46,6 +48,10 @@ import * as stream_ui_updates from "./stream_ui_updates.ts";
|
|||||||
import * as sub_store from "./sub_store.ts";
|
import * as sub_store from "./sub_store.ts";
|
||||||
import type {StreamSubscription} from "./sub_store.ts";
|
import type {StreamSubscription} from "./sub_store.ts";
|
||||||
import * as util from "./util.ts";
|
import * as util from "./util.ts";
|
||||||
|
import * as views_util from "./views_util.ts";
|
||||||
|
|
||||||
|
let archived_status_dropdown_filter: string;
|
||||||
|
let filters_dropdown_widget: dropdown_widget.DropdownWidget;
|
||||||
|
|
||||||
export function is_sub_already_present(sub: StreamSubscription): boolean {
|
export function is_sub_already_present(sub: StreamSubscription): boolean {
|
||||||
return stream_ui_updates.row_for_stream_id(sub.stream_id).length > 0;
|
return stream_ui_updates.row_for_stream_id(sub.stream_id).length > 0;
|
||||||
@@ -384,6 +390,20 @@ export function update_settings_for_unsubscribed(slim_sub: StreamSubscription):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function triage_stream(left_panel_params: LeftPanelParams, sub: StreamSubscription): string {
|
function triage_stream(left_panel_params: LeftPanelParams, sub: StreamSubscription): string {
|
||||||
|
const current_channel_visibility_filter = archived_status_dropdown_filter;
|
||||||
|
const channel_visibility_filters = stream_settings_data.FILTERS;
|
||||||
|
if (
|
||||||
|
current_channel_visibility_filter === channel_visibility_filters.NON_ARCHIVED_CHANNELS &&
|
||||||
|
sub.is_archived
|
||||||
|
) {
|
||||||
|
return "rejected";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
current_channel_visibility_filter === channel_visibility_filters.ARCHIVED_CHANNELS &&
|
||||||
|
!sub.is_archived
|
||||||
|
) {
|
||||||
|
return "rejected";
|
||||||
|
}
|
||||||
if (left_panel_params.show_subscribed && !sub.subscribed) {
|
if (left_panel_params.show_subscribed && !sub.subscribed) {
|
||||||
// reject non-subscribed streams
|
// reject non-subscribed streams
|
||||||
return "rejected";
|
return "rejected";
|
||||||
@@ -485,12 +505,15 @@ export function update_empty_left_panel_message(): void {
|
|||||||
has_streams = stream_data.get_unsorted_subs().length;
|
has_streams = stream_data.get_unsorted_subs().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const has_hidden_streams =
|
const all_channels_hidden =
|
||||||
$("#channels_overlay_container .stream-row:not(.notdisplayed)").length === 0;
|
$("#channels_overlay_container .stream-row:not(.notdisplayed)").length === 0;
|
||||||
const has_search_query =
|
const has_search_query =
|
||||||
$<HTMLInputElement>("#stream_filter input[type='text']").val()!.trim() !== "";
|
$<HTMLInputElement>("#stream_filter input[type='text']").val()!.trim() !== "";
|
||||||
// Show "no channels match" text if all channels are hidden and there's a search query.
|
const has_filter =
|
||||||
if (has_hidden_streams && has_search_query) {
|
archived_status_dropdown_filter !== stream_settings_data.FILTERS.ALL_CHANNELS;
|
||||||
|
|
||||||
|
// Both search queries and filters can lead to all channels being hidden.
|
||||||
|
if (all_channels_hidden && (has_search_query || (has_filter && has_streams))) {
|
||||||
$(".no-streams-to-show").children().hide();
|
$(".no-streams-to-show").children().hide();
|
||||||
$(".no_stream_match_filter_empty_text").show();
|
$(".no_stream_match_filter_empty_text").show();
|
||||||
$(".no-streams-to-show").show();
|
$(".no-streams-to-show").show();
|
||||||
@@ -639,6 +662,66 @@ export function switch_stream_sort(tab_name: string): void {
|
|||||||
redraw_left_panel();
|
redraw_left_panel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filters_dropdown_options(current_value: string | number | undefined): {
|
||||||
|
unique_id: string;
|
||||||
|
name: string;
|
||||||
|
bold_current_selection: boolean;
|
||||||
|
}[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
unique_id: stream_settings_data.FILTERS.ARCHIVED_CHANNELS,
|
||||||
|
name: $t({defaultMessage: "Archived channels"}),
|
||||||
|
bold_current_selection:
|
||||||
|
current_value === stream_settings_data.FILTERS.ARCHIVED_CHANNELS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unique_id: stream_settings_data.FILTERS.NON_ARCHIVED_CHANNELS,
|
||||||
|
name: $t({defaultMessage: "Non-archived channels"}),
|
||||||
|
bold_current_selection:
|
||||||
|
current_value === stream_settings_data.FILTERS.NON_ARCHIVED_CHANNELS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unique_id: stream_settings_data.FILTERS.ALL_CHANNELS,
|
||||||
|
name: $t({defaultMessage: "Archived and non-archived"}),
|
||||||
|
bold_current_selection: current_value === stream_settings_data.FILTERS.ALL_CHANNELS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_filters_for_tests(filter_widget: dropdown_widget.DropdownWidget): void {
|
||||||
|
filters_dropdown_widget = filter_widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter_click_handler(
|
||||||
|
event: JQuery.TriggeredEvent,
|
||||||
|
dropdown: tippy.Instance,
|
||||||
|
widget: dropdown_widget.DropdownWidget,
|
||||||
|
): void {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const filter_id = $(event.currentTarget).attr("data-unique-id");
|
||||||
|
assert(filter_id !== undefined);
|
||||||
|
// We don't support multiple filters, so we clear existing and add the new filter.
|
||||||
|
archived_status_dropdown_filter = filter_id;
|
||||||
|
redraw_left_panel();
|
||||||
|
dropdown.hide();
|
||||||
|
widget.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_up_dropdown_widget(): void {
|
||||||
|
archived_status_dropdown_filter = stream_settings_data.FILTERS.NON_ARCHIVED_CHANNELS;
|
||||||
|
filters_dropdown_widget = new dropdown_widget.DropdownWidget({
|
||||||
|
...views_util.COMMON_DROPDOWN_WIDGET_PARAMS,
|
||||||
|
get_options: filters_dropdown_options,
|
||||||
|
widget_name: "stream_settings_filter",
|
||||||
|
item_click_callback: filter_click_handler,
|
||||||
|
$events_container: $("#stream_filter"),
|
||||||
|
default_id: archived_status_dropdown_filter,
|
||||||
|
});
|
||||||
|
filters_dropdown_widget.setup();
|
||||||
|
}
|
||||||
|
|
||||||
function setup_page(callback: () => void): void {
|
function setup_page(callback: () => void): void {
|
||||||
// We should strongly consider only setting up the page once,
|
// We should strongly consider only setting up the page once,
|
||||||
// but I am writing these comments write before a big release,
|
// but I am writing these comments write before a big release,
|
||||||
@@ -723,6 +806,7 @@ function setup_page(callback: () => void): void {
|
|||||||
const new_stream_announcements_stream_sub = stream_data.get_sub_by_name(
|
const new_stream_announcements_stream_sub = stream_data.get_sub_by_name(
|
||||||
new_stream_announcements_stream,
|
new_stream_announcements_stream,
|
||||||
);
|
);
|
||||||
|
const realm_has_archived_channels = stream_data.get_archived_subs().length > 0;
|
||||||
|
|
||||||
const template_data = {
|
const template_data = {
|
||||||
new_stream_announcements_stream_sub,
|
new_stream_announcements_stream_sub,
|
||||||
@@ -747,6 +831,7 @@ function setup_page(callback: () => void): void {
|
|||||||
disable_message_retention_setting:
|
disable_message_retention_setting:
|
||||||
!realm.zulip_plan_is_not_limited || !current_user.is_owner,
|
!realm.zulip_plan_is_not_limited || !current_user.is_owner,
|
||||||
group_setting_labels: settings_config.all_group_setting_labels.stream,
|
group_setting_labels: settings_config.all_group_setting_labels.stream,
|
||||||
|
realm_has_archived_channels,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendered = render_stream_settings_overlay(template_data);
|
const rendered = render_stream_settings_overlay(template_data);
|
||||||
@@ -756,6 +841,7 @@ function setup_page(callback: () => void): void {
|
|||||||
initialize_components();
|
initialize_components();
|
||||||
redraw_left_panel();
|
redraw_left_panel();
|
||||||
stream_create.set_up_handlers();
|
stream_create.set_up_handlers();
|
||||||
|
set_up_dropdown_widget();
|
||||||
|
|
||||||
const throttled_redraw_left_panel = _.throttle(redraw_left_panel, 50);
|
const throttled_redraw_left_panel = _.throttle(redraw_left_panel, 50);
|
||||||
$("#stream_filter input[type='text']").on("input", () => {
|
$("#stream_filter input[type='text']").on("input", () => {
|
||||||
@@ -876,6 +962,23 @@ export function change_state(
|
|||||||
toggler.goto(left_side_tab);
|
toggler.goto(left_side_tab);
|
||||||
}
|
}
|
||||||
switch_to_stream_row(stream_id);
|
switch_to_stream_row(stream_id);
|
||||||
|
|
||||||
|
const sub = stream_data.get_sub_by_id(stream_id);
|
||||||
|
if (sub) {
|
||||||
|
const FILTERS = stream_settings_data.FILTERS;
|
||||||
|
const should_update_filter =
|
||||||
|
(archived_status_dropdown_filter === FILTERS.NON_ARCHIVED_CHANNELS &&
|
||||||
|
sub.is_archived) ||
|
||||||
|
(archived_status_dropdown_filter === FILTERS.ARCHIVED_CHANNELS && !sub.is_archived);
|
||||||
|
if (should_update_filter) {
|
||||||
|
if (sub.is_archived) {
|
||||||
|
archived_status_dropdown_filter = FILTERS.ARCHIVED_CHANNELS;
|
||||||
|
} else {
|
||||||
|
archived_status_dropdown_filter = FILTERS.NON_ARCHIVED_CHANNELS;
|
||||||
|
}
|
||||||
|
filters_dropdown_widget.render(archived_status_dropdown_filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -642,11 +642,21 @@ h4.user_group_setting_subsection_title {
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template:
|
grid-template:
|
||||||
"search-input clear-search more-options-button" auto / minmax(0, 1fr)
|
"search-input clear-search dropdown-widget" auto / minmax(0, 1fr)
|
||||||
30px;
|
30px;
|
||||||
border-bottom: 1px solid var(--color-border-modal-bar);
|
border-bottom: 1px solid var(--color-border-modal-bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#stream_settings_filter_widget {
|
||||||
|
margin-left: 10px;
|
||||||
|
gap: 3px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream_settings_filter_container.hide_filter {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#user_group_visibility_settings_widget {
|
#user_group_visibility_settings_widget {
|
||||||
grid-area: more-options-button;
|
grid-area: more-options-button;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
{{#if stream_settings_link}}
|
{{#if stream_settings_link}}
|
||||||
{{#unless stream.is_archived}}
|
|
||||||
<a class="message-header-stream-settings-button tippy-zulip-tooltip" data-tooltip-template-id="stream-details-tooltip-template" data-tippy-placement="bottom" href="{{stream_settings_link}}">
|
<a class="message-header-stream-settings-button tippy-zulip-tooltip" data-tooltip-template-id="stream-details-tooltip-template" data-tippy-placement="bottom" href="{{stream_settings_link}}">
|
||||||
{{> navbar_icon_and_title . }}
|
{{> navbar_icon_and_title . }}
|
||||||
</a>
|
</a>
|
||||||
@@ -13,11 +12,6 @@
|
|||||||
{{/unless}}
|
{{/unless}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
{{else}}
|
|
||||||
<span class="navbar_title">
|
|
||||||
{{> navbar_icon_and_title . }}
|
|
||||||
</span>
|
|
||||||
{{/unless}}
|
|
||||||
<span class="narrow_description rendered_markdown single-line-rendered-markdown">
|
<span class="narrow_description rendered_markdown single-line-rendered-markdown">
|
||||||
{{#if rendered_narrow_description}}
|
{{#if rendered_narrow_description}}
|
||||||
{{rendered_markdown rendered_narrow_description}}
|
{{rendered_markdown rendered_narrow_description}}
|
||||||
|
@@ -25,6 +25,9 @@
|
|||||||
<button type="button" class="clear_search_button" id="clear_search_stream_name">
|
<button type="button" class="clear_search_button" id="clear_search_stream_name">
|
||||||
<i class="zulip-icon zulip-icon-close" aria-hidden="true"></i>
|
<i class="zulip-icon zulip-icon-close" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="stream_settings_filter_container {{#unless realm_has_archived_channels}}hide_filter{{/unless}}">
|
||||||
|
{{> ../dropdown_widget widget_name="stream_settings_filter"}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="no-streams-to-show">
|
<div class="no-streams-to-show">
|
||||||
<div class="subscribed_streams_tab_empty_text">
|
<div class="subscribed_streams_tab_empty_text">
|
||||||
|
@@ -716,6 +716,7 @@ test("delete_sub", () => {
|
|||||||
|
|
||||||
stream_data.add_sub(canada);
|
stream_data.add_sub(canada);
|
||||||
const num_subscribed_subs = stream_data.num_subscribed_subs();
|
const num_subscribed_subs = stream_data.num_subscribed_subs();
|
||||||
|
const archived_subs = stream_data.get_archived_subs();
|
||||||
|
|
||||||
assert.ok(stream_data.is_subscribed(canada.stream_id));
|
assert.ok(stream_data.is_subscribed(canada.stream_id));
|
||||||
assert.equal(stream_data.get_sub("Canada").stream_id, canada.stream_id);
|
assert.equal(stream_data.get_sub("Canada").stream_id, canada.stream_id);
|
||||||
@@ -728,6 +729,7 @@ test("delete_sub", () => {
|
|||||||
assert.ok(stream_data.get_sub("Canada"));
|
assert.ok(stream_data.get_sub("Canada"));
|
||||||
assert.ok(sub_store.get(canada.stream_id));
|
assert.ok(sub_store.get(canada.stream_id));
|
||||||
assert.equal(stream_data.num_subscribed_subs(), num_subscribed_subs);
|
assert.equal(stream_data.num_subscribed_subs(), num_subscribed_subs);
|
||||||
|
assert.equal(stream_data.get_archived_subs().length, archived_subs.length + 1);
|
||||||
|
|
||||||
blueslip.expect("warn", "Failed to archive stream 99999");
|
blueslip.expect("warn", "Failed to archive stream 99999");
|
||||||
stream_data.delete_sub(99999);
|
stream_data.delete_sub(99999);
|
||||||
|
@@ -209,6 +209,11 @@ run_test("redraw_left_panel", ({override, mock_template}) => {
|
|||||||
populated_subs = data.subscriptions;
|
populated_subs = data.subscriptions;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filters_dropdown_widget = {
|
||||||
|
render: function render() {},
|
||||||
|
};
|
||||||
|
stream_settings_ui.set_filters_for_tests(filters_dropdown_widget);
|
||||||
|
|
||||||
stream_settings_ui.render_left_panel_superset();
|
stream_settings_ui.render_left_panel_superset();
|
||||||
|
|
||||||
const sub_stubs = [];
|
const sub_stubs = [];
|
||||||
@@ -223,6 +228,11 @@ run_test("redraw_left_panel", ({override, mock_template}) => {
|
|||||||
|
|
||||||
$.create("#channels_overlay_container .stream-row", {children: sub_stubs});
|
$.create("#channels_overlay_container .stream-row", {children: sub_stubs});
|
||||||
|
|
||||||
|
const $no_streams_message = $(".no-streams-to-show");
|
||||||
|
const $child_element = $(".subscribed_streams_tab_empty_text");
|
||||||
|
$no_streams_message.children = () => $child_element;
|
||||||
|
$child_element.hide = () => [];
|
||||||
|
|
||||||
let ui_called = false;
|
let ui_called = false;
|
||||||
scroll_util.reset_scrollbar = ($elem) => {
|
scroll_util.reset_scrollbar = ($elem) => {
|
||||||
ui_called = true;
|
ui_called = true;
|
||||||
@@ -360,6 +370,7 @@ run_test("redraw_left_panel", ({override, mock_template}) => {
|
|||||||
test_filter({input: "d", show_subscribed: true}, [poland]);
|
test_filter({input: "d", show_subscribed: true}, [poland]);
|
||||||
assert.ok($(".stream-row-denmark").hasClass("active"));
|
assert.ok($(".stream-row-denmark").hasClass("active"));
|
||||||
|
|
||||||
|
$(".stream-row.active").attr("data-stream-id", 101);
|
||||||
stream_settings_ui.switch_stream_tab("subscribed");
|
stream_settings_ui.switch_stream_tab("subscribed");
|
||||||
assert.ok(!$(".stream-row-denmark").hasClass("active"));
|
assert.ok(!$(".stream-row-denmark").hasClass("active"));
|
||||||
assert.ok(!$(".right .settings").visible());
|
assert.ok(!$(".right .settings").visible());
|
||||||
|
Reference in New Issue
Block a user