Merge branch 'zulip:main' into branch-left-align

This commit is contained in:
Aditya Chaudhary 2024-07-02 01:23:08 +05:45 committed by GitHub
commit bbd66de7c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 515 additions and 333 deletions

View File

@ -187,9 +187,11 @@ Replace `<username>` and `<server_ip>` with the appropriate values below.
{!import-self-hosted-server-tips.md!}
```
cd /tmp
tar -xf mattermost_data.tar.gz
cd /home/zulip/deployments/current
./scripts/stop-server
./manage.py convert_mattermost_data /tmp/mattermost_data.tar.gz --output /tmp/converted_mattermost_data
./manage.py convert_mattermost_data /tmp/mattermost_data --output /tmp/converted_mattermost_data
./manage.py import '' /tmp/converted_mattermost_data/<team-name>
./scripts/start-server
```
@ -197,9 +199,11 @@ Replace `<username>` and `<server_ip>` with the appropriate values below.
Alternatively, to import into a custom subdomain, run:
```
cd /tmp
tar -xf mattermost_data.tar.gz
cd /home/zulip/deployments/current
./scripts/stop-server
./manage.py convert_mattermost_data /tmp/mattermost_data.tar.gz --output /tmp/converted_mattermost_data
./manage.py convert_mattermost_data /tmp/mattermost_data --output /tmp/converted_mattermost_data
./manage.py import <subdomain> /tmp/converted_mattermost_data/<team-name>
./scripts/start-server
```

View File

@ -114,6 +114,7 @@ select = [
"EXE", # shebang
"F", # flakes
"FLY", # string formatting
"FURB", # refurbishing
"G", # logging format
"I", # import sorting
"INT", # gettext

View File

@ -147,7 +147,7 @@ NGINX_LOG_LINE_RE = re.compile(
(?P<hostname> \S+ ) \s+
(?P<duration> \S+ )
""",
re.X,
re.VERBOSE,
)
PYTHON_LOG_LINE_RE = re.compile(
@ -178,7 +178,7 @@ PYTHON_LOG_LINE_RE = re.compile(
) \s+ via \s+ (?P<user_agent> .* )
\)
""",
re.X,
re.VERBOSE,
)

View File

@ -98,7 +98,7 @@ def setup_shell_profile(shell_profile: str) -> None:
def write_command(command: str) -> None:
if os.path.exists(shell_profile_path):
with open(shell_profile_path) as shell_profile_file:
lines = [line.strip() for line in shell_profile_file.readlines()]
lines = [line.strip() for line in shell_profile_file]
if command not in lines:
with open(shell_profile_path, "a+") as shell_profile_file:
shell_profile_file.writelines(command + "\n")

View File

@ -82,7 +82,7 @@ mypy_args += ["--", *python_files, *pyi_files]
rc = subprocess.call([mypy_command, *mypy_args])
if rc != 0:
print("")
print()
print("See https://zulip.readthedocs.io/en/latest/testing/mypy.html for debugging tips.")
sys.exit(rc)

View File

@ -72,7 +72,7 @@ EXEMPT_FILES = make_set(
"web/src/compose_closed_ui.ts",
"web/src/compose_fade.ts",
"web/src/compose_notifications.ts",
"web/src/compose_popovers.js",
"web/src/compose_popovers.ts",
"web/src/compose_recipient.ts",
"web/src/compose_reply.ts",
"web/src/compose_send_menu_popover.js",

View File

@ -140,7 +140,7 @@ or report and ask for help in chat.zulip.org""",
file=sys.stderr,
)
if os.environ.get("GITHUB_ACTIONS"):
print("", file=sys.stderr)
print(file=sys.stderr)
print(
"""
Screenshots generated on failure are extremely helpful for understanding
@ -150,7 +150,7 @@ below to download and view the generated screenshots.
""",
file=sys.stderr,
)
print("", file=sys.stderr)
print(file=sys.stderr)
else:
print(
"It's also worthy to see screenshots generated on failure stored under var/puppeteer/*.png"

View File

@ -1,10 +1,10 @@
#!/usr/bin/env python3
import argparse
import os
import pathlib
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Dict, List
bot_commits = 0
@ -36,7 +36,7 @@ def retrieve_log(repo: str, revisions: str) -> List[str]:
def find_path(repository: str) -> str:
return str(pathlib.Path().resolve().parents[0] / repository)
return str(Path.cwd().parent / repository)
def process_repo(

View File

@ -347,16 +347,16 @@ async function test_search_venice(page: Page): Promise<void> {
await page.waitForSelector(await get_stream_li(page, "Verona"), {visible: true});
await page.click("#streams_header .left-sidebar-title");
await page.waitForSelector(".input-append.notdisplayed");
await page.waitForSelector(".stream_search_section.notdisplayed");
}
async function test_stream_search_filters_stream_list(page: Page): Promise<void> {
console.log("Filter streams using left side bar");
await page.waitForSelector(".input-append.notdisplayed"); // Stream filter box invisible initially
await page.waitForSelector(".stream_search_section.notdisplayed"); // Stream filter box invisible initially
await page.click("#streams_header .left-sidebar-title");
await page.waitForSelector("#streams_list .input-append.notdisplayed", {hidden: true});
await page.waitForSelector("#streams_list .stream_search_section.notdisplayed", {hidden: true});
// assert streams exist by waiting till they're visible
await page.waitForSelector(await get_stream_li(page, "Denmark"), {visible: true});

View File

@ -99,6 +99,39 @@ export function create({
return pill_widget;
}
export function create_without_add_button({
$pill_container,
get_potential_subscribers,
onPillCreateAction,
onPillRemoveAction,
}: {
$pill_container: JQuery;
get_potential_subscribers: () => User[];
onPillCreateAction: (pill_user_ids: number[]) => void;
onPillRemoveAction: (pill_user_ids: number[]) => void;
}): CombinedPillContainer {
const pill_widget = input_pill.create<CombinedPill>({
$container: $pill_container,
create_item_from_text,
get_text_from_item,
});
function get_users(): User[] {
const potential_subscribers = get_potential_subscribers();
return user_pill.filter_taken_users(potential_subscribers, pill_widget);
}
pill_widget.onPillCreate(() => {
onPillCreateAction(get_pill_user_ids(pill_widget));
});
pill_widget.onPillRemove(() => {
onPillRemoveAction(get_pill_user_ids(pill_widget));
});
set_up_pill_typeahead({pill_widget, $pill_container, get_users});
return pill_widget;
}
function get_pill_user_ids(pill_widget: CombinedPillContainer): number[] {
const user_ids = user_pill.get_user_ids(pill_widget);
const stream_user_ids = stream_pill.get_user_ids(pill_widget);

View File

@ -1,4 +1,5 @@
import $ from "jquery";
import assert from "minimalistic-assert";
import * as tippy from "tippy.js";
import render_compose_control_buttons_popover from "../templates/popovers/compose_control_buttons/compose_control_buttons_popover.hbs";
@ -6,13 +7,12 @@ import render_mobile_message_buttons_popover from "../templates/popovers/mobile_
import * as compose_actions from "./compose_actions";
import * as giphy_state from "./giphy_state";
import * as narrow_state from "./narrow_state";
import * as popover_menus from "./popover_menus";
import * as popovers from "./popovers";
import * as rows from "./rows";
import {parse_html} from "./ui_util";
export function initialize() {
export function initialize(): void {
// compose box buttons popover shown on mobile widths.
// We want this click event to propagate and hide other popovers
// that could possibly obstruct user from using this popover.
@ -25,16 +25,11 @@ export function initialize() {
// actions
target: ".mobile_button_container",
placement: "top",
theme: "popover-menu",
onShow(instance) {
popover_menus.popover_instances.compose_mobile_button = instance;
popover_menus.on_show_prep(instance);
instance.setContent(
parse_html(
render_mobile_message_buttons_popover({
is_in_private_narrow: narrow_state.narrowed_to_pms(),
}),
),
);
instance.setContent(parse_html(render_mobile_message_buttons_popover()));
},
onMount(instance) {
const $popper = $(instance.popper);
@ -58,7 +53,7 @@ export function initialize() {
// Destroy instance so that event handlers
// are destroyed too.
instance.destroy();
popover_menus.popover_instances.compose_mobile_button = undefined;
popover_menus.popover_instances.compose_mobile_button = null;
},
});
@ -68,6 +63,7 @@ export function initialize() {
popover_menus.register_popover_menu(".compose_control_menu_wrapper", {
placement: "top",
onShow(instance) {
assert(instance.reference instanceof HTMLElement);
const parent_row = rows.get_closest_row(instance.reference);
let preview_mode_on;
// If the popover is opened from a message edit form, we want to
@ -91,7 +87,7 @@ export function initialize() {
},
onHidden(instance) {
instance.destroy();
popover_menus.popover_instances.compose_control_buttons = undefined;
popover_menus.popover_instances.compose_control_buttons = null;
},
});
}

View File

@ -239,7 +239,7 @@ export function is_emoji_present_in_text(text, emoji_dict) {
}
function filter_emojis() {
const $elt = $(".emoji-popover-filter").expectOne();
const $elt = $("#emoji-popover-filter").expectOne();
const query = $elt.val().trim().toLowerCase();
const message_id = Number($(".emoji-search-results-container").attr("data-message-id"));
const search_results_visible = $(".emoji-search-results-container").is(":visible");
@ -423,7 +423,7 @@ function get_next_emoji_coordinates(move_by) {
function change_focus_to_filter() {
const $popover = $(emoji_popover_instance.popper);
$popover.find(".emoji-popover-filter").trigger("focus");
$popover.find("#emoji-popover-filter").trigger("focus");
// If search is active reset current selected emoji to first emoji.
if (search_is_active) {
current_section = 0;
@ -457,12 +457,12 @@ export function navigate(event_name, e) {
const $emoji_map = $popover.find(".emoji-popover-emoji-map").expectOne();
const $selected_emoji = get_rendered_emoji(current_section, current_index);
const is_filter_focused = $(".emoji-popover-filter").is(":focus");
const is_filter_focused = $("#emoji-popover-filter").is(":focus");
// special cases
if (is_filter_focused) {
// Move down into emoji map.
const filter_text = $(".emoji-popover-filter").val();
const is_cursor_at_end = $(".emoji-popover-filter").caret() === filter_text.length;
const filter_text = $("#emoji-popover-filter").val();
const is_cursor_at_end = $("#emoji-popover-filter").caret() === filter_text.length;
if (event_name === "down_arrow" || (is_cursor_at_end && event_name === "right_arrow")) {
$selected_emoji.trigger("focus");
if (current_section === 0 && current_index < 6) {
@ -489,7 +489,7 @@ export function navigate(event_name, e) {
// goes to beginning) with something reasonable and
// consistent (cursor goes to the end of the filter
// string).
$(".emoji-popover-filter").trigger("focus").caret(Number.POSITIVE_INFINITY);
$("#emoji-popover-filter").trigger("focus").caret(Number.POSITIVE_INFINITY);
scroll_util.get_scroll_element($emoji_map).scrollTop(0);
scroll_util.get_scroll_element($(".emoji-search-results-container")).scrollTop(0);
current_section = 0;
@ -526,7 +526,7 @@ export function navigate(event_name, e) {
}
function process_keypress(e) {
const is_filter_focused = $(".emoji-popover-filter").is(":focus");
const is_filter_focused = $("#emoji-popover-filter").is(":focus");
const pressed_key = e.which;
if (
!is_filter_focused &&
@ -538,7 +538,7 @@ function process_keypress(e) {
e.preventDefault();
e.stopPropagation();
const $emoji_filter = $(".emoji-popover-filter");
const $emoji_filter = $("#emoji-popover-filter");
const old_query = $emoji_filter.val();
let new_query = "";
@ -599,8 +599,8 @@ function register_popover_events($popover) {
emoji_select_tab(scroll_util.get_scroll_element($emoji_map));
});
$(".emoji-popover-filter").on("input", filter_emojis);
$(".emoji-popover-filter").on("keydown", process_enter_while_filtering);
$("#emoji-popover-filter").on("input", filter_emojis);
$("#emoji-popover-filter").on("keydown", process_enter_while_filtering);
$(".emoji-popover").on("keypress", process_keypress);
$(".emoji-popover").on("keydown", (e) => {
// Because of cross-browser issues we need to handle Backspace
@ -615,6 +615,7 @@ function register_popover_events($popover) {
function get_default_emoji_popover_options() {
return {
theme: "popover-menu",
placement: "top",
popperOptions: {
modifiers: [
@ -804,7 +805,7 @@ function register_click_handlers() {
}
});
$("body").on("click", ".emoji-popover-filter", () => {
$("body").on("click", "#emoji-popover-filter", () => {
reset_emoji_showcase();
});

View File

@ -726,6 +726,45 @@ export class Filter {
return true;
}
static adjusted_terms_if_moved(raw_terms: NarrowTerm[], message: Message): NarrowTerm[] | null {
if (message.type !== "stream") {
return null;
}
assert(typeof message.display_recipient === "string");
assert(typeof message.topic === "string");
const adjusted_terms = [];
let terms_changed = false;
for (const term of raw_terms) {
const adjusted_term = {...term};
if (
Filter.canonicalize_operator(term.operator) === "channel" &&
!util.lower_same(term.operand, message.display_recipient)
) {
adjusted_term.operand = message.display_recipient;
terms_changed = true;
}
if (
Filter.canonicalize_operator(term.operator) === "topic" &&
!util.lower_same(term.operand, message.topic)
) {
adjusted_term.operand = message.topic;
terms_changed = true;
}
adjusted_terms.push(adjusted_term);
}
if (!terms_changed) {
return null;
}
return adjusted_terms;
}
equals(filter: Filter, excluded_operators?: string[]): boolean {
return _.isEqual(
filter.sorted_terms(excluded_operators),

View File

@ -155,6 +155,7 @@ function toggle_giphy_popover(target) {
popover_menus.toggle_popover_menu(
target,
{
theme: "popover-menu",
placement: "top",
onCreate(instance) {
instance.setContent(ui_util.parse_html(render_giphy_picker()));

View File

@ -288,6 +288,7 @@ export function update_messages(events) {
}
}
// The event.message_ids received from the server are not in sorted order.
// Sorts in ascending order.
event_messages.sort((a, b) => a.id - b.id);
if (
@ -307,6 +308,18 @@ export function update_messages(events) {
drafts.rename_stream_recipient(old_stream_id, orig_topic, new_stream_id, new_topic);
}
// Remove the stream_topic_entry for the old topics;
// must be called before we call set message topic.
const num_messages = event_messages.length;
if (num_messages > 0) {
stream_topic_history.remove_messages({
stream_id: old_stream_id,
topic_name: orig_topic,
num_messages,
max_removed_msg_id: event_messages[num_messages - 1].id,
});
}
for (const moved_message of event_messages) {
if (realm.realm_allow_edit_history) {
/* Simulate the format of server-generated edit
@ -335,23 +348,6 @@ export function update_messages(events) {
}
moved_message.last_edit_timestamp = event.edit_timestamp;
// Remove the Recent Conversations entry for the old topics;
// must be called before we call set_message_topic.
//
// TODO: Use a single bulk request to do this removal.
// Note that we need to be careful to only remove IDs
// that were present in stream_topic_history data.
// This may not be possible to do correctly without extra
// complexity; the present loop assumes stream_topic_history has
// only messages in message_store, but that's been false
// since we added the server_history feature.
stream_topic_history.remove_messages({
stream_id: moved_message.stream_id,
topic_name: moved_message.topic,
num_messages: 1,
max_removed_msg_id: moved_message.id,
});
// Update the unread counts; again, this must be called
// before we modify the topic field on the message.
unread.update_unread_topics(moved_message, event);

View File

@ -233,18 +233,24 @@ function try_rendering_locally_for_same_narrow(filter, opts) {
return false;
}
// If the difference between the current filter and the new filter
// is just a `near` operator, or just the value of a `near` operator,
// we can render the new filter without a rerender of the message list
// if the target message in the `near` operator is already rendered.
const excluded_operators = ["near"];
if (!filter.equals(current_filter, excluded_operators)) {
return false;
}
if (filter.has_operator("near")) {
const target_id = Number.parseInt(filter.operands("near")[0], 10);
if (!message_lists.current?.get(target_id)) {
const target_message = message_lists.current?.get(target_id);
if (!target_message) {
return false;
}
const adjusted_terms = Filter.adjusted_terms_if_moved(filter.terms(), target_message);
if (adjusted_terms !== null) {
filter = new Filter(adjusted_terms);
}
// If the difference between the current filter and the new filter
// is just a `near` operator, or just the value of a `near` operator,
// we can render the new filter without a rerender of the message list
// if the target message in the `near` operator is already rendered.
const excluded_operators = ["near"];
if (!filter.equals(current_filter, excluded_operators)) {
return false;
}
@ -256,7 +262,7 @@ function try_rendering_locally_for_same_narrow(filter, opts) {
}
message_lists.current.data.filter = filter;
update_hash_to_match_filter(filter);
update_hash_to_match_filter(filter, "retarget message location");
return true;
}
@ -402,38 +408,6 @@ export function show(raw_terms, opts) {
if (id_info.target_id && filter.has_operator("channel") && filter.has_operator("topic")) {
const target_message = message_store.get(id_info.target_id);
function adjusted_terms_if_moved(raw_terms, message) {
const adjusted_terms = [];
let terms_changed = false;
for (const term of raw_terms) {
const adjusted_term = {...term};
if (
Filter.canonicalize_operator(term.operator) === "channel" &&
!util.lower_same(term.operand, message.display_recipient)
) {
adjusted_term.operand = message.display_recipient;
terms_changed = true;
}
if (
Filter.canonicalize_operator(term.operator) === "topic" &&
!util.lower_same(term.operand, message.topic)
) {
adjusted_term.operand = message.topic;
terms_changed = true;
}
adjusted_terms.push(adjusted_term);
}
if (!terms_changed) {
return null;
}
return adjusted_terms;
}
if (target_message) {
// If we have the target message ID for the narrow in our
// local cache, and the target message has been moved from
@ -447,7 +421,10 @@ export function show(raw_terms, opts) {
// The stream name is invalid or incorrect in the URL.
// We reconstruct the narrow with the data from the
// target message ID that we have.
const adjusted_terms = adjusted_terms_if_moved(raw_terms, target_message);
const adjusted_terms = Filter.adjusted_terms_if_moved(
raw_terms,
target_message,
);
if (adjusted_terms === null) {
blueslip.error("adjusted_terms impossibly null");
@ -485,7 +462,10 @@ export function show(raw_terms, opts) {
!narrow_matches_target_message &&
(narrow_exists_in_edit_history || !realm.realm_allow_edit_history)
) {
const adjusted_terms = adjusted_terms_if_moved(raw_terms, target_message);
const adjusted_terms = Filter.adjusted_terms_if_moved(
raw_terms,
target_message,
);
if (adjusted_terms !== null) {
show(adjusted_terms, {
...opts,

View File

@ -119,14 +119,14 @@ export function get_message_id(elem: HTMLElement): number {
return message_id;
}
export function get_closest_group(element: string): JQuery {
export function get_closest_group(element: HTMLElement): JQuery {
// This gets the closest message row to an element, whether it's
// a recipient bar or message. With our current markup,
// this is the most reliable way to do it.
return $(element).closest("div.recipient_row");
}
export function get_closest_row(element: string): JQuery {
export function get_closest_row(element: HTMLElement): JQuery {
return $(element).closest("div.message_row");
}

View File

@ -9,10 +9,8 @@ import type {ListWidget as ListWidgetType} from "./list_widget";
import * as people from "./people";
import {current_user} from "./state_data";
import * as stream_create_subscribers_data from "./stream_create_subscribers_data";
import type {CombinedPillContainer} from "./typeahead_helper";
import * as user_sort from "./user_sort";
let pill_widget: CombinedPillContainer;
let all_users_list_widget: ListWidgetType<number, people.User>;
export function get_principals(): number[] {
@ -33,16 +31,35 @@ function add_all_users(): void {
add_user_ids(user_ids);
}
function remove_user_ids(user_ids: number[]): void {
stream_create_subscribers_data.remove_user_ids(user_ids);
function soft_remove_user_id(user_id: number): void {
stream_create_subscribers_data.soft_remove_user_id(user_id);
redraw_subscriber_list();
}
function undo_soft_remove_user_id(user_id: number): void {
stream_create_subscribers_data.undo_soft_remove_user_id(user_id);
redraw_subscriber_list();
}
function sync_user_ids(user_ids: number[]): void {
stream_create_subscribers_data.sync_user_ids(user_ids);
redraw_subscriber_list();
}
function build_pill_widget({$parent_container}: {$parent_container: JQuery}): void {
const $pill_container = $parent_container.find(".pill-container");
const get_potential_subscribers = stream_create_subscribers_data.get_potential_subscribers;
pill_widget = add_subscribers_pill.create({$pill_container, get_potential_subscribers});
add_subscribers_pill.create_without_add_button({
$pill_container,
get_potential_subscribers,
onPillCreateAction: add_user_ids,
// It is better to sync the current set of user ids in the input
// instead of removing user_ids from the user_ids_set, otherwise
// we'll have to have more complex logic of when to remove
// a user and when not to depending upon their group, channel
// and individual pills.
onPillRemoveAction: sync_user_ids,
});
}
export function create_handlers($container: JQuery): void {
@ -56,24 +73,14 @@ export function create_handlers($container: JQuery): void {
e.preventDefault();
const $elem = $(e.target);
const user_id = Number.parseInt($elem.attr("data-user-id")!, 10);
remove_user_ids([user_id]);
soft_remove_user_id(user_id);
});
const button_selector = ".add_subscribers_container button.add-subscriber-button";
function add_users({pill_user_ids}: {pill_user_ids: number[]}): void {
add_user_ids(pill_user_ids);
// eslint-disable-next-line unicorn/no-array-callback-reference
const $pill_widget_button = $container.find(button_selector);
$pill_widget_button.prop("disabled", true);
pill_widget.clear();
}
add_subscribers_pill.set_up_handlers({
get_pill_widget: () => pill_widget,
$parent_container: $container,
pill_selector: ".add_subscribers_container .input",
button_selector,
action: add_users,
$container.on("click", ".undo_soft_removed_potential_subscriber", (e) => {
e.preventDefault();
const $elem = $(e.target);
const user_id = Number.parseInt($elem.attr("data-user-id")!, 10);
undo_soft_remove_user_id(user_id);
});
}
@ -100,6 +107,9 @@ export function build_widgets(): void {
is_current_user: user.user_id === current_user_id,
disabled: stream_create_subscribers_data.must_be_subscribed(user.user_id),
img_src: people.small_avatar_url_for_person(user),
soft_removed: stream_create_subscribers_data.user_id_in_soft_remove_list(
user.user_id,
),
};
return render_new_stream_user(item);
},

View File

@ -1,11 +1,15 @@
import _ from "lodash";
import * as people from "./people";
import type {User} from "./people";
import {current_user} from "./state_data";
let user_id_set: Set<number>;
let soft_remove_user_id_set: Set<number>;
export function initialize_with_current_user(): void {
user_id_set = new Set([current_user.user_id]);
soft_remove_user_id_set = new Set();
}
export function sorted_user_ids(): number[] {
@ -24,7 +28,7 @@ export function get_all_user_ids(): number[] {
export function get_principals(): number[] {
// Return list of user ids which were selected by user.
return [...user_id_set];
return _.difference([...user_id_set], [...soft_remove_user_id_set]);
}
export function get_potential_subscribers(): User[] {
@ -42,6 +46,9 @@ export function add_user_ids(user_ids: number[]): void {
const user = people.maybe_get_user_by_id(user_id);
if (user) {
user_id_set.add(user_id);
// Re-adding a user explicitly will not undo the soft remove on their row.
// e.g If `Iago` was added as part of a group and crossed out.
// Now, adding another group with Iago as part of it should not undo the soft remove.
}
}
}
@ -50,5 +57,28 @@ export function add_user_ids(user_ids: number[]): void {
export function remove_user_ids(user_ids: number[]): void {
for (const user_id of user_ids) {
user_id_set.delete(user_id);
undo_soft_remove_user_id(user_id);
}
}
export function sync_user_ids(user_ids: number[]): void {
// Current user does not have their pill in their input
// box, so we need to make sure that we don't delete
// it unnecessarily while syncing.
if (user_id_set.has(current_user.user_id)) {
user_ids.push(current_user.user_id);
}
user_id_set = new Set(user_ids);
}
export function soft_remove_user_id(user_id: number): void {
soft_remove_user_id_set.add(user_id);
}
export function undo_soft_remove_user_id(user_id: number): void {
soft_remove_user_id_set.delete(user_id);
}
export function user_id_in_soft_remove_list(user_id: number): boolean {
return soft_remove_user_id_set.has(user_id);
}

View File

@ -185,18 +185,14 @@ export class PerStreamHistory {
maybe_remove(topic_name: string, num_messages: number): void {
const existing = this.topics.get(topic_name);
if (!existing || existing.count === 0) {
if (!existing) {
return;
}
if (existing.count <= num_messages) {
this.topics.delete(topic_name);
if (!is_complete_for_stream_id(this.stream_id)) {
// Request server for latest message in topic if we
// cannot be sure that we have all messages in the topic.
update_topic_last_message_id(this.stream_id, topic_name);
return;
}
// Verify if this topic still has messages from the server.
update_topic_last_message_id(this.stream_id, topic_name);
}
existing.count -= num_messages;

View File

@ -408,16 +408,11 @@ export function enable_or_disable_add_subscribers_elements(
stream_creation = false,
) {
const $input_element = $container_elem.find(".input").expectOne();
const $add_subscribers_button = $container_elem
.find('button[name="add_subscriber"]')
.expectOne();
const $add_subscribers_container = $(".edit_subscribers_for_stream .subscriber_list_settings");
$input_element.prop("contenteditable", enable_elem);
$add_subscribers_button.prop("disabled", !enable_elem);
if (enable_elem) {
$add_subscribers_button.css("pointer-events", "");
$add_subscribers_container[0]?._tippy?.destroy();
$container_elem.find(".add_subscribers_container").removeClass("add_subscribers_disabled");
} else {
@ -437,5 +432,13 @@ export function enable_or_disable_add_subscribers_elements(
.find(".add_all_users_to_stream_btn_container")
.addClass("add_subscribers_disabled");
}
} else {
const $add_subscribers_button = $container_elem
.find('button[name="add_subscriber"]')
.expectOne();
$add_subscribers_button.prop("disabled", !enable_elem);
if (enable_elem) {
$add_subscribers_button.css("pointer-events", "");
}
}
}

View File

@ -385,6 +385,11 @@ div.overlay {
margin: 0;
}
/* TODO: Once all layouts using this button
are modernized, the Font Awesome icon
should be replaced with a Zulip icon,
and its formatting should have no extra
space around its viewbox in SVG. */
.clear_search_button {
&:hover {
color: hsl(0deg 0% 0%);

View File

@ -60,13 +60,21 @@
/* This represents the space in the sidebar reserved for symbols like
the #; labels like the stream name go to the right of this. */
--left-sidebar-privacy-icon-column-size: 19px;
/* The full topic indentation includes 4px of indent in addition to
the above (and another 5px of padding not measured here) */
/* 13px at 14px/1em */
--left-sidebar-topic-resolve-width: 0.9286em;
/* At legacy sizes, the full indentation to the
left of the topic name was 25px of gutter,
plus 13px for the topic-resolution checkmark.
That works out to 38px (25px + 13px), which
we here express as pixels, as that is always
the amount of space to the left of the topic
name. However, CSS subtracts the em-unit width
of the topic-resolution checkmark to prevent
the the topic name from being shifted to the
right. */
--left-sidebar-topic-indent: calc(
var(--left-sidebar-far-left-gutter-size) +
var(--left-sidebar-privacy-icon-column-size) + 4px
38px - var(--left-sidebar-topic-resolve-width)
);
--left-sidebar-topic-resolve-width: 13px;
/* space direct message / stream / topic names from unread counters
and @ mention indicators by 3px on the right */
--left-sidebar-before-unread-count-padding: 3px;
@ -409,6 +417,7 @@
--color-message-content-container-border: hsl(0deg 0% 0% / 10%);
--color-message-content-container-border-focus: hsl(0deg 0% 57%);
--color-compose-control-button-background-hover: hsl(0deg 0% 0% / 5%);
--color-compose-focus-ring: var(--color-outline-focus);
/* Text colors */
--color-text-default: hsl(0deg 0% 20%);
@ -684,7 +693,6 @@
--color-background-modal: hsl(212deg 28% 18%);
--color-background-invitee-emails-pill-container: hsl(0deg 0% 0% / 20%);
--color-unmuted-or-followed-topic-list-item: hsl(236deg 33% 90%);
--color-outline-focus: hsl(0deg 0% 67%);
--color-recipient-bar-controls-spinner: hsl(0deg 0% 100%);
--color-background-search: hsl(0deg 0% 20%);
--color-background-search-option-hover: hsl(0deg 0% 30%);
@ -782,6 +790,7 @@
--color-message-content-container-border: hsl(0deg 0% 0% / 80%);
--color-message-content-container-border-focus: hsl(0deg 0% 100% / 27%);
--color-compose-control-button-background-hover: hsl(0deg 0% 100% / 5%);
--color-compose-focus-ring: hsl(0deg 0% 67%);
/* Text colors */
--color-text-default: hsl(0deg 0% 100% / 75%);

View File

@ -490,7 +490,7 @@
}
&:focus-visible {
outline-color: var(--color-outline-focus);
outline-color: var(--color-compose-focus-ring);
}
}
@ -1096,7 +1096,7 @@ textarea.new_message_textarea {
}
&:focus-visible {
outline-color: var(--color-outline-focus);
outline-color: var(--color-compose-focus-ring);
}
}
@ -1308,11 +1308,6 @@ textarea.new_message_textarea {
}
}
.compose_mobile_stream_button i,
.compose_mobile_direct_message_button i {
margin-right: 4px;
}
/* Class for send-area buttons, such as
Drafts and the send-adjacent vdots */
.send-control-button {
@ -1335,7 +1330,7 @@ textarea.new_message_textarea {
}
&:focus-visible {
outline-color: var(--color-outline-focus);
outline-color: var(--color-compose-focus-ring);
}
&:hover {
@ -1381,7 +1376,7 @@ textarea.new_message_textarea {
will handle the dimension change, so there won't
be any movement of the vdots in this state. */
outline: 0;
border: 2px solid var(--color-outline-focus);
border: 2px solid var(--color-compose-focus-ring);
}
@media ((width >= $sm_min) and (width < $mc_min)) {

View File

@ -306,9 +306,9 @@
}
}
& textarea.new_message_textarea {
&.invalid,
&.invalid:focus {
#message-content-container {
&:has(textarea.new_message_textarea.invalid),
&:has(textarea.new_message_textarea.invalid:focus) {
border-color: hsl(3deg 73% 74%);
box-shadow: 0 0 2px hsl(3deg 73% 74%);
}
@ -411,7 +411,7 @@
color: inherit;
}
.dropdown-list-search .dropdown-list-search-input:focus {
.popover-filter-input-wrapper .popover-filter-input:focus {
background-color: hsl(225deg 6% 7%);
border: 1px solid hsl(0deg 0% 100% / 50%);
box-shadow: 0 0 5px hsl(0deg 0% 100% / 40%);

View File

@ -39,7 +39,8 @@
li.show-more-topics {
& a {
font-size: 12px;
/* 12px at 14px/1em */
font-size: 0.8571em;
}
}
@ -68,21 +69,15 @@ li.show-more-topics {
padding: 0;
font-weight: normal;
.input-append.topic_search_section {
padding: 2px 0 2px
calc(
var(--left-sidebar-topic-indent) -
var(--left-sidebar-topic-resolve-width)
);
margin-bottom: 3px;
margin-left: 3px;
& input {
width: calc(100% - 50px);
}
.topic_search_section {
margin: 3px 0;
.clear_search_button {
margin-left: -1px;
grid-area: row-content;
justify-self: self-end;
/* Override app-component positioning. */
position: static;
padding-right: 4px;
}
}
@ -279,7 +274,8 @@ li.show-more-topics {
align-items: baseline;
& a {
font-size: 12px;
/* 12px at 14px/1em */
font-size: 0.8571em;
}
.unread_count {
@ -716,7 +712,8 @@ li.top_left_scheduled_messages {
.dm-box,
.subscription_block,
.topic-box,
.searching-for-more-topics {
.searching-for-more-topics,
.topic_search_section {
display: grid;
align-items: center;
/* This general pattern of elements applies to every single row in the left
@ -875,10 +872,14 @@ li.top_left_scheduled_messages {
.topic-box,
.searching-for-more-topics {
grid-template-columns:
25px var(--left-sidebar-topic-resolve-width) minmax(0, 1fr) minmax(
0,
max-content
)
var(--left-sidebar-topic-indent) var(--left-sidebar-topic-resolve-width)
minmax(0, 1fr) minmax(0, max-content)
30px 0;
}
.topic_search_section {
grid-template-columns:
var(--left-sidebar-topic-indent) 0 minmax(0, 1fr) minmax(0, max-content)
30px 0;
}
@ -909,6 +910,10 @@ li.top_left_scheduled_messages {
}
}
.topic-list-filter {
grid-area: row-content;
}
.searching-for-more-topics img {
height: 16px;
grid-area: row-content;
@ -916,7 +921,8 @@ li.top_left_scheduled_messages {
.sidebar-topic-check {
grid-area: starting-anchor-element;
font-size: 15px;
/* 15px at 14px/1em */
font-size: 1.0714em;
}
.stream-markers-and-controls,
@ -1179,7 +1185,7 @@ li.topic-list-item {
width: var(--left-sidebar-header-icon-width);
}
.input-append {
.stream_search_section {
grid-area: filter-box;
display: flex;
justify-content: stretch;
@ -1188,7 +1194,7 @@ li.topic-list-item {
line-height: 20px;
white-space: nowrap;
& input {
.stream-list-filter {
/* Use the border-box model so flex
can do its thing despite whatever
padding and border we specify. */
@ -1331,14 +1337,6 @@ li.topic-list-item {
overflow: hidden;
}
.topic-list-filter {
/* Input width = 100% - 30px right-margin - 6px right-padding */
/* To keep the right edge of input along with its borders inline with other
topic items we consider to subtract the space given for right margin of
other items, and right padding of input element. */
width: calc(100% - 36px);
}
.zero_count {
visibility: hidden;
}
@ -1370,7 +1368,8 @@ li.topic-list-item {
display: block;
text-decoration: none;
color: inherit;
font-size: 12px;
/* 12px at 14px/1em */
font-size: 0.8571em;
& span {
display: block;

View File

@ -919,31 +919,6 @@ ul {
outline: 1px solid hsl(0deg 100% 50%);
}
.search-box {
display: flex;
position: sticky;
padding: 2px;
& input {
flex-grow: 1;
margin: 5px;
border-radius: 3px;
}
.clear_search_button {
position: absolute;
top: 5px;
right: 3px;
font-size: 16px;
&:focus {
.fa-remove {
outline: 2px solid var(--color-outline-focus);
}
}
}
}
.giphy-scrolling-container {
overflow: auto;
height: 200px;

View File

@ -171,32 +171,6 @@
.emoji-popover {
width: 250px;
.emoji-popover-top {
position: relative;
padding: 8px 10px;
margin-bottom: 0;
background-color: var(--color-background-emoji-picker-popover);
border-radius: 3px 3px 0 0;
.fa-search {
position: absolute;
color: hsl(0deg 0% 73%);
top: 15px;
left: 17px;
z-index: 3;
}
.emoji-popover-filter {
margin: auto;
padding-left: 22px;
width: calc(100% - 22px - 8px);
font-size: 90%;
}
}
.emoji-popover-category-tabs {
/* Flex needed here to work around #7511 (90% zoom issues in firefox) */
display: flex;

View File

@ -201,6 +201,10 @@ h4.user_group_setting_subsection_title {
th.user-remove-actions {
min-width: 80px;
}
.strikethrough {
text-decoration: line-through;
}
}
}
}

View File

@ -2222,24 +2222,24 @@ body:not(.hide-left-sidebar) {
overscroll-behavior: contain;
}
.dropdown-list-container {
.dropdown-list-search {
display: flex;
.popover-filter-input-wrapper {
display: flex;
.dropdown-list-search-input {
background: var(--color-background-widget-input);
color: var(--color-text-dropdown-input);
width: 100%;
margin: 4px 4px 2px;
.popover-filter-input {
background: var(--color-background-widget-input);
color: var(--color-text-dropdown-input);
width: 100%;
margin: 4px 4px 2px;
&:focus {
background: hsl(0deg 0% 100%);
border: 1px solid hsl(229.09deg 21.57% 10% / 80%);
box-shadow: 0 0 6px hsl(228deg 9.8% 20% / 30%);
}
&:focus {
background: hsl(0deg 0% 100%);
border: 1px solid hsl(229.09deg 21.57% 10% / 80%);
box-shadow: 0 0 6px hsl(228deg 9.8% 20% / 30%);
}
}
}
.dropdown-list-container {
.dropdown-list-wrapper {
/* Sync with `max-height` in dropdown_widget. */
max-height: 210px;

View File

@ -1,6 +1,6 @@
<div class="dropdown-list-container {{widget_name}}-dropdown-list-container">
<div class="dropdown-list-search">
<input class="dropdown-list-search-input filter_text_input{{#if hide_search_box}} hide{{/if}}" type="text" placeholder="{{t 'Filter' }}" autofocus/>
<div class="dropdown-list-search popover-filter-input-wrapper">
<input class="dropdown-list-search-input popover-filter-input filter_text_input{{#if hide_search_box}} hide{{/if}}" type="text" placeholder="{{t 'Filter' }}" autofocus/>
</div>
<div class="dropdown-list-wrapper" data-simplebar data-simplebar-tab-index="-1">
<ul class="dropdown-list"></ul>

View File

@ -1,4 +1,4 @@
<div class="input-append topic_search_section filter-topics">
<div class="topic_search_section filter-topics">
<input class="topic-list-filter home-page-input filter_text_input" id="filter-topic-input" type="text" autocomplete="off" placeholder="{{t 'Filter topics'}}" />
<button type="button" class="btn clear_search_button" id="clear_search_topic_button">
<i class="fa fa-remove" aria-hidden="true"></i>

View File

@ -1,11 +1,8 @@
<div id="giphy_grid_in_popover">
<div class="arrow"></div>
<div class="popover-inner">
<div class="search-box">
<input type="text" tabindex=0 id="giphy-search-query" class="search-query" placeholder="{{t 'Search GIFs' }}" />
<button type="button" class="btn clear_search_button" id="giphy_search_clear">
<i class="fa fa-remove" aria-hidden="true"></i>
</button>
<div class="popover-filter-input-wrapper">
<input type="text" id="giphy-search-query" class="popover-filter-input filter_text_input" placeholder="{{t 'Filter' }}" autofocus/>
</div>
<div class="giphy-scrolling-container" data-simplebar data-simplebar-tab-index="-1">
{{! We need a container we can replace

View File

@ -178,7 +178,7 @@
<span class="masked_unread_count"></span>
</div>
<div class="input-append notdisplayed stream_search_section">
<div class="notdisplayed stream_search_section">
<input class="stream-list-filter home-page-input filter_text_input" type="text" autocomplete="off" placeholder="{{t 'Filter channels' }}" />
<button type="button" class="btn clear_search_button" id="clear_search_stream_button">
<i class="fa fa-remove" aria-hidden="true"></i>

View File

@ -1,8 +1,7 @@
<div class="emoji-picker-popover" data-emoji-destination="{{#if message_id }}reaction{{else if is_status_emoji_popover}}status{{else}}composition{{/if}}">
<div class="emoji-popover">
<div class="emoji-popover-top">
<input class="emoji-popover-filter filter_text_input" type="text" placeholder="{{t 'Search' }}" />
<i class="fa fa-search" aria-hidden="true"></i>
<div class="popover-filter-input-wrapper">
<input id="emoji-popover-filter" class="popover-filter-input filter_text_input" type="text" placeholder="{{t 'Filter' }}" autofocus/>
</div>
<div class="emoji-popover-category-tabs">
{{#each emoji_categories}}

View File

@ -1,22 +1,18 @@
<ul class="nav nav-list">
<li>
<a class="compose_mobile_stream_button">
<i class="fa fa-bullhorn" aria-hidden="true"></i>
{{#if is_in_private_narrow }}
{{t "New channel message" }}
{{else}}
{{t "New topic" }}
{{/if}}
</a>
</li>
<li>
<a class="compose_mobile_direct_message_button">
<i class="fa fa-envelope" aria-hidden="true"></i>
{{#if is_in_private_narrow }}
{{t "New direct message" }}
{{else}}
{{t "New direct message" }}
{{/if}}
</a>
</li>
</ul>
<div class="popover-menu" data-simplebar data-simplebar-tab-index="-1">
<ul role="menu" class="popover-menu-list">
<li role="none" class="link-item popover-menu-list-item">
<a role="menuitem" class="compose_mobile_stream_button popover-menu-link" tabindex="0">
<i class="popover-menu-icon zulip-icon zulip-icon-square-plus" aria-hidden="true"></i>
<span class="popover-menu-label">{{t "Start new conversation" }}</span>
{{popover_hotkey_hints "C"}}
</a>
</li>
<li role="none" class="link-item popover-menu-list-item">
<a role="menuitem" class="compose_mobile_direct_message_button popover-menu-link" tabindex="0">
<i class="popover-menu-icon zulip-icon zulip-icon-mail" aria-hidden="true"></i>
<span class="popover-menu-label">{{t "New direct message" }}</span>
{{popover_hotkey_hints "X"}}
</a>
</li>
</ul>
</div>

View File

@ -5,9 +5,11 @@
{{~! Squash whitespace so that placeholder is displayed when empty. ~}}
</div>
</div>
<div class="add_subscriber_btn_wrapper inline-block">
<button type="submit" name="add_subscriber" class="button add-subscriber-button add-users-button small rounded sea-green" tabindex="0">
{{t 'Add' }}
</button>
</div>
{{#if (not hide_add_button)}}
<div class="add_subscriber_btn_wrapper inline-block">
<button type="submit" name="add_subscriber" class="button add-subscriber-button add-users-button small rounded sea-green" tabindex="0">
{{t 'Add' }}
</button>
</div>
{{/if}}
</div>

View File

@ -1,13 +1,17 @@
<tr>
<td class="panel_user_list">
{{> ../user_display_only_pill display_value=full_name}}
{{> ../user_display_only_pill display_value=full_name strikethrough=soft_removed}}
</td>
{{#if email}}
<td class="subscriber-email">{{email}}</td>
<td class="subscriber-email {{#if soft_removed}} strikethrough {{/if}}">{{email}}</td>
{{else}}
<td class="hidden-subscriber-email">{{t "(hidden)"}}</td>
<td class="hidden-subscriber-email {{#if soft_removed}} strikethrough {{/if}}">{{t "(hidden)"}}</td>
{{/if}}
<td>
<button {{#if disabled}} disabled="disabled"{{/if}} data-user-id="{{user_id}}" class="remove_potential_subscriber button small rounded white">{{t 'Remove' }}</button>
{{#if soft_removed}}
<button data-user-id="{{user_id}}" class="undo_soft_removed_potential_subscriber button small rounded white">{{t 'Add' }}</button>
{{else}}
<button {{#if disabled}} disabled="disabled"{{/if}} data-user-id="{{user_id}}" class="remove_potential_subscriber button small rounded white">{{t 'Remove' }}</button>
{{/if}}
</td>
</tr>

View File

@ -1,6 +1,6 @@
<div class="subscriber_list_settings">
<div class="subscriber_list_add float-left">
{{> add_subscribers_form}}
{{> add_subscribers_form hide_add_button=true}}
</div>
<br />
@ -12,7 +12,7 @@
</div>
<div class="create_stream_subscriber_list_header">
<h4 class="stream_setting_subsection_title">{{t 'Subscribers' }}</h4>
<h4 class="stream_setting_subsection_title">{{t 'Subscribers preview' }}</h4>
<input class="add-user-list-filter filter_text_input" name="user_list_filter" type="text"
autocomplete="off" placeholder="{{t 'Filter subscribers' }}" />
</div>

View File

@ -3,7 +3,7 @@
{{#if img_src}}
<img class="pill-image" src="{{img_src}}" />
{{/if}}
<span class="pill-label">
<span class="pill-label {{#if strikethrough}} strikethrough {{/if}}" >
<span class="pill-value">{{display_value}}</span>
{{#if is_current_user}}<span class="my_user_status">{{t '(you)'}}</span>{{/if}}
{{~#if should_add_guest_user_indicator}}&nbsp;<i>({{t 'guest'}})</i>{{~/if~}}

View File

@ -2204,3 +2204,59 @@ run_test("equals", () => {
),
);
});
run_test("adjusted_terms_if_moved", () => {
// should return null for non-stream messages
let raw_terms = [{operator: "channel", operand: "Foo"}];
let message = {type: "private"};
let result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.strictEqual(result, null);
// should return null if no terms are changed
raw_terms = [{operator: "channel", operand: "general"}];
message = {type: "stream", display_recipient: "general", topic: "discussion"};
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.strictEqual(result, null);
// should adjust channel term to match message's display_recipient
raw_terms = [{operator: "channel", operand: "random"}];
message = {type: "stream", display_recipient: "general", topic: "discussion"};
let expected = [{operator: "channel", operand: "general"}];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
// should adjust topic term to match message's topic
raw_terms = [{operator: "topic", operand: "random"}];
message = {type: "stream", display_recipient: "general", topic: "discussion"};
expected = [{operator: "topic", operand: "discussion"}];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
// should adjust both channel and topic terms when both are different
raw_terms = [
{operator: "channel", operand: "random"},
{operator: "topic", operand: "random"},
];
message = {type: "stream", display_recipient: "general", topic: "discussion"};
expected = [
{operator: "channel", operand: "general"},
{operator: "topic", operand: "discussion"},
];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
// should not adjust terms that are not channel or topic
raw_terms = [
{operator: "channel", operand: "random"},
{operator: "topic", operand: "random"},
{operator: "sender", operand: "alice"},
];
message = {type: "stream", display_recipient: "general", topic: "discussion"};
expected = [
{operator: "channel", operand: "general"},
{operator: "topic", operand: "discussion"},
{operator: "sender", operand: "alice"},
];
result = Filter.adjusted_terms_if_moved(raw_terms, message);
assert.deepStrictEqual(result, expected);
});

View File

@ -78,3 +78,71 @@ test("must_be_subscribed", () => {
assert.ok(!stream_create_subscribers_data.must_be_subscribed(me.user_id));
assert.ok(!stream_create_subscribers_data.must_be_subscribed(test_user101.user_id));
});
test("sync_user_ids", () => {
// sync_user_ids should not remove current user if already present.
stream_create_subscribers_data.initialize_with_current_user();
stream_create_subscribers_data.sync_user_ids([test_user101.user_id, test_user102.user_id]);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [
me.user_id,
test_user101.user_id,
test_user102.user_id,
]);
// sync_user_ids should not add current user if already not present.
stream_create_subscribers_data.remove_user_ids([me.user_id]);
stream_create_subscribers_data.sync_user_ids([test_user101.user_id, test_user102.user_id]);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [
test_user101.user_id,
test_user102.user_id,
]);
});
test("soft remove", () => {
stream_create_subscribers_data.initialize_with_current_user();
stream_create_subscribers_data.add_user_ids([test_user101.user_id, test_user102.user_id]);
stream_create_subscribers_data.soft_remove_user_id(test_user102.user_id);
// sorted_user_ids should still have all the users.
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [
me.user_id,
test_user101.user_id,
test_user102.user_id,
]);
assert.deepEqual(stream_create_subscribers_data.get_principals(), [
me.user_id,
test_user101.user_id,
]);
assert.ok(stream_create_subscribers_data.user_id_in_soft_remove_list(test_user102.user_id));
assert.ok(!stream_create_subscribers_data.user_id_in_soft_remove_list(test_user101.user_id));
// Removing a user_id should also remove them from soft remove list.
stream_create_subscribers_data.remove_user_ids([test_user102.user_id]);
assert.ok(!stream_create_subscribers_data.user_id_in_soft_remove_list(test_user102.user_id));
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [
me.user_id,
test_user101.user_id,
]);
assert.deepEqual(stream_create_subscribers_data.get_principals(), [
me.user_id,
test_user101.user_id,
]);
// Undo soft remove
stream_create_subscribers_data.soft_remove_user_id(test_user101.user_id);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [
me.user_id,
test_user101.user_id,
]);
assert.deepEqual(stream_create_subscribers_data.get_principals(), [me.user_id]);
stream_create_subscribers_data.undo_soft_remove_user_id(test_user101.user_id);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [
me.user_id,
test_user101.user_id,
]);
assert.deepEqual(stream_create_subscribers_data.get_principals(), [
me.user_id,
test_user101.user_id,
]);
});

View File

@ -214,15 +214,23 @@ test("server_history", () => {
history = stream_topic_history.get_recent_topic_names(stream_id);
assert.deepEqual(history, ["hist2", "hist1"]);
// We can try to remove a historical message, but it should
// have no effect.
// Removing message from a topic fetched from server history, will send
// query to the server to get the latest message id in the topic.
let update_topic_called = false;
stream_topic_history.set_update_topic_last_message_id((stream_id, topic_name) => {
assert.equal(stream_id, 66);
assert.equal(topic_name, "hist2");
update_topic_called = true;
});
stream_topic_history.remove_messages({
stream_id,
topic_name: "hist2",
num_messages: 1,
});
assert.equal(update_topic_called, true);
history = stream_topic_history.get_recent_topic_names(stream_id);
assert.deepEqual(history, ["hist2", "hist1"]);
assert.deepEqual(history, ["hist1"]);
stream_topic_history.set_update_topic_last_message_id(noop);
// If we call back to the server for history, the
// effect is always additive. We may decide to prune old

View File

@ -43,7 +43,7 @@ def is_outdated_server(user_profile: Optional[UserProfile]) -> bool:
def pop_numerals(ver: str) -> Tuple[List[int], str]:
match = re.search(r"^( \d+ (?: \. \d+ )* ) (.*)", ver, re.X)
match = re.search(r"^( \d+ (?: \. \d+ )* ) (.*)", ver, re.VERBOSE)
if match is None:
return [], ver
numerals, rest = match.groups()
@ -94,9 +94,9 @@ def version_lt(ver1: str, ver2: str) -> Optional[bool]:
def find_mobile_os(user_agent: str) -> Optional[str]:
if re.search(r"\b Android \b", user_agent, re.I | re.X):
if re.search(r"\b Android \b", user_agent, re.IGNORECASE | re.VERBOSE):
return "android"
if re.search(r"\b(?: iOS | iPhone\ OS )\b", user_agent, re.I | re.X):
if re.search(r"\b(?: iOS | iPhone\ OS )\b", user_agent, re.IGNORECASE | re.VERBOSE):
return "ios"
return None

View File

@ -27,7 +27,7 @@ class IncludeExtension(Extension):
class IncludeBlockProcessor(BlockProcessor):
RE = re.compile(r"^ {,3}\{!([^!]+)!\} *$", re.M)
RE = re.compile(r"^ {,3}\{!([^!]+)!\} *$", re.MULTILINE)
def __init__(self, parser: BlockParser, base_path: str) -> None:
super().__init__(parser)

View File

@ -9,7 +9,7 @@ pattern = re.compile(
(/ (?P<version> [^/ ]* ))?
([ /] .*)?
$""",
re.X,
re.VERBOSE,
)

View File

@ -26,18 +26,18 @@ def process_zcommands(content: str, user_profile: UserProfile) -> Dict[str, Any]
if command == "ping":
return {}
elif command == "night":
if user_profile.color_scheme == UserProfile.COLOR_SCHEME_NIGHT:
elif command == "dark":
if user_profile.color_scheme == UserProfile.COLOR_SCHEME_DARK:
return dict(msg="You are still in dark theme.")
return dict(
msg=change_mode_setting(
setting_name="dark theme",
switch_command="light",
setting="color_scheme",
setting_value=UserProfile.COLOR_SCHEME_NIGHT,
setting_value=UserProfile.COLOR_SCHEME_DARK,
)
)
elif command == "day":
elif command == "light":
if user_profile.color_scheme == UserProfile.COLOR_SCHEME_LIGHT:
return dict(msg="You are still in light theme.")
return dict(

View File

@ -64,4 +64,4 @@ Create default stream groups which the users can choose during sign up.
print(default_stream_group.description)
for stream in default_stream_group.streams.all():
print(stream.name)
print("")
print()

View File

@ -35,7 +35,7 @@ class Command(ZulipBaseCommand):
print(f"{user_profile.delivery_email} has the following active sessions:")
for session in user_sessions(user_profile):
print(session.expire_date, session.get_decoded())
print("")
print()
print(
f"{user_profile.delivery_email} has {get_active_bots_owned_by_user(user_profile).count()} active bots that will also be deactivated."
)

View File

@ -61,15 +61,15 @@ class Command(ZulipBaseCommand):
if options["for_real"]:
do_delete_old_unclaimed_attachments(delta_weeks)
print("")
print()
print("Unclaimed files deleted.")
if options["clean_up_storage"]:
print("")
print()
self.clean_attachment_upload_backend(dry_run=not options["for_real"])
if not options["for_real"]:
print("")
print()
raise CommandError("This was a dry run. Pass -f to actually delete.")
def clean_attachment_upload_backend(self, dry_run: bool = True) -> None:

View File

@ -100,7 +100,7 @@ class Command(ZulipBaseCommand):
tos_prompt = input(
"Do you want to agree to the Zulip Terms of Service and proceed? [Y/n] "
)
print("")
print()
if not (
tos_prompt.lower() == "y" or tos_prompt.lower() == "" or tos_prompt.lower() == "yes"
):

View File

@ -11,7 +11,7 @@ attachment_url_re = re.compile(r"[/\-]user[\-_]uploads[/\.-].*?(?=[ )]|\Z)")
def attachment_url_to_path_id(attachment_url: str) -> str:
path_id_raw = re.sub(r"[/\-]user[\-_]uploads[/\.-]", "", attachment_url)
# Remove any extra '.' after file extension. These are probably added by the user
return re.sub(r"[.]+$", "", path_id_raw, flags=re.M)
return re.sub(r"[.]+$", "", path_id_raw, flags=re.MULTILINE)
def check_and_create_attachments(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:

View File

@ -32,7 +32,7 @@ def fix_topics(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None
# Nothing to do if there are no messages.
return
print("")
print()
while lower_bound < max_id:
print(f"Processed {lower_bound} / {max_id}")
with connection.cursor() as cursor:

View File

@ -30,7 +30,7 @@ def fix_stream_names(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -
if len(realm_ids) == 0:
return
print("")
print()
for realm_id in realm_ids:
print(f"Processing realm {realm_id}")
realm_stream_dicts = Stream.objects.filter(realm_id=realm_id).values("id", "name")

View File

@ -44,7 +44,7 @@ def revoke_invitations(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor)
return confirmation_ids
print("")
print()
for realm_id in Realm.objects.values_list("id", flat=True):
deactivated_user_ids = UserProfile.objects.filter(
is_active=False, realm_id=realm_id

View File

@ -37,7 +37,7 @@ def fix_email_gateway_attachment_owner(
if len(orphan_attachments) == 0:
return
print("")
print()
print(f"Found {len(orphan_attachments)} email gateway attachments to reattach")
for attachment in orphan_attachments:
# We look for the message posted by "Internal" at the same

View File

@ -152,7 +152,7 @@ def log_extra_usermessage_rows(apps: StateApps, schema_editor: BaseDatabaseSchem
)
else:
log_file = sys.stderr
print("", file=log_file)
print(file=log_file)
stack.enter_context(redirect_stdout(log_file))
for message in messages:
@ -189,7 +189,7 @@ def log_extra_usermessage_rows(apps: StateApps, schema_editor: BaseDatabaseSchem
for um in ums:
read = "(read)" if um.flags & 1 else "(unread)"
print(f" {um.user_profile.delivery_email} {read}")
print("")
print()
class Migration(migrations.Migration):

View File

@ -73,9 +73,9 @@ class UserBaseSettings(models.Model):
twenty_four_hour_time = models.BooleanField(default=False)
starred_message_counts = models.BooleanField(default=True)
COLOR_SCHEME_AUTOMATIC = 1
COLOR_SCHEME_NIGHT = 2
COLOR_SCHEME_DARK = 2
COLOR_SCHEME_LIGHT = 3
COLOR_SCHEME_CHOICES = [COLOR_SCHEME_AUTOMATIC, COLOR_SCHEME_NIGHT, COLOR_SCHEME_LIGHT]
COLOR_SCHEME_CHOICES = [COLOR_SCHEME_AUTOMATIC, COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT]
color_scheme = models.PositiveSmallIntegerField(default=COLOR_SCHEME_AUTOMATIC)
# Information density is established through

View File

@ -170,7 +170,7 @@ def render_javascript_code_example(
) -> List[str]:
pattern = rf'^add_example\(\s*"[^"]*",\s*{re.escape(json.dumps(function))},\s*\d+,\s*async \(client, console\) => \{{\n(.*?)^(?:\}}| *\}},\n)\);$'
with open("zerver/openapi/javascript_examples.js") as f:
m = re.search(pattern, f.read(), re.M | re.S)
m = re.search(pattern, f.read(), re.MULTILINE | re.DOTALL)
if m is None:
return []
function_source_lines = dedent(m.group(1)).splitlines()

View File

@ -474,11 +474,11 @@ class ChangeSettingsTest(ZulipTestCase):
self.login("hamlet")
result = self.client_patch(
"/json/settings/display", dict(color_scheme=UserProfile.COLOR_SCHEME_NIGHT)
"/json/settings/display", dict(color_scheme=UserProfile.COLOR_SCHEME_DARK)
)
self.assert_json_success(result)
hamlet = self.example_user("hamlet")
self.assertEqual(hamlet.color_scheme, UserProfile.COLOR_SCHEME_NIGHT)
self.assertEqual(hamlet.color_scheme, UserProfile.COLOR_SCHEME_DARK)
def test_changing_setting_using_notification_setting_endpoint(self) -> None:
"""

View File

@ -1298,7 +1298,7 @@ class UserProfileTest(ZulipTestCase):
do_change_user_setting(cordelia, "emojiset", "twitter", acting_user=None)
do_change_user_setting(cordelia, "timezone", "America/Phoenix", acting_user=None)
do_change_user_setting(
cordelia, "color_scheme", UserProfile.COLOR_SCHEME_NIGHT, acting_user=None
cordelia, "color_scheme", UserProfile.COLOR_SCHEME_DARK, acting_user=None
)
do_change_user_setting(
cordelia, "enable_offline_email_notifications", False, acting_user=None
@ -1342,8 +1342,8 @@ class UserProfileTest(ZulipTestCase):
self.assertEqual(cordelia.timezone, "America/Phoenix")
self.assertEqual(hamlet.timezone, "")
self.assertEqual(iago.color_scheme, UserProfile.COLOR_SCHEME_NIGHT)
self.assertEqual(cordelia.color_scheme, UserProfile.COLOR_SCHEME_NIGHT)
self.assertEqual(iago.color_scheme, UserProfile.COLOR_SCHEME_DARK)
self.assertEqual(cordelia.color_scheme, UserProfile.COLOR_SCHEME_DARK)
self.assertEqual(hamlet.color_scheme, UserProfile.COLOR_SCHEME_AUTOMATIC)
self.assertEqual(iago.enable_offline_email_notifications, False)

View File

@ -27,7 +27,7 @@ class ZcommandTest(ZulipTestCase):
user.color_scheme = UserProfile.COLOR_SCHEME_LIGHT
user.save()
payload = dict(command="/night")
payload = dict(command="/dark")
result = self.client_post("/json/zcommand", payload)
response_dict = self.assert_json_success(result)
self.assertIn("Changed to dark theme", response_dict["msg"])
@ -39,10 +39,10 @@ class ZcommandTest(ZulipTestCase):
def test_day_zcommand(self) -> None:
self.login("hamlet")
user = self.example_user("hamlet")
user.color_scheme = UserProfile.COLOR_SCHEME_NIGHT
user.color_scheme = UserProfile.COLOR_SCHEME_DARK
user.save()
payload = dict(command="/day")
payload = dict(command="/light")
result = self.client_post("/json/zcommand", payload)
response_dict = self.assert_json_success(result)
self.assertIn("Changed to light theme", response_dict["msg"])

View File

@ -17,7 +17,8 @@ if os.path.exists("/etc/zulip/sharding.json"):
data, # backwards compatibility
)
shard_regexes = [
(re.compile(regex, re.I), port) for regex, port in data.get("shard_regexes", [])
(re.compile(regex, re.IGNORECASE), port)
for regex, port in data.get("shard_regexes", [])
]