From f7f6f8703ea0f15ad0bd28f0fe4f37b38b9c37df Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Mon, 21 Oct 2024 02:55:03 +0530 Subject: [PATCH 001/276] scheduled_messages_overlay_ui: Refactor function to not mutate object. Instead of mutating `message_render_context` object with fields, we declare the object once depending on msg_type. This is a prep-commit for migrating the module to TS. --- web/src/scheduled_messages_overlay_ui.js | 43 +++++++++++++++--------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/web/src/scheduled_messages_overlay_ui.js b/web/src/scheduled_messages_overlay_ui.js index 26edb6574b..81005f606c 100644 --- a/web/src/scheduled_messages_overlay_ui.js +++ b/web/src/scheduled_messages_overlay_ui.js @@ -64,23 +64,34 @@ function format(scheduled_messages) { const formatted_msgs = []; const sorted_messages = sort_scheduled_messages(scheduled_messages); for (const msg of sorted_messages) { - const msg_render_context = {...msg}; - if (msg.type === "stream") { - msg_render_context.is_stream = true; - msg_render_context.stream_id = msg.to; - msg_render_context.stream_name = sub_store.maybe_get_stream_name( - msg_render_context.stream_id, - ); - const color = stream_data.get_color(msg_render_context.stream_id); - msg_render_context.recipient_bar_color = stream_color.get_recipient_bar_color(color); - msg_render_context.stream_privacy_icon_color = - stream_color.get_stream_privacy_icon_color(color); - } else { - msg_render_context.is_stream = false; - msg_render_context.recipients = people.get_recipients(msg.to.join(",")); - } + let msg_render_context; const time = new Date(msg.scheduled_delivery_timestamp * 1000); - msg_render_context.formatted_send_at_time = timerender.get_full_datetime(time, "time"); + const formatted_send_at_time = timerender.get_full_datetime(time, "time"); + if (msg.type === "stream") { + const stream_id = msg.to; + const stream_name = sub_store.maybe_get_stream_name(stream_id); + const color = stream_data.get_color(stream_id); + const recipient_bar_color = stream_color.get_recipient_bar_color(color); + const stream_privacy_icon_color = stream_color.get_stream_privacy_icon_color(color); + + msg_render_context = { + ...msg, + is_stream: true, + stream_id, + stream_name, + recipient_bar_color, + stream_privacy_icon_color, + formatted_send_at_time, + }; + } else { + const recipients = people.get_recipients(msg.to.join(",")); + msg_render_context = { + ...msg, + is_stream: false, + recipients, + formatted_send_at_time, + }; + } formatted_msgs.push(msg_render_context); } return formatted_msgs; From 9aa897b8e2661316f77cac44201d6cf4beee0eb8 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Mon, 21 Oct 2024 18:20:19 +0530 Subject: [PATCH 002/276] scheduled_messages_overlay_ui: Return early if msg-id is undefined. --- web/src/scheduled_messages_overlay_ui.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web/src/scheduled_messages_overlay_ui.js b/web/src/scheduled_messages_overlay_ui.js index 81005f606c..657d75de83 100644 --- a/web/src/scheduled_messages_overlay_ui.js +++ b/web/src/scheduled_messages_overlay_ui.js @@ -24,11 +24,11 @@ export const keyboard_handling_context = { return scheduled_messages_ids; }, on_enter() { - const focused_element_id = Number.parseInt( - messages_overlay_ui.get_focused_element_id(this), - 10, - ); - scheduled_messages_ui.edit_scheduled_message(focused_element_id); + const focused_element_id = messages_overlay_ui.get_focused_element_id(this); + if (focused_element_id === undefined) { + return; + } + scheduled_messages_ui.edit_scheduled_message(Number.parseInt(focused_element_id, 10)); overlays.close_overlay("scheduled"); }, on_delete() { @@ -114,6 +114,9 @@ export function launch() { $messages_list.append($(rendered_list)); const first_element_id = keyboard_handling_context.get_items_ids()[0]; + if (first_element_id === undefined) { + return; + } messages_overlay_ui.set_initial_element(first_element_id, keyboard_handling_context); } From ee52a7b15587331263061debea2d18b752cabf86 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Mon, 21 Oct 2024 21:39:18 +0530 Subject: [PATCH 003/276] scheduled_messages_overlay_ui: Convert module to TypeScript. --- tools/test-js-with-node | 2 +- ...ui.js => scheduled_messages_overlay_ui.ts} | 63 +++++++++++++------ 2 files changed, 46 insertions(+), 19 deletions(-) rename web/src/{scheduled_messages_overlay_ui.js => scheduled_messages_overlay_ui.ts} (75%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 9e38314ffd..61fb25a7e3 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -199,7 +199,7 @@ EXEMPT_FILES = make_set( "web/src/saved_snippets_ui.ts", "web/src/scheduled_messages.ts", "web/src/scheduled_messages_feed_ui.ts", - "web/src/scheduled_messages_overlay_ui.js", + "web/src/scheduled_messages_overlay_ui.ts", "web/src/scheduled_messages_ui.ts", "web/src/scroll_bar.ts", "web/src/scroll_util.ts", diff --git a/web/src/scheduled_messages_overlay_ui.js b/web/src/scheduled_messages_overlay_ui.ts similarity index 75% rename from web/src/scheduled_messages_overlay_ui.js rename to web/src/scheduled_messages_overlay_ui.ts index 657d75de83..fc16ce67d2 100644 --- a/web/src/scheduled_messages_overlay_ui.js +++ b/web/src/scheduled_messages_overlay_ui.ts @@ -1,4 +1,5 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; import render_scheduled_message from "../templates/scheduled_message.hbs"; import render_scheduled_messages_overlay from "../templates/scheduled_messages_overlay.hbs"; @@ -8,12 +9,30 @@ import * as messages_overlay_ui from "./messages_overlay_ui"; import * as overlays from "./overlays"; import * as people from "./people"; import * as scheduled_messages from "./scheduled_messages"; +import type {ScheduledMessage} from "./scheduled_messages"; import * as scheduled_messages_ui from "./scheduled_messages_ui"; import * as stream_color from "./stream_color"; import * as stream_data from "./stream_data"; import * as sub_store from "./sub_store"; import * as timerender from "./timerender"; +type ScheduledMessageRenderContext = ScheduledMessage & + ( + | { + is_stream: true; + formatted_send_at_time: string; + recipient_bar_color: string; + stream_id: number; + stream_name: string; + stream_privacy_icon_color: string; + } + | { + is_stream: false; + formatted_send_at_time: string; + recipients: string; + } + ); + export const keyboard_handling_context = { get_items_ids() { const scheduled_messages_ids = []; @@ -40,7 +59,7 @@ export const keyboard_handling_context = { messages_overlay_ui.focus_on_sibling_element(this); // We need to have a super responsive UI feedback here, so we remove the row from the DOM manually $focused_row.remove(); - scheduled_messages.delete_scheduled_message(focused_element_id); + scheduled_messages.delete_scheduled_message(Number.parseInt(focused_element_id, 10)); }, items_container_selector: "scheduled-messages-container", items_list_selector: "scheduled-messages-list", @@ -49,20 +68,25 @@ export const keyboard_handling_context = { id_attribute_name: "data-scheduled-message-id", }; -function sort_scheduled_messages(scheduled_messages) { +function sort_scheduled_messages( + scheduled_messages: Map, +): ScheduledMessage[] { const sorted_messages = [...scheduled_messages.values()].sort( (msg1, msg2) => msg1.scheduled_delivery_timestamp - msg2.scheduled_delivery_timestamp, ); return sorted_messages; } -export function handle_keyboard_events(event_key) { +export function handle_keyboard_events(event_key: string): void { messages_overlay_ui.modals_handle_events(event_key, keyboard_handling_context); } -function format(scheduled_messages) { +function format( + scheduled_messages: Map, +): ScheduledMessageRenderContext[] { const formatted_msgs = []; const sorted_messages = sort_scheduled_messages(scheduled_messages); + for (const msg of sorted_messages) { let msg_render_context; const time = new Date(msg.scheduled_delivery_timestamp * 1000); @@ -74,9 +98,10 @@ function format(scheduled_messages) { const recipient_bar_color = stream_color.get_recipient_bar_color(color); const stream_privacy_icon_color = stream_color.get_stream_privacy_icon_color(color); + assert(stream_name !== undefined); msg_render_context = { ...msg, - is_stream: true, + is_stream: true as const, stream_id, stream_name, recipient_bar_color, @@ -87,7 +112,7 @@ function format(scheduled_messages) { const recipients = people.get_recipients(msg.to.join(",")); msg_render_context = { ...msg, - is_stream: false, + is_stream: false as const, recipients, formatted_send_at_time, }; @@ -97,7 +122,7 @@ function format(scheduled_messages) { return formatted_msgs; } -export function launch() { +export function launch(): void { $("#scheduled_messages_overlay_container").html(render_scheduled_messages_overlay()); overlays.open_overlay({ name: "scheduled", @@ -117,10 +142,10 @@ export function launch() { if (first_element_id === undefined) { return; } - messages_overlay_ui.set_initial_element(first_element_id, keyboard_handling_context); + messages_overlay_ui.set_initial_element(String(first_element_id), keyboard_handling_context); } -export function rerender() { +export function rerender(): void { if (!overlays.scheduled_messages_open()) { return; } @@ -132,7 +157,7 @@ export function rerender() { $messages_list.append($(rendered_list)); } -export function remove_scheduled_message_id(scheduled_msg_id) { +export function remove_scheduled_message_id(scheduled_msg_id: number): void { if (overlays.scheduled_messages_open()) { $( `#scheduled_messages_overlay .scheduled-message-row[data-scheduled-message-id=${scheduled_msg_id}]`, @@ -140,12 +165,12 @@ export function remove_scheduled_message_id(scheduled_msg_id) { } } -export function initialize() { +export function initialize(): void { $("body").on("click", ".scheduled-message-row .restore-overlay-message", (e) => { - let scheduled_msg_id = $(e.currentTarget) - .closest(".scheduled-message-row") - .attr("data-scheduled-message-id"); - scheduled_msg_id = Number.parseInt(scheduled_msg_id, 10); + const scheduled_msg_id = Number.parseInt( + $(e.currentTarget).closest(".scheduled-message-row").attr("data-scheduled-message-id")!, + 10, + ); scheduled_messages_ui.edit_scheduled_message(scheduled_msg_id); overlays.close_overlay("scheduled"); e.stopPropagation(); @@ -156,13 +181,15 @@ export function initialize() { const scheduled_msg_id = $(e.currentTarget) .closest(".scheduled-message-row") .attr("data-scheduled-message-id"); - scheduled_messages.delete_scheduled_message(scheduled_msg_id); + assert(scheduled_msg_id !== undefined); + + scheduled_messages.delete_scheduled_message(Number.parseInt(scheduled_msg_id, 10)); e.stopPropagation(); e.preventDefault(); }); - $("body").on("focus", ".scheduled-message-info-box", (e) => { - messages_overlay_ui.activate_element(e.target, keyboard_handling_context); + $("body").on("focus", ".scheduled-message-info-box", function (this: HTMLElement) { + messages_overlay_ui.activate_element(this, keyboard_handling_context); }); } From 49c283660507300c83b8c0ea738a93a180bd5930 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Thu, 10 Oct 2024 07:05:32 +0000 Subject: [PATCH 004/276] message_scroll: Mark message as read after blue box moves past it. Fixes #31833 Quoting from the issue: Some of the reasoning behind that proposal was: We want the first unread message to be what gets highlighted by the blue box, since that's what you should read first. It's bad to eat one unread message when entering a message feed via N or otherwise. A consistent algorithm would be that messages get marked as read when you move the blue box past them ... except that there'd be no way to mark the last message that way. Because the bottom being visible marks things as read, it should be fine to make this change now, even though there wouldn't currently be a way to use the location of the blue box to mark the last message in the current view as read. --- web/src/message_scroll.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/web/src/message_scroll.js b/web/src/message_scroll.js index 50267e9183..54a585390b 100644 --- a/web/src/message_scroll.js +++ b/web/src/message_scroll.js @@ -157,12 +157,23 @@ export function initialize() { if (event.mark_read && event.previously_selected_id !== -1) { // Mark messages between old pointer and new pointer as read - let messages; if (event.id < event.previously_selected_id) { - messages = event.msg_list.message_range(event.id, event.previously_selected_id); - } else { - messages = event.msg_list.message_range(event.previously_selected_id, event.id); + // We don't mark messages as read when the pointer moves up. + return; } + + const messages = event.msg_list.message_range(event.previously_selected_id, event.id); + // If the user just arrived at the message `event.id`, we don't mark it as read. + // We only mark messages as read when the pointer moves past the message. + // This is likely the last message in the list. So, we loop through the messages + // in reverse order to find the message. + for (let i = messages.length - 1; i >= 0; i -= 1) { + if (messages[i].id === event.id) { + delete messages[i]; + break; + } + } + if (event.msg_list.can_mark_messages_read()) { unread_ops.notify_server_messages_read(messages, {from: "pointer"}); } else if ( From 3ff6a89fe6b21f69845efc0e9a1a0477308b8da9 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Mon, 21 Oct 2024 13:01:24 +0000 Subject: [PATCH 005/276] message_scroll: Fix last message not marked as read on `end` keypress. Since we removed `unread_ops.process_visible` on system initiated scrolling in #32038, this is important to take care of separately. Reproducer: Go to a topic with a lot of unread messages and press `end` key. Last message is not marked as read. --- web/src/message_scroll.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/message_scroll.js b/web/src/message_scroll.js index 54a585390b..dba55a6565 100644 --- a/web/src/message_scroll.js +++ b/web/src/message_scroll.js @@ -163,12 +163,13 @@ export function initialize() { } const messages = event.msg_list.message_range(event.previously_selected_id, event.id); - // If the user just arrived at the message `event.id`, we don't mark it as read. + // If the user just arrived at the message `event.id`, we don't mark it as read + // unless it is the last message in the list. // We only mark messages as read when the pointer moves past the message. // This is likely the last message in the list. So, we loop through the messages // in reverse order to find the message. for (let i = messages.length - 1; i >= 0; i -= 1) { - if (messages[i].id === event.id) { + if (messages[i].id === event.id && event.id !== event.msg_list.last()?.id) { delete messages[i]; break; } From 5686233699b557c20afacd20457535362afa14c9 Mon Sep 17 00:00:00 2001 From: bedo Date: Thu, 17 Oct 2024 13:52:16 +0300 Subject: [PATCH 006/276] message_header: Add date on every recipient bar in search_results. Fixes #31958 --- web/src/message_list_view.ts | 15 +++++++++++++++ web/templates/recipient_row.hbs | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/web/src/message_list_view.ts b/web/src/message_list_view.ts index ee62d2b3bf..25b951ac48 100644 --- a/web/src/message_list_view.ts +++ b/web/src/message_list_view.ts @@ -106,6 +106,7 @@ export type MessageGroup = { topic_url: string | undefined; user_can_resolve_topic: boolean; visibility_policy: number | false; + always_display_date: boolean; } | { is_stream: false; @@ -114,6 +115,7 @@ export type MessageGroup = { is_private: true; pm_with_url: string; recipient_users: RecipientRowUser[]; + always_display_date: boolean; } ); @@ -425,6 +427,14 @@ function maybe_restore_focus_to_message_edit_form(): void { }, 0); } +function is_search_view(): boolean { + const current_filter = narrow_state.filter(); + if (current_filter && !current_filter.supports_collapsing_recipients()) { + return true; + } + return false; +} + type SubscriptionMarkers = { bookend_top: boolean; stream_name: string; @@ -443,6 +453,9 @@ function populate_group_from_message( const message_group_id = _.uniqueId("message_group_"); const date = get_group_display_date(message, year_changed); + // Each searched message is a self-contained result, + // so we always display date in the recipient bar for those messages. + const always_display_date = is_search_view(); if (is_stream) { assert(message.type === "stream"); // stream messages have string display_recipient @@ -501,6 +514,7 @@ function populate_group_from_message( topic_is_resolved, visibility_policy, all_visibility_policies, + always_display_date, }; } // Private message group @@ -520,6 +534,7 @@ function populate_group_from_message( pm_with_url: message.pm_with_url, recipient_users: get_users_for_recipient_row(message), display_reply_to_for_tooltip: message_store.get_pm_full_names(user_ids), + always_display_date, }; } diff --git a/web/templates/recipient_row.hbs b/web/templates/recipient_row.hbs index a17b7c9cce..f053a94e2c 100644 --- a/web/templates/recipient_row.hbs +++ b/web/templates/recipient_row.hbs @@ -76,7 +76,7 @@ {{/if}} - {{{date}}} + {{{date}}} {{else}} @@ -96,7 +96,7 @@ {{/tr~}} - {{{date}}} + {{{date}}} {{/if}} From 8e9c592ce39c7c13f97fe533fabd3d2cb5c970db Mon Sep 17 00:00:00 2001 From: evykassirer Date: Tue, 22 Oct 2024 15:46:59 -0700 Subject: [PATCH 007/276] search: Only move cursor to end of selection if cursor is there already. Fixes bug reported here: https://chat.zulip.org/#narrow/channel/9-issues/topic/Broken.20links.20to.20previous.20messages.3F/near/196462 --- web/src/search.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/search.ts b/web/src/search.ts index 5b5085d24c..c83bb46826 100644 --- a/web/src/search.ts +++ b/web/src/search.ts @@ -13,6 +13,7 @@ import * as search_pill from "./search_pill"; import type {SearchPillWidget} from "./search_pill"; import * as search_suggestion from "./search_suggestion"; import type {NarrowTerm} from "./state_data"; +import * as util from "./util"; // Exported for unit testing export let is_using_input_method = false; @@ -24,8 +25,12 @@ let on_narrow_search: OnNarrowSearch; function set_search_bar_text(text: string): void { $("#search_query").text(text); - // After setting the text, move the cursor to the end of the line. - window.getSelection()!.modify("move", "forward", "line"); + const current_selection = window.getSelection()!; + if (current_selection.anchorNode?.isSameNode(util.the($("#search_query")))) { + // After setting the text, move the cursor to the end of the line if + // the cursor is in the search bar. + current_selection.modify("move", "forward", "line"); + } } function get_search_bar_text(): string { From 389b851f81b8ad907e9a9cbd7a57210fb4e633e3 Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Tue, 3 Sep 2024 21:41:18 +0200 Subject: [PATCH 008/276] update_user_backend: Allow authorized org owners to change user emails. This adds a new special UserProfile flag can_change_user_emails(disabled by default) and the ability for changing the email address of users in the realm via update_user_backend. This is useful for allowing organizations to update user emails without needing to set up a SCIM integration, but since it gives the ability to hijack user accounts, it needs to be behind this additional permission and can't be just given to organization owners by default. Analogical to how the create_user_backend endpoint works. --- tools/test-api | 9 ++-- version.py | 2 +- zerver/actions/users.py | 22 ++++++++ .../management/commands/change_user_role.py | 15 +++++- ...0616_userprofile_can_change_user_emails.py | 17 +++++++ zerver/models/users.py | 2 + zerver/openapi/zulip.yaml | 10 ++++ zerver/tests/test_users.py | 51 +++++++++++++++++++ zerver/views/users.py | 28 +++++++++- 9 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 zerver/migrations/0616_userprofile_can_change_user_emails.py diff --git a/tools/test-api b/tools/test-api index 73fa6acd1b..b3b2310973 100755 --- a/tools/test-api +++ b/tools/test-api @@ -59,7 +59,8 @@ with test_server_running( # Prepare the admin client email = "iago@zulip.com" # Iago is an admin realm = get_realm("zulip") - user = get_user(email, realm) + iago = get_user(email, realm) + user = iago # Iago needs permission to manage all user groups. admins_group = NamedUserGroup.objects.get( @@ -69,9 +70,10 @@ with test_server_running( realm, "can_manage_all_groups", admins_group, acting_user=None ) - # Required to test can_create_users endpoints. + # Required to test can_create_users and can_change_user_emails endpoints. user.can_create_users = True - user.save(update_fields=["can_create_users"]) + user.can_change_user_emails = True + user.save(update_fields=["can_create_users", "can_change_user_emails"]) api_key = get_api_key(user) site = "http://zulip.zulipdev.com:9981" @@ -85,6 +87,7 @@ with test_server_running( email = "desdemona@zulip.com" # desdemona is an owner realm = get_realm("zulip") user = get_user(email, realm) + api_key = get_api_key(user) site = "http://zulip.zulipdev.com:9981" owner_client = Client( diff --git a/version.py b/version.py index c0c47a6a89..3e74f3ec6a 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 312 # Last bumped for adding 'realm_export_consent' event type. +API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id} # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/actions/users.py b/zerver/actions/users.py index 81ae6a0ef7..ac8fb55c61 100644 --- a/zerver/actions/users.py +++ b/zerver/actions/users.py @@ -749,6 +749,28 @@ def do_change_can_create_users(user_profile: UserProfile, value: bool) -> None: ) +@transaction.atomic(savepoint=False) +def do_change_can_change_user_emails(user_profile: UserProfile, value: bool) -> None: + event_time = timezone_now() + old_value = user_profile.can_change_user_emails + + user_profile.can_change_user_emails = value + user_profile.save(update_fields=["can_change_user_emails"]) + + RealmAuditLog.objects.create( + realm=user_profile.realm, + event_type=AuditLogEventType.USER_SPECIAL_PERMISSION_CHANGED, + event_time=event_time, + acting_user=None, + modified_user=user_profile, + extra_data={ + RealmAuditLog.OLD_VALUE: old_value, + RealmAuditLog.NEW_VALUE: value, + "property": "can_change_user_emails", + }, + ) + + @transaction.atomic(durable=True) def do_update_outgoing_webhook_service( bot_profile: UserProfile, service_interface: int, service_payload_url: str diff --git a/zerver/management/commands/change_user_role.py b/zerver/management/commands/change_user_role.py index e0c49256b1..b8ff9518e5 100644 --- a/zerver/management/commands/change_user_role.py +++ b/zerver/management/commands/change_user_role.py @@ -6,6 +6,7 @@ from django.core.management.base import CommandError from typing_extensions import override from zerver.actions.users import ( + do_change_can_change_user_emails, do_change_can_create_users, do_change_can_forge_sender, do_change_is_billing_admin, @@ -23,6 +24,7 @@ ROLE_CHOICES = [ "guest", "can_forge_sender", "can_create_users", + "can_change_user_emails", "is_billing_admin", ] @@ -65,7 +67,12 @@ ONLY perform this on customer request from an authorized person. "guest": UserProfile.ROLE_GUEST, } - if options["new_role"] not in ["can_forge_sender", "can_create_users", "is_billing_admin"]: + if options["new_role"] not in [ + "can_forge_sender", + "can_create_users", + "can_change_user_emails", + "is_billing_admin", + ]: new_role = user_role_map[options["new_role"]] if not options["grant"]: raise CommandError( @@ -109,6 +116,12 @@ ONLY perform this on customer request from an authorized person. elif not user.can_create_users and not options["grant"]: raise CommandError("User can't create users for this realm.") do_change_can_create_users(user, options["grant"]) + elif options["new_role"] == "can_change_user_emails": + if user.can_change_user_emails and options["grant"]: + raise CommandError("User can already change user emails for this realm.") + elif not user.can_change_user_emails and not options["grant"]: + raise CommandError("User can't change user emails for this realm.") + do_change_can_change_user_emails(user, options["grant"]) else: assert options["new_role"] == "is_billing_admin" if user.is_billing_admin and options["grant"]: diff --git a/zerver/migrations/0616_userprofile_can_change_user_emails.py b/zerver/migrations/0616_userprofile_can_change_user_emails.py new file mode 100644 index 0000000000..b29d1ca843 --- /dev/null +++ b/zerver/migrations/0616_userprofile_can_change_user_emails.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.8 on 2024-09-02 20:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0615_system_bot_avatars"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="can_change_user_emails", + field=models.BooleanField(db_index=True, default=False), + ), + ] diff --git a/zerver/models/users.py b/zerver/models/users.py index b32e3efe4c..406271b38f 100644 --- a/zerver/models/users.py +++ b/zerver/models/users.py @@ -571,6 +571,8 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): can_forge_sender = models.BooleanField(default=False, db_index=True) # Users with this flag set can create other users via API. can_create_users = models.BooleanField(default=False, db_index=True) + # Users with this flag can change email addresses of users in the realm via the API. + can_change_user_emails = models.BooleanField(default=False, db_index=True) # Used for rate-limiting certain automated messages generated by bots last_reminder = models.DateTimeField(default=None, null=True) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index f3b3762193..268d9394c8 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -12779,6 +12779,16 @@ paths: type: object example: [{"id": 4, "value": "0"}, {"id": 5, "value": "1909-04-05"}] + new_email: + description: | + New email address for the user. Requires the user making + the request to be an organization administrator and + additionally have the `.can_change_user_emails` special + permission. + + **Changes**: New in Zulip 10.0 (feature level 313). + type: string + example: username@example.com encoding: role: contentType: application/json diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index cb51ea802d..bbdf86fa96 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -23,6 +23,7 @@ from zerver.actions.user_settings import bulk_regenerate_api_keys, do_change_use from zerver.actions.user_topics import do_set_user_topic_visibility_policy from zerver.actions.users import ( change_user_is_active, + do_change_can_change_user_emails, do_change_can_create_users, do_change_can_forge_sender, do_change_is_billing_admin, @@ -1120,6 +1121,56 @@ class BulkCreateUserTest(ZulipTestCase): ) +class AdminChangeUserEmailTest(ZulipTestCase): + def test_change_user_email_backend(self) -> None: + cordelia = self.example_user("cordelia") + realm_admin = self.example_user("iago") + self.login_user(realm_admin) + + valid_params = dict(new_email="cordelia_new@zulip.com") + + self.assertEqual(realm_admin.can_change_user_emails, False) + result = self.client_patch(f"/json/users/{cordelia.id}", valid_params) + self.assert_json_error(result, "User not authorized to change user emails") + + do_change_can_change_user_emails(realm_admin, True) + # can_change_user_emails is insufficient without being a realm administrator: + do_change_user_role(realm_admin, UserProfile.ROLE_MEMBER, acting_user=None) + result = self.client_patch(f"/json/users/{cordelia.id}", valid_params) + self.assert_json_error(result, "Insufficient permission") + + do_change_user_role(realm_admin, UserProfile.ROLE_REALM_ADMINISTRATOR, acting_user=None) + result = self.client_patch( + f"/json/users/{cordelia.id}", + dict(new_email="invalid"), + ) + self.assert_json_error(result, "Invalid new email address.") + + result = self.client_patch( + f"/json/users/{UserProfile.objects.latest('id').id + 1}", + dict(new_email="new@zulip.com"), + ) + self.assert_json_error(result, "No such user") + + result = self.client_patch( + f"/json/users/{cordelia.id}", + dict(new_email=realm_admin.delivery_email), + ) + self.assert_json_error(result, "New email value error: Already has an account.") + + result = self.client_patch(f"/json/users/{cordelia.id}", valid_params) + self.assert_json_success(result) + + cordelia.refresh_from_db() + self.assertEqual(cordelia.delivery_email, "cordelia_new@zulip.com") + + last_realm_audit_log = RealmAuditLog.objects.last() + assert last_realm_audit_log is not None + self.assertEqual(last_realm_audit_log.event_type, AuditLogEventType.USER_EMAIL_CHANGED) + self.assertEqual(last_realm_audit_log.modified_user, cordelia) + self.assertEqual(last_realm_audit_log.acting_user, realm_admin) + + class AdminCreateUserTest(ZulipTestCase): def test_create_user_backend(self) -> None: # This test should give us complete coverage on diff --git a/zerver/views/users.py b/zerver/views/users.py index ce69de8feb..b16f84a298 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -4,6 +4,8 @@ from typing import Annotated, Any, TypeAlias from django.conf import settings from django.contrib.auth.models import AnonymousUser +from django.core import validators +from django.core.exceptions import ValidationError from django.core.files.uploadedfile import UploadedFile from django.db import transaction from django.http import HttpRequest, HttpResponse @@ -26,6 +28,7 @@ from zerver.actions.user_settings import ( check_change_bot_full_name, check_change_full_name, do_change_avatar_fields, + do_change_user_delivery_email, do_regenerate_api_key, ) from zerver.actions.users import ( @@ -39,7 +42,7 @@ from zerver.decorator import require_member_or_admin, require_realm_admin from zerver.forms import PASSWORD_TOO_WEAK_ERROR, CreateUserForm from zerver.lib.avatar import avatar_url, get_avatar_for_inaccessible_user, get_gravatar_url from zerver.lib.bot_config import set_bot_config -from zerver.lib.email_validation import email_allowed_for_realm +from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm from zerver.lib.exceptions import ( CannotDeactivateLastUserError, JsonableError, @@ -207,11 +210,17 @@ def update_user_backend( full_name: str | None = None, role: Json[RoleParamType] | None = None, profile_data: Json[list[ProfileDataElement]] | None = None, + new_email: str | None = None, ) -> HttpResponse: target = access_user_by_id( user_profile, user_id, allow_deactivated=True, allow_bots=True, for_admin=True ) + if new_email is not None and ( + not user_profile.can_change_user_emails or not user_profile.is_realm_admin + ): + raise JsonableError(_("User not authorized to change user emails")) + if role is not None and target.role != role: # Require that the current user has permissions to # grant/remove the role in question. @@ -261,6 +270,23 @@ def update_user_backend( ) do_update_user_custom_profile_data_if_changed(target, clean_profile_data) + if new_email is not None and target.delivery_email != new_email: + assert user_profile.can_change_user_emails and user_profile.is_realm_admin + try: + validators.validate_email(new_email) + except ValidationError: + raise JsonableError(_("Invalid new email address.")) + try: + validate_email_not_already_in_realm( + user_profile.realm, + new_email, + verbose=False, + ) + except ValidationError as e: + raise JsonableError(_("New email value error: {message}").format(message=e.message)) + + do_change_user_delivery_email(target, new_email, acting_user=user_profile) + return json_success(request) From 77e7a2d30f5ec1250bd146885fb0ba393690e19b Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Thu, 5 Sep 2024 01:51:50 +0200 Subject: [PATCH 009/276] users: Add API endpoint to update_user_backend by real email. The old endpoint for updating a user worked only via user id. Now we add a different entry to this functionality, fetching the user by .delivery_email. update_user_backend becomes the main function handling all the logic, invoked by the two endpoints. --- api_docs/include/rest-endpoints.md | 1 + version.py | 2 +- zerver/openapi/curl_param_value_generators.py | 15 +- zerver/openapi/zulip.yaml | 164 ++++++++++++------ zerver/tests/test_users.py | 57 ++++++ zerver/views/users.py | 48 ++++- zproject/urls.py | 7 +- 7 files changed, 232 insertions(+), 62 deletions(-) diff --git a/api_docs/include/rest-endpoints.md b/api_docs/include/rest-endpoints.md index 69618ce4cd..eb9f006102 100644 --- a/api_docs/include/rest-endpoints.md +++ b/api_docs/include/rest-endpoints.md @@ -66,6 +66,7 @@ * [Get all users](/api/get-users) * [Create a user](/api/create-user) * [Update a user](/api/update-user) +* [Update a user by email](/api/update-user-by-email) * [Deactivate a user](/api/deactivate-user) * [Deactivate own user](/api/deactivate-own-user) * [Reactivate a user](/api/reactivate-user) diff --git a/version.py b/version.py index 3e74f3ec6a..d0ee0f4da6 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id} +API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id} and the new PATCH /users/{email} endpoint # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/openapi/curl_param_value_generators.py b/zerver/openapi/curl_param_value_generators.py index 7d0ecc416d..107388e9a8 100644 --- a/zerver/openapi/curl_param_value_generators.py +++ b/zerver/openapi/curl_param_value_generators.py @@ -23,7 +23,7 @@ from zerver.lib.upload import upload_message_attachment from zerver.lib.users import get_api_key from zerver.models import Client, Message, NamedUserGroup, UserPresence from zerver.models.realms import get_realm -from zerver.models.users import get_user +from zerver.models.users import UserProfile, get_user from zerver.openapi.openapi import Parameter GENERATOR_FUNCTIONS: dict[str, Callable[[], dict[str, object]]] = {} @@ -252,6 +252,19 @@ def create_user() -> dict[str, object]: } +@openapi_param_value_generator(["/users/{email]:patch", "/users/{user_id}:patch"]) +def new_email_value() -> dict[str, object]: + count = 0 + exists = True + while exists: + email = f"new{count}@zulip.com" + exists = UserProfile.objects.filter(delivery_email=email).exists() + count += 1 + return { + "new_email": email, + } + + @openapi_param_value_generator(["/user_groups/create:post"]) def create_user_group_data() -> dict[str, object]: return { diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 268d9394c8..8d712299ee 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -12634,6 +12634,52 @@ paths: "delivery_email": null, }, } + patch: + operationId: update-user-by-email + summary: Update a user by email + tags: ["users"] + x-requires-administrator: true + description: | + Administrative endpoint to update the details of another user in the organization by their email address. + Works the same way as [`PATCH /users/{user_id}`](/api/update-user) but fetching the target user by their + real email address. + + The requester needs to have permission to view the target user's real email address, subject to the + user's email address visibility setting. Otherwise, the dummy address of the format + `user{id}@{realm.host}` needs be used. This follows the same rules as `GET /users/{email}`. + + **Changes**: New in Zulip 10.0 (feature level 313). + parameters: + - name: email + in: path + required: true + description: | + The email address of the user, specified following the same rules as + [`GET /users/{email}`](/api/get-user-by-email). + schema: + type: string + example: hamlet@zulip.com + requestBody: + $ref: "#/components/requestBodies/UpdateUser" + responses: + "200": + $ref: "#/components/responses/SimpleSuccess" + + "400": + description: Bad request. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CodedError" + - example: + { + "result": "error", + "msg": "Guests cannot be organization administrators", + "code": "BAD_REQUEST", + } + description: | + A typical unsuccessful JSON response: /users/{user_id}: get: operationId: get-user @@ -12738,62 +12784,7 @@ paths: parameters: - $ref: "#/components/parameters/UserId" requestBody: - required: false - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - full_name: - description: | - The user's full name. - - **Changes**: Removed unnecessary JSON-encoding of this parameter in - Zulip 5.0 (feature level 106). - type: string - example: NewName - role: - description: | - New [role](/api/roles-and-permissions) for the user. Roles are encoded as: - - - Organization owner: 100 - - Organization administrator: 200 - - Organization moderator: 300 - - Member: 400 - - Guest: 600 - - Only organization owners can add or remove the owner role. - - The owner role cannot be removed from the only organization owner. - - **Changes**: New in Zulip 3.0 (feature level 8), replacing the previous - pair of `is_admin` and `is_guest` boolean parameters. Organization moderator - role added in Zulip 4.0 (feature level 60). - type: integer - example: 400 - profile_data: - description: | - A dictionary containing the to be updated custom profile field data for the user. - type: array - items: - type: object - example: - [{"id": 4, "value": "0"}, {"id": 5, "value": "1909-04-05"}] - new_email: - description: | - New email address for the user. Requires the user making - the request to be an organization administrator and - additionally have the `.can_change_user_emails` special - permission. - - **Changes**: New in Zulip 10.0 (feature level 313). - type: string - example: username@example.com - encoding: - role: - contentType: application/json - profile_data: - contentType: application/json + $ref: "#/components/requestBodies/UpdateUser" responses: "200": @@ -24475,6 +24466,67 @@ components: allOf: - $ref: "#/components/schemas/IgnoredParametersSuccess" + ################ + # Shared request bodies + ################ + requestBodies: + UpdateUser: + required: false + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + full_name: + description: | + The user's full name. + + **Changes**: Removed unnecessary JSON-encoding of this parameter in + Zulip 5.0 (feature level 106). + type: string + example: NewName + role: + description: | + New [role](/api/roles-and-permissions) for the user. Roles are encoded as: + + - Organization owner: 100 + - Organization administrator: 200 + - Organization moderator: 300 + - Member: 400 + - Guest: 600 + + Only organization owners can add or remove the owner role. + + The owner role cannot be removed from the only organization owner. + + **Changes**: New in Zulip 3.0 (feature level 8), replacing the previous + pair of `is_admin` and `is_guest` boolean parameters. Organization moderator + role added in Zulip 4.0 (feature level 60). + type: integer + example: 400 + profile_data: + description: | + A dictionary containing the updated custom profile field data for the user. + type: array + items: + type: object + example: + [{"id": 4, "value": "0"}, {"id": 5, "value": "1909-04-05"}] + new_email: + description: | + New email address for the user. Requires the user making the request + to be an organization owner and additionally have the `.can_change_user_emails` + special permission. + + **Changes**: New in Zulip 10.0 (feature level 285). + type: string + example: username@example.com + encoding: + role: + contentType: application/json + profile_data: + contentType: application/json + #################### # Shared parameters #################### diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index bbdf86fa96..d898093105 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -1121,6 +1121,63 @@ class BulkCreateUserTest(ZulipTestCase): ) +class UpdateUserByEmailEndpointTest(ZulipTestCase): + def test_update_user_by_email(self) -> None: + self.login("iago") + hamlet = self.example_user("hamlet") + do_change_user_setting( + hamlet, + "email_address_visibility", + UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE, + acting_user=None, + ) + + result = self.client_patch( + f"/json/users/{hamlet.delivery_email}", + dict(full_name="Newname"), + ) + self.assert_json_success(result) + hamlet.refresh_from_db() + self.assertEqual(hamlet.full_name, "Newname") + + do_change_user_setting( + hamlet, + "email_address_visibility", + UserProfile.EMAIL_ADDRESS_VISIBILITY_MEMBERS, + acting_user=None, + ) + result = self.client_patch( + f"/json/users/{hamlet.delivery_email}", + dict(full_name="Newname2"), + ) + self.assert_json_success(result) + hamlet.refresh_from_db() + self.assertEqual(hamlet.full_name, "Newname2") + + do_change_user_setting( + hamlet, + "email_address_visibility", + UserProfile.EMAIL_ADDRESS_VISIBILITY_NOBODY, + acting_user=None, + ) + result = self.client_patch( + f"/json/users/{hamlet.delivery_email}", + dict(full_name="Newname2"), + ) + self.assert_json_error(result, "No such user") + + # The dummy email can be used, when we don't have access + # to the target's email address. + dummy_email = hamlet.email + result = self.client_patch( + f"/json/users/{dummy_email}", + dict(full_name="Newname3"), + ) + self.assert_json_success(result) + hamlet.refresh_from_db() + self.assertEqual(hamlet.full_name, "Newname3") + + class AdminChangeUserEmailTest(ZulipTestCase): def test_change_user_email_backend(self) -> None: cordelia = self.example_user("cordelia") diff --git a/zerver/views/users.py b/zerver/views/users.py index b16f84a298..6a45fefef4 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -202,7 +202,7 @@ class ProfileDataElement(BaseModel): @typed_endpoint @transaction.atomic(durable=True) -def update_user_backend( +def update_user_by_id_api( request: HttpRequest, user_profile: UserProfile, *, @@ -215,7 +215,53 @@ def update_user_backend( target = access_user_by_id( user_profile, user_id, allow_deactivated=True, allow_bots=True, for_admin=True ) + return update_user_backend( + request, + user_profile, + target, + full_name=full_name, + role=role, + profile_data=profile_data, + new_email=new_email, + ) + +@typed_endpoint +@transaction.atomic(durable=True) +def update_user_by_email_api( + request: HttpRequest, + user_profile: UserProfile, + *, + email: PathOnly[str], + full_name: str | None = None, + role: Json[RoleParamType] | None = None, + profile_data: Json[list[ProfileDataElement]] | None = None, + new_email: str | None = None, +) -> HttpResponse: + target = access_user_by_email( + user_profile, email, allow_deactivated=True, allow_bots=True, for_admin=True + ) + return update_user_backend( + request, + user_profile, + target, + full_name=full_name, + role=role, + profile_data=profile_data, + new_email=new_email, + ) + + +def update_user_backend( + request: HttpRequest, + user_profile: UserProfile, + target: UserProfile, + *, + full_name: str | None = None, + role: Json[RoleParamType] | None = None, + profile_data: Json[list[ProfileDataElement]] | None = None, + new_email: str | None = None, +) -> HttpResponse: if new_email is not None and ( not user_profile.can_change_user_emails or not user_profile.is_realm_admin ): diff --git a/zproject/urls.py b/zproject/urls.py index 49f1c9710b..0703e9f104 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -232,7 +232,8 @@ from zerver.views.users import ( patch_bot_backend, reactivate_user_backend, regenerate_bot_api_key, - update_user_backend, + update_user_by_email_api, + update_user_by_id_api, ) from zerver.views.video_calls import ( complete_zoom_user, @@ -316,11 +317,11 @@ v1_api_and_json_patterns = [ rest_path( "users/", GET=get_members_backend, - PATCH=update_user_backend, + PATCH=update_user_by_id_api, DELETE=deactivate_user_backend, ), rest_path("users//subscriptions/", GET=get_subscription_backend), - rest_path("users/", GET=get_user_by_email), + rest_path("users/", GET=get_user_by_email, PATCH=update_user_by_email_api), rest_path("bots", GET=get_bots_backend, POST=add_bot_backend), rest_path("bots//api_key/regenerate", POST=regenerate_bot_api_key), rest_path("bots/", PATCH=patch_bot_backend, DELETE=deactivate_bot_backend), From e04e8c3019605122a3141b1ac7b2e750f4dd334a Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Tue, 22 Oct 2024 20:58:06 +0200 Subject: [PATCH 010/276] update_user_backend: Add API changelog entries. --- api_docs/changelog.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 6b5288807a..4ff1285387 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,17 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 313** + +* [`PATCH /users/{user_id}`](/api/update-user): Added `new_email` field to + allow updating the email address of the target user. The requester must be + an organization administrator and have the `can_change_user_emails` special + permission. +* [`PATCH /users/{email}`](/api/update-user-by-email): Added new endpoint, + which is a copy of [`PATCH /users/{user_id}`](/api/update-user), but the user + is specified by their email address, following the same rules as [`GET + /users/{email}`](/api/get-user-by-email). + **Feature level 312** * [`GET /events`](/api/get-events): Added `realm_export_consent` event From f7750a07a2481fc1d039a19c93932db67dedd44c Mon Sep 17 00:00:00 2001 From: Sayam Samal Date: Mon, 21 Oct 2024 02:43:36 +0530 Subject: [PATCH 011/276] modals: Fix focus advancement in move topic modal. Previously, when selecting a new channel in the "move topic" modal using the keyboard, focus failed to advance to the topic input automatically. Users had to press the tab key an extra time to move focus to the topic input, which was not the intended behavior. This commit modifies the `move_topic_on_update` function in `web/src/stream_popover.js` to explicitly set focus on the topic input field after a channel is selected. --- web/src/stream_popover.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/stream_popover.js b/web/src/stream_popover.js index 7623a16976..4157eb6e99 100644 --- a/web/src/stream_popover.js +++ b/web/src/stream_popover.js @@ -547,6 +547,9 @@ export async function build_move_topic_to_stream_popover( dropdown.hide(); event.preventDefault(); event.stopPropagation(); + + // Move focus to the topic input after a new stream is selected. + $("#move_topic_form .move_messages_edit_topic").trigger("focus"); } function move_topic_post_render() { From 6956947a7379b1fd041a2d216c3010639e6d07e5 Mon Sep 17 00:00:00 2001 From: Kislay Udbhav Verma Date: Sat, 19 Oct 2024 21:53:56 +0530 Subject: [PATCH 012/276] topic_link_util: Add `&` as a character which can produce broken links. `&` in topic names like `©` can result in broken stream topic links. So we generate fallback markdown links for them too. This is a follow up to #30071. --- web/src/topic_link_util.ts | 4 +++- web/tests/topic_link_util.test.js | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/topic_link_util.ts b/web/src/topic_link_util.ts index f56c914622..95bc1412cb 100644 --- a/web/src/topic_link_util.ts +++ b/web/src/topic_link_util.ts @@ -4,7 +4,7 @@ import * as internal_url from "../shared/src/internal_url"; import * as stream_data from "./stream_data"; -const invalid_stream_topic_regex = /[*>`]|(\$\$)/g; +const invalid_stream_topic_regex = /[`>*&]|(\$\$)/g; export function will_produce_broken_stream_topic_link(word: string): boolean { return invalid_stream_topic_regex.test(word); @@ -24,6 +24,8 @@ export function escape_invalid_stream_topic_characters(text: string): string { return ">"; case "*": return "*"; + case "&": + return "&"; case "$$": return "$$"; default: diff --git a/web/tests/topic_link_util.test.js b/web/tests/topic_link_util.test.js index 905e5db0c3..b38910fd33 100644 --- a/web/tests/topic_link_util.test.js +++ b/web/tests/topic_link_util.test.js @@ -83,6 +83,11 @@ run_test("stream_topic_link_syntax_test", () => { "[#$$MONEY$$](#narrow/channel/6-.24.24MONEY.24.24)", ); + assert.equal( + topic_link_util.get_stream_topic_link_syntax("#**Sweden>&ab", "&ab"), + "[#Sweden>&ab](#narrow/channel/1-Sweden/topic/.26ab)", + ); + // Only for full coverage of the module. assert.equal(topic_link_util.escape_invalid_stream_topic_characters("Sweden"), "Sweden"); }); From 81c4e45d01df115ce7211f9b687c3ef810cec9b2 Mon Sep 17 00:00:00 2001 From: Kislay Udbhav Verma Date: Sat, 19 Oct 2024 13:34:56 +0530 Subject: [PATCH 013/276] topic_link_util: Use different escape sequence for backticks. Instead of "`", we use "`" to prevent an lxml parsing bug. --- web/src/topic_link_util.ts | 2 +- web/tests/topic_link_util.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/topic_link_util.ts b/web/src/topic_link_util.ts index 95bc1412cb..320f36e689 100644 --- a/web/src/topic_link_util.ts +++ b/web/src/topic_link_util.ts @@ -19,7 +19,7 @@ function get_stream_name_from_topic_link_syntax(syntax: string): string { export function escape_invalid_stream_topic_characters(text: string): string { switch (text) { case "`": - return "`"; + return "`"; case ">": return ">"; case "*": diff --git a/web/tests/topic_link_util.test.js b/web/tests/topic_link_util.test.js index b38910fd33..d1ba594390 100644 --- a/web/tests/topic_link_util.test.js +++ b/web/tests/topic_link_util.test.js @@ -47,11 +47,11 @@ run_test("stream_topic_link_syntax_test", () => { ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>t", "test `test` test"), - "[#Sweden>test `test` test](#narrow/channel/1-Sweden/topic/test.20.60test.60.20test)", + "[#Sweden>test `test` test](#narrow/channel/1-Sweden/topic/test.20.60test.60.20test)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Denmark>t", "test `test` test`s"), - "[#Denmark>test `test` test`s](#narrow/channel/2-Denmark/topic/test.20.60test.60.20test.60s)", + "[#Denmark>test `test` test`s](#narrow/channel/2-Denmark/topic/test.20.60test.60.20test.60s)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>typeah", "error due to *"), From 516d1ab82bedcfbe52800bca21f2affff41737b5 Mon Sep 17 00:00:00 2001 From: PieterCK Date: Tue, 22 Oct 2024 23:05:32 +0700 Subject: [PATCH 014/276] avatar: Ensure system bots' avatar URLs follow convention. Previously, requesting system bots URLs did not return any -medium.png variants and SVG file was also used for notification bots' avatar, which was problematic. In this commit, the -medium.png variants is added for the avatars of system bots and zulip-icon-square.svg is also converted into notification-bot.png for the notification bot. The get_avatar_url method has been updated to return the "medium" file variants for the system bots. Additionally, the system bots' avatar files is moved to a dedicated directory to simplify the hashing logic for these files. Now, all files in the "images/static_avatars/" directory will be hashed. --- static/images/email-gateway-bot.png | Bin 160852 -> 0 bytes .../static_avatars/emailgateway-medium.png | Bin 0 -> 160025 bytes static/images/static_avatars/emailgateway.png | Bin 0 -> 5651 bytes .../notification-bot-medium.png | Bin 0 -> 28115 bytes .../static_avatars/notification-bot.png | Bin 0 -> 4875 bytes .../static_avatars/welcome-bot-medium.png | Bin 0 -> 23074 bytes static/images/static_avatars/welcome-bot.png | Bin 0 -> 9065 bytes static/images/welcome-bot.png | Bin 81296 -> 0 bytes zerver/lib/avatar.py | 32 ++++++++++++------ zerver/lib/storage.py | 8 ++--- zerver/tests/test_upload.py | 23 +++++++++++++ 11 files changed, 49 insertions(+), 14 deletions(-) delete mode 100644 static/images/email-gateway-bot.png create mode 100644 static/images/static_avatars/emailgateway-medium.png create mode 100644 static/images/static_avatars/emailgateway.png create mode 100644 static/images/static_avatars/notification-bot-medium.png create mode 100644 static/images/static_avatars/notification-bot.png create mode 100644 static/images/static_avatars/welcome-bot-medium.png create mode 100644 static/images/static_avatars/welcome-bot.png delete mode 100644 static/images/welcome-bot.png diff --git a/static/images/email-gateway-bot.png b/static/images/email-gateway-bot.png deleted file mode 100644 index ce10f48fc2d2eaaddf03e06198ce0e804c53b2a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160852 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 ZyB=u#&N0YpHI#hvWppHaND|@ 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%0DqloO=l|_K_Gh-V_Pl-#xiF*lP%Qv?Yv=z^kQ>v z+KKV%%5?ITJbc`Og-Pw%tnqS7EIlg0+JklhX`|!4v(U`;Av!VL_&w>F$I|42G-za8 zDAL!j|3o7P^Hz^&mR-zyB8t&@#lMtp;O7=OgzuBs?tW~9*T$Wz0l3QL@C1(N8xTMR zuH!9_#K1wxyLbQ2NQAp%euAewjJRrdlChPVNuFY?48rhG(F#h-k>>nxKjjEMy#FIh z)7>zy`+%^}Xk`;&qOP?<{K=&$Aya=82Hzc`%?<3e(H7YEQ}vhXgi{IGf_(Ic2EG}B zn`u;kzZl{!($c|OPkxA3XaZTdJD>{3l*%8bjiZuUNXa&39dqjI2#jT z5CTBPsjFiPx;>3DH?5J_Qlj>;EEP}_jmDVVy1)j|={%^lXKzw1uo%~|Jh@9Y%XA$W zVcm)V<8IDgnj(c+DV5kj6wBA(*0QU-aB7Mw_Z|W1c4gwwytet9)(Rp6+F)oi(bL_$u# zu3JkKXW4WaXp&GP9Yuxf+Ou-wbXwpWl`rO+6OY5x$6@yBl=fD2hsoh%9vIqmv!Uw~WJj^q&9ZA5>ffr5H#ruB$qx;9?P_9PB?Fh05rA z5%}nmKVKIjm1^+eIIOp$@AHojrN2e=R zJ;n3dPS>l%?V^$yj>{V8CWFcSp}(C|-C$*R|G!JlmCBC;-o< zdBD{7)J{!@wXS!qv!U8N%_#H~fx+m3f#UyHsU!ivJ4v}o4O$V*0=m{?cOS++@blU( zj*~v1cqU$OwF*$!gis|KN48r_uTjhfqQa2e7-?`alg7* z9_#N%U_y!aDcAnk%4(NgE(#HB#3T8=9ZIqjlC=eo z)C>3CAgx4L%f?Vs%w|~jq;)K;uo#v2M=+-=RtrYC_qMJVo2Hl!lbBeY2*u@}uUy=p z6IDN|RRi>{wzazH5Qi>RJ3*mpWy~3*=vkxwg>iNxXw>!vPWNa;m~f)tT@YyeEX$-& zVxJ3Fqf~um8T@-IPh=RwWk3=zp4*y^x%(gm4vq2*PNlEIEv0!iW>k4raGAr!G&o+y z<6_$`GP0S?K5=xYPnv9D<%|1qu7wqp#EDKYLI5Vkr4TIi*>jzyqh_=$dcVBNN~LNH zIZaZ!P8hY5lPt$9#Xrb4hlyAYJvPVQ)x=NMV-1H>Ow~?KqtOm=P8~VRk$1RG3AV*g z<8oYwui#At6;6fG1YfsFeb+I2OA4G(;yU zP-C%5>;o(ma0A&zg^yTekYfKrnW!ZLsxQH4sfb@dXfya9ckbA)2-dhglM#$(tP2w) z?K$c(x;(y}S($co`3&2<0AF8pUA@jS?{eOhHDr) z9%fE!9o~V28-&OLQ-O?g^&0`vq#WM>IG&a}W$o>MMGR=VJyUKb5QD;|;Ijtxs95sz ziFE@0Ao2z{8xG1O`9FXf)`(onKvcyIkLo+v z@R~Ui9;s!qTfKr067+(hOs@`~!bOT%lkFnM0_A$HU)FyZz%O_fj$`AAegbdS?;k__ z6;^!0Fl?tTRC`b|cut^PybNvryzHTXd-LNaa;795rFiJjg3%T}BtT-_o7=}YWeu?y zjJVLjnjJfF-6?F$K=&0p{1|fIUCP-TpxBk#0rGi$@bf3)%4gs)EFa-cbgU$NXUTz- z)u>a2`b)=gidX@>*|5Zj>XGH1uvvvY9+1o%6cwCixV$>elcOe6KS}tQ7x|YS79@L8 zoYa>A+lYRNN}0M**}r9KT~eXiZCHRierDl(4P6`X;O)=&`lz!lWm1id2RVDhi9$hP zg4nGPYa%bHcQj*t1LOCVO_$_I>Som(t7Wkc%q}P@ca$-(;WEb-=~?mvH2jK6Xb4V) z?+U`EvQ6N@O9lO0bYy+7eXyQAX=rgg%PTn)9Md<87#^?8L!wt*FFqMQuj%WF+#v5Q z@8DOdUp56xWv}h&>Ri{ChS6@-pVo*N06Rk?7U4Fp`XEd zS6BzSAAn<2Y|s6K;---#Al95D76=91f$|fIMxPxUe&5HI>#>y~>Jo!Y2>1;k?*n$< z+u!S1-_=W7ACR9l)Yt16Xs^j_3`7JySc=Zr}ya(b|?c4tJy%e z?R&DNz&awC{S-lG7^>DY??wzTFGPVEWGjY7uxK`jN2Z_{Aan%U4AFHuG@YgcKa@eVJ|TH=3oYk)OQS?084Yi!FeO4g00Pj z{tom)wxTPbAUEtR?R8i>+Zm&=5i4Pe9NVUpKc^AzX=+Lts^8*&ue95PZcsbar$^mO zDVk}PuDqYly={;J{j${)UaAd;u@jlb3ENdx9<_BevtHiUG7JLy%GNp7VEMdR*=QU| zVr>4sFO%EuqyiTKGietldL_D-MB3g{fvX3gB2zZY;AgO>Vv*>k6EV}XjF-yPfU-;j z7Q;ZBE#4Fy?vpCjOh<=e|481TKzEE*LXiT~=CFF*diR;;0@3EGvJ|XL-d)(@R$R{Q z<+AHb1-*d&BgB^wbpO7)jT*=xI;4J*nJTKcHG z$vY{eE6N6tC!8u3Pw0ALPu}(&J={i?>Z8kM{(RJ8`@Bnye*0+HUGk5oaqCjFsLW7X z8K50yaTRjd4zC;WE?3I)QzDseGEp@HLO2H+!!5#C%_S)n;Yj@3JrX33CFjvB7g&r{ z8fse3nMNdl)e?}Iz!C~kUS1+AMXs?Tk7CGMYm{?y$ULVS%^eN=xQ9#0K*K>E%d9xp zN+eZ$p-|L4Qhm)z-_#Aw+YeXqH+y)ueh-EHdBk`}PzUU! z+U-I9jonwTkH{g&o(GoTK3D4QmL9$Vs#bVc^n1A<6`V8T1+`o);RMqgQbbk+QxWRc zl8m>)RiCGzypg))YK$}%+Nrp;mc8e|wzn?E2S8IN@6-UL|F#>ihVX9Jb^v%iAYWB= zgLr;;e$h-Dk!QFI$LV7ue)k0S?YwVb1b`WWCGJzUyNG%vKyM~%?WMl?B;&jvDN+;qt4Gm@*&98aBCVKm|llO!@1-*X~>Vnd4^8qwzI9C)?E>J#4oDWbqxn zKEFbK-r4*eNBxN^L4|e0#TzY-$AoGK`;-PE zua5i@R8krt$~rh`m53Fa!hu)R%_gWgW3hxVd+NiMx7s#DN4_&ORHb{;&&Qx#e$01$ zziD>gZm@c8fo_v;I`MZvNqdqZWn6;ksXGy^#hpW7z1FEL~``lb$rc*NzUH zI~Bz4L9|0x0@GoZwJ3s1%J=ein^aZHv0<-;_1RsX(*McskCSd$dS|^~ZdUP6%kUf< zS{Mu%Z3!ggJio?)(VF6ZdGLohsddg}8&nu&lPG^{%LbKf{}K;jx-Ohl#wbEPx@gqQ z1t~+3coKGuBlhS6v1<=j{01&0VwP?#runwcTBH7G| zvG@hB)3uCnfIg9>a9j9*YkMI-{t<=v-h=|N%_PQ+ z>xT=NZcEvCIYti8R$OYcTYqoJ_042E`Tlf=(Dm#-ue0cx28^;{qn7cP5y)ONCp$4J zCa4)YV+B00dm?g|##$9JSDTaLEhWZiWQrim7X?#J*|7(zd5G?`v`v+e^G*S(W3=vM_+r)c<57Snip{UNCbmf=i%h zG&2>Qw%e1}6}0yENBneYZSp3)p7e2ZJ>#3YPny9x=6trn_SK*oi zurk5T3F@5w>5x$DnEVE>9g%$oQ*q94K0v>~PJU)jr!GlWZ7)1*oGNF*RMzaXDC>7x zz|}aRu7o33gQ^Xn@dS8&T(_sI)7T4tSq%f91C-N+|9X?dvgyfa19|r>f7HEU)UyIa z37QuQ{-feGHHx@1bAXG|aa|&jPG1a0SvcVt9ob&k;Qp~Skh04}m9ak7@Ba5L8Cyx^ zDC#fFd-=U`eu#Y_61d!Py!UYFOUJtCq|wY24%suz&txgA?mOBipV!uvtq<`}9JPLN z>v<@gb)JDJ`f?VqOP5oL_%o&MGKV?f&D}lzHe4ywQezH3hH8Mh?x>S-O_1dECL(ik zjoLN+^R>6gVjZg_C@zkj=VV5fe@Tjn8IdUvdsn!S$BLPxsm;AQ*z#=@pIX?miD>OZ z6d(JS!*4CI*nZ;LogPxyr++~{a!WPi?Z*dJk->=6NGl}ju%WM2vJ&Hg_o(j1i zWB!y-_=z2u%SHSOuIuh%g~Of9ksJunNX|b3S`W1cb1hcHUwzb?m+5McrhMC2W4DD= zZM~0yVHG^7|BYzD!l+I;G@XIkiEjUqy)fms(*eV62lhI!1E~ASwcBjTO@VFWUyvmo z(e98FoY$@|sWy|~uqaDok`XdBrj!|lrN%W2NUga-t3U9<+=;S@ZN_>Pe0}TeCF;(P z65@R3B38eW`zqK=1isNU)Yd!$OP^^DUOdu8oes8!nqivUinTI@w00!sknfWP{uX6_ z2fx?h4<>qaquNpi7J{S0V7#t=xH`IIY#;^7Zz~`yM&=!;@-C(HG zOFppSUF1C4IRQZaUt(vaYv^{$C(FCQoxn$i)Hkn1pi|s|Y zyAsB2YEnV6d92OitcoX1xOwWQ(jymKlXU97N}f&X$e0QUsE%%4NO2V3+6uAuVL336 zXc5A1!bqwx8&CGDz5nBxdL5{gkMD^ zU&d(UQfL$fpXEQ3GX+H{{1Kj)tNnsd7CAvT2^?I#Tz9b7rBaP~l-ozI;F{$qCSED&Np==E^pMuIv-0!=pt3%S!>CGa zZIsX|TUv%qHXz^^&r zvbfINzC{LSbsTkN($D1Kj_BlXF~JMc&anK)0!bUSE|4LJnr9_L!DMtxIiiCX#MZ7Bd!g8vLhGmQvF8V?}OCD+zlu`CLV|gX7LTz(CTA7GR&-Lhg zDzNX&(Bveh^XF{`yXS+^>-iNaE(HpZU@>%z`5oja&XD|E1{BU4+sj0AM|ui)u5Y=M ziMb=YWf28%Bw0mSK01MNJ~xKE^U(n&KQR)js2MUjl!h*$yOyWXcm;5$EMIi9YzXcb zf9f5lk;Tm(uDsS+v>=Dpxq${doJLy?aT7{oyYB{sFHIB?DQQ?K!c`qZ_)9zSo>~4@ zz&HKJ^kiT>>7Wg7ZM8PLF2BzRBw}K)KdGbLw!#dNSlh5A>i+2Z;tMLZ)JG@8Q`N2!#;T#c<3yh9Yamr1&wRt5u^b0%*3nYJnmNf16G<% z1rJT#rAyY!6GRW8w+<e z`q3h|^0qWVrlV9aZYUz0!cEL6O$KZDv?rsZY-R4D^ebUCX6OFY3`=Nw8E}a8+Qq7h zi-W5PT3sj!16eVqSLdNzEN+jw^(Z8*&f%D}rk}-F*346A#!sHn1;K~HXu_rebH-zU zl+&XeJQ)iwI+spTl{bD)2C_^a0SHQ<8bM)@b48Qta!2D|axDtJdmlCY z#zkJ5fy&+C$wMCWtR)vglRTj-_sMoQ;h$luX+fU71;12vy%4&B1;|ppZY_elZ>(5@ zSX7Qw3NO3&#bj;M-}1;V6Y=4)v2QYsBQM$p4&O!XgAW4#+vg3x`yt@up5cw_dBT)2 z2$-5|;Y}88^$0$X*_Uo$>&g&2G%Ep}nejAG=j}A4VeOY-SS3|&+GZ0S)OA7$>% zm4j)jHB4;Nuo;_d11YT3*2!~`OY0b0_dE$_{TgC| zE$^Mum}iWwKqTd4R-mF@4D#Ee4*+~>ZrTe}JL9Nr5?jOp7`^~HXEGIdcwLOv-vLpN+@T?$*Y|k)fzGm(v@X1@64Ig=vQ;{s_W%n76ssh7D|>50`%)4*254A`3HIx<21 zc-agz+<*iU)!6Z!-Fl{O7RAleM-88S+VIo^j{opA;EUf3Htnu7@j&QSi_OD{>=hN- z27X2K$N(~>miOgLGDsVP(eenDA@WhHR9?N1MEx0HdPIeuwo2~Cl&`Yb_8c_%leXkt zX+}6^4q69T)rnM1G-@7Zssn3DUMuOTp`HxL^HpG{Pj{~W7#bg1=pmy5czMaZcGXhP zb)F06W@DHDttD8_(aat79&;s@R$Fg6W>)>7f)q;+otchmpSp`hhwjuA2zchymfH#= z%hVUW8%SyuImmY##knYx4{M-2T=K z-+txhtQ=wa@HwEMPLkc^=I;Xh!wPV90(!k9BuhFpa##Z~AG1TIXHd6cz?fLs}#gt;i^^>uH)TE3E55J#<=+ z^GxZWk2kg={P<#+JS|+UI$fS|&MrxHIWN=tEmt-JIHqo+Vls9uJh%K z)>&6Uw*CL)y=k*u*O4W*^1k~p5&%bVo<|WJL`~GJ5?NgxU9NIecYo?`mm?hE{ulo0 z@NaN991geL;c#@=UD4%ky9P-mRh2|a6eUs|0nUQ}4j>5-6M^?``a|Ygnfp9Y6LImt z>Jud19nRT9?#z`dGxs^SW$u))ZotFB+ov1<#z*dt45O%JiOPjtxwVep=_xmyqtCd&JoNWDUmF^_N}+MAQKVB(H*8Di-Jzen%-oA`KGj@mYPY*RwMMbz&XZY zqI#l^>RDxo1?$=%*&5R5jps~MgRR*HdC1Ek^s>Bb#kJXl_r}l+h!PN*XRt?b=%eVo zfPJ*5s?g$%;XqB%j3D4`%7c+kvvv8YmStEx`N2$Sc&6x488T`| zieAJ}MTdlq&~lO&xVQJrZj8rJqt4_+q3EX){~UlX;s>0rztr%RTY<0K0_@tZNe*ND z2YQI~Q7;Amm4hA}tEWL)qm%u;I4;^|_=A0py}JzG zdltU`E!gqX4yIrGrIXj?EaC%mFkmS=N?^$ z`NeV&-KQ#IsLv|M84s(|W!pnByaq)4GhDnA_SM^gtM(Ya|GeYJF9B!Pf!VCv z$Z&*mc;Omhrx2G0W2BG`nQ~1_MYN-VQC1eUyz5Mi-}9%f^E9;qM%h#lRa^+WW`;$2 zUW~rf4bTpy&@}2PEiMhWXR)O*y;iLnZ)J07vS)MJZC%RF7z+b%Ju$316CsIGaJ4;z zPc^IkSw#<+oPZXGjR*)?FXYl^rM9?GrGPzykRjit-9yUnmph`2pqjJ66yL`nd7ogV zyvNne_eM#89-jjoOsj)WQ=?vqpe=rC@NePp^#yF}9Qf*Oz}IgFZog8$j+Ra2`fn1> z#2Dgl$G#=vUaZx)&`t9)^&RV!0jZBUU6aU~o?{srvSd?iSFeDW@!=Z+i z<1)`ps9d^e!GKn>fXhzM6qUcV(E1QRZ$Q~l!yv~r$l6NxP1o6gyu4n9>XYx5ahL&X z>(LRTU-T^vn{W&p!pyOK)o}MUj%}-kYxV#?*bn^j1Ndy_n6D_isnxJnu__eAfJ@ze zl12?f&@1T#%Y(!^on=P1?92*J^l>&`L@e{BlIdZaE1(Xbr_0)GayLvhr-0Uq2gnqx zx~~tR`e$>O+?1D7&T%(8D2zzCbltBV-dc}vhmrx;Xm~uEmdZL$N%9b8QhUz~ZVQ8U z0VLEZ>_810yq#JRe^eY6&AB!L-V76cZk6U2GSp;B>N({PYwRfYhy}1b}s%k4m^I(pqb-h;o|>k;A#cArw=Ja2pHq?Az#@h zuJ=_EYw((;>&lJPXcK6%!UqGe2vStOJi)5w$ zg_mRU(m5@H>2Ir)GS53wF|lgbXu2(d@m>>kW!;$2rOu;N%LJ~&sFL&3pL=~3d%(sO z?E{4_X~CWACKc0UslRQHON!sDISWF*d9%aJ^f5GWsBp-wGW3IV&~D=>6A#+FImIM` zs`(AhjqIo47x@PBW~MQ4&ErKw#cX*T4F`~aRD-6^PW1GyUDND=rvR+2^rr*=7?oeL z!*RP=Gf^NV!$$K4EIH>5eZT{oJNAEle|fBb7jjw*$#0tG;ol2(ryBD-J#t8-Zc93n!t42 zN#0{f>_6cqhF`n4!Ur|scy-tvjykqPZk?W%%;|gaR5yjl<>6{y2K$AkLjqMq z?n1@CqzFEn8Me;h-?+o=p}T>r_LL@i62Z+n(sKbw9f#q}I`HmM$6r1jiuK?vz@8mT z8KV}5)>5|WHLjF?-Pj3d5MG3=>nA+PrV(30aV}u|gZsRcS`8sbSUIl%j2WB;4_ePq zvC2GRPRP=>Cghv)G)Rh?tCM{&0s&lUurM0-(Nj2t<(6VhGF}sEC?k+pB*k-=c&x4f z$3AjA_KM-DH{Aa8tA_ioUv8hD^tkR4;9q{l@Qphh|Kr~n4jqlfU*Yowjf|YjXSEW} zXKbY47ieox*LR($JWCut=2Me|RV11yuT|`14_yd5$C1mgFy{K?c@z z-?~q=K0&1IUplliPsrS^iYjmesUOccN)0R@DJvOxlA^uCVd5&x0r2`5Oy)7 zBCRIn2}ubYq@4~`=z`&FaG)dhB}YYb<^y|4q2){qpls4!pVK74y<`_Y9+`H*sWXnZ zj++0Q9|HGZ@AmcE4L4uW7+dj_^L~u^=-7+3ZPjq|6^4KPdyYq6H2cZR@IxQCZQrud zN197LlV3tAo*^_9%G)*U(Hg*6dnMtpp;~Wdg~gJ+0c}g6wG=4tA8zULmYB|o!;R79 z#EsI~vVYJ{NH{@7yV{ByJbQaiR^D63V+&xvD$zn5t(XBMi@^c-W zQNZA2pi@Z(T6EB+#24cuRtj#wRAI<5NzWqIM<|+h?#!1@ckEh28%e*`y2h+P%X0E% z@iijJ+;CywACHBHW4!uevoGBQJa{YY<|_m^kufE#%E>Cv-Yk;|6sf2C}*M=Z^ffH5=xV zi&N1@`B!?`6krerZ|~dWGU?!6SJPfKI&V*@qxle+G>1%Bbgt6zu$gtrN}qafOsMF6 zrX6Pc>vMnPa91LT^jF@69XkoUcg+3%8{=UR@w?@lTUY178m(Duio5eF!=CNH#XH=7 z@{;*$?*VJE6W(S`8KHEw4ND*L&r~ia$C3*47*Bdxn&7xNWJM@llfk%eTsO zVHI6))_TCkFjgi+=rGNDbeat-sp*553tP-aS$jA;tLTWsHBC<5R7;`?X7pK(0#2Jq;M z@L#;)_TF*$wpGJyMy_=}2UvH8G#@K2$^YcrPB3089mur5+$su)`bsor12Pcp?MZReT)c%(Ww!vP z*~#?()+WE98n{Gf&OW?`wR4N%!&8nQy=ZvuP-wxOR|A({WSFmn$YQLU=iJMw$NQ`H zI3B*+aQQ{RBQF9kzw0=10$5)FGpprH_tKRd6Pr%eWT54xN~NR7Oc>Ag_8~P}x@j!D z>ffegd0FjFaT=-fI!|R%=fig4@;5folj&y;_1#?ra=gL`#t9Jc5BO7Qq`auw8i|W! z$`HtT<8~S!KZQK$Fs!(DoPJQ5Jnv&3`srPtg+pB`Uv9SZi+2FG?*)GU9@yR28fLXOe$&bRAUZQ!Te!V* z6n@|k@c;h8@%nqP#lo;-OPOF6L6(KF#qmh7m<*}gE75VF-j}}_=vX@W)T0)wG`+H( z+IMUEw0Zak*qr>LIOzaVgq9vQI!{T7xi%j8fMIp3X%4>S!&rOD1VWC^nm|yOqB?Ah zI7(?{9pcFPNBHc+a@1^h363;*(M<_~?wamQ7SEGF1* z79*de;jwe8`&Vx_yW=Xycb^3wdC~lx52?dlrpG_%_Eeu%&1h6Iwr;RHdLlol(RP4sbNia=E9d8C@#|AnBR6i+WoKD2tgtknjz#6^Wk>mVMrd4b@_rIgQ?Hty6d_mh=O6z7_?-4TxXzD1eDcPJ zZ)_M|32|tZ#g> zq+b zny(~7-uIbQw-h*Suxw9>DO!;y%vKhN!;R}5z8Ld{v=XXW^S1+@(Lh5G#Rps?!ytxo z1=5dfP5AXLzs*xV4$WMOh7SA>clBhq^06LnmZ=nER1Ei{*GqDWS)0m|TWROcK20K@uvJoW90 zUBJV4o85l{{K`GB`HUJF_36z{u>)zoxp=R?2mIONj{OILBPU`5-^z@Mr?NBA;AO)q zyQxInui$4UG8Owps0TS@X-jH%Icl%{D=fLIk5nQUW?VOD$7Qn7z-ppm_e zFtf~K)G{S=1q7=z;LN(=;0N#zPPn~)0(kGZ;r{C#dv;h095=VF-t~Ojs^f;s3>R&U z9q~`T4m^JdIC&b_Iv*7%SH1g?rW?&kAU=qdv06OzGblZ6%wm#Hu0h_KekK*Q=evZ{ z&G0K#T76|71pF=pujY{Z&1v9SW_G~%d)@&$lgU!s^NC}X#1|HDfPhSPjthvo+LEwqh$~-iijjNY8%xM=*Y}(RTe#NBbz(C!2 zu|tKkw3hsw#c2=4B7%VC>{M2@>4iTyFZkzkp_}e~HBB|s;*A9-F^ytF6R2=wB5I#z z+o}pkQ|@J_1V*cw%BdGesh;Rms?9{jgi|z(feIqgZ_k<6AXQeRO%t*VzV(J2p5C1DguAJdKtOG{RAr3JGv9$qHh_Omm-oEWx zIoKO0gyKRh0hh#SrvaHh7`|f=pzcN^-BJ54!qZ}m+LBJ7B|XrpgKN?^4?$VLc-=g? z9_c}moG1KSUj)8gz6;oQ4e;>Yz@68W1UGxrTZ{tfm<)jVnRVdc`)-fF3Ow?X`wNF) zJGKB@^2fJbjH`%JHX;0VEbS=aHT4}LXzF7t&GrNjuU5{AXEo(ADPX~8rhwpz2 z_^-a=cG*RSE%Uam>jFr_(~Fq7efCD+{u>N`@eANTJ^?@ek=y#BX{7^1jsx`Mm5dQHb#n;<{ z;X_lgw0##QRpxM6_-AmpIoJS-OP18j)MJ@l&o&IFjNvyZE#+ag%o1z9bq6r({I>ez zgsneRXZUMSnz0qUK-ri)477Jiy#m!~}ci<7nYXATs07*na zQ~*q2xynuhYESE1K*M8m6@;6eyD`gzvj2)Tq4!&v;2bO zq3!d4!52A467d+CM8ONRI6^5vYEx5y#)!vb`i)kfb6V1wbO#=y6Q6mUGC0VCndm zt*kA8`ONX9n_v&^i-$E`=s$IO^c3)&r-8qI%5m(Y&|22_g_;g@>SA6uhM;(Cwzxc; z4V^A5kUbAHx5nL(*Sf`X&&d{v(cwA^qo^}YSx9PHMSOvXKbMDp23S7&6=h@f zrTlAsoJ^n*FLmOm_(%ezzf`zW`L8gddTJGNI*ogv9vDHG3j38{hfXrPZ9(V`-Y_B5@lU&&&gnhXjmyg_!-qfe(&5zW0pbxr2^}?*_hjOELOQeZ(B76vo%S zx&pgxZ|u~+VmI)}3$Q~+!Vp$hq;IM%f&mUq#rZ%dWKfGjNcKq`?fO7GDaGeEEPJ(* zt<^Xf&eK=Gpfyl`l|O9;F#v+~2_>&chgV77NXJuO6$8@ZhSYboKhyk!VKhCWg&Lh= zlaCAuQf&#CEK9_-i4ghnq+u_v8z0jlPe6=M&wJN2pYGmL| zabx$*nt>{Cv?VtRB>Uy$xG})%qMDQ`J=qNsl{pEqS$#Y8;S7~G4*8rNV?HicR1Tv& zA-J{%rFRhOuwnpaj(0zFyz?P&dd+a;gyZgOfom=?FfX+E8|DzvFo4<2am^)$-@6OA zVwdBIR}K3Q0f&#dtvrO4c6d`bSHf~H8}CAT9KbaH?${JQSF{pD5SGxElY;d z7r2p$F(Mk|$TLUyoJ`Mh%r*$$2jN;f+ge-8JXH9o5%;c*49*CCT0|3&hLc~N0}F>e z_e;lHM}R{|fN$LeT)79Tt@}jj&i7G{J#^6nY7>_?C^`D2yQpRve#6{F1eVLGs_^M!-8a_?H7)clVhA zSMN68cOC5aJ_Fo(r41t3%zAm_9$Yayvu^(CVc?Mi?vK0(JA4e7&)l}mQ!Jixa<$RY z9bN4Pa^vH7%RMZ;(1y<$B3ODgZLSv) zl#4wP6r@b5wijSj0G=Cuq$q=|M94J^&SN@0*3bK{b~$A}1GNu~djho%j$qubxu=tN zH`YQbZ^Ahlnpi8w@8T&1(WjF&tdzt#!#@DFWe&Sym*L^Nfv?@cUq`DudNwmE7uTsm zhOMm`o_-VfKOc2}<2?Wd-3A;qt8_gLE)85M%Yp5-sTEs3y(KXiA?L|9oELJ58TAYsA#k zz*w}KfF#&tdFl{K)ubw9?4G4NTWbvz?8&)fL{7y;f(Yn^>J${v)OLt3nUt*arlHMp zj@ZNaEBoQ_b=NY8rDwTpmRCIW2JqUuzymh{-@FUB`HFKo>~nhT+zNdAGr;{f0RP+H z0Z+ab57$^(v6_8TD5%1V%KM=~c*|&`6s!DCH`hF;SD0iBrG{O92mo|8NZld2*C2y< z_Ust-PfqZx0_k(@$U*1g_3=iheipT@=+tyc>V0$R{b?oM=nqSWts7RsFeCHqBzj6~ zHXLQVX>Rd@R8s2&@u?04Rr4qyoYc8!aVrX`yBy02$ z{LI7u=sW|`Z4uyQRkAy8>0qSiZdN>4ZZpd?fZVe|(@>Bf%V}|rb`4>m3~DCNZM|As z^LS$ts&-5az)@dz;HmH>@vYZ>D}8RfvaMPxgF1yrt!mPs^GC>S20?|IYPZ-qfrU!1 zObPOKl)OR#ftd_HP?-0`#n5VN=_H+zmZBVM{nuvjJK-OPy;zu^JOh0GCgAIL#>Tfx zcdF%0PqFhzDLP=bzV7((%fOEg00#~MCq8n_EO@D6zP9IZV5&_ZrNvo%t`dhh5JCLx z{{NgEwjosm2tB5#tDQG4KcIG0hj3b|{!MVU=2g?RI6Vu|Wa80TP$s6yi(sXrr!bo5Ifix3t;Cq!`JUJ zeEAl~UDv?2u1@Ra*tDZpo|qVU{V@Eomw+eV0A6~_?ex0ELt=a&M5(z<+DOy2=EG5& zG)MsyOioWd$1~Ib_`0gfwb+&B8CW}znj;2d-lhp9M`>PXa}uqBQ7}CS$m`4^g`EzR zQij$=zTmgVKmL4b1B1}uoL}9F5FeN0tl6Z{OhHLcc=Xw8bh77kX67~{+@jqr+%|?e zP0BHDGzbrA1ec0D%2A7QMQ1d4bzpsb%RPjAjBpZRU_?_rbHL~UN{oyGjne|5Ki+R$ zg{?2(&mT0r@(!@J?)d!8z*QH+cWi0oXA?n24htdv9b3&me>3c^tK-(Z{RiX69!{?T zvsv*EVY3r=BAQSwtHM?mRIG8GvagLm)ciZgHQDbFkWcSrG;1bzLX)bIf)>@?YJ+9!l2I1APFyKZZ1D_DCWHe{4S;b&GB!plx&=$SeAew5plti2@oG5Z;%{L6>ifk`qP-wWOEa|MGD=ydjP7U zae|H1k7dT{rU+Luik5(E7_O^ZJ-YDQTGLB9l!90|Fw317CPAi1m1nHjE(g~ClXpi; z)40PI?wA3#WhJKh?;dr3;x+g`y4UP0w>ef=Pg`!_Sk&TWSO=39nGUU2)~ zGr;~s%JtASrIRj#T`GxUk=ppylh2CuT-T)_8@Y{>y}Sp*<$jMXReQ`K7IHEk5eY3f zX-8c77Iu-Qs5%RiX5Dkd1(li_ajAm&9Vk#?@&8S+;EvsfWc+Ko3w zGbt!QoY6i(As1bkvocYqlMiHiyU25<+kZ3sGv=M=$~LhhZc1Gc`Dt2iWlR8onGlMK zzrd`7A3Q>8)U2)*kT zGRn$zt_j5H{LMe_9xQ3~_N~Ah?*sqG4;_E|oZ(+Tg|TBF9!bp zdB=}lh-cZGHUFqXUKwPs4OMz@Exg}wNSmz*+tLlxQxB?{#CNZC0VZS<(`&lZ4@1&K z=*d8S1~l)NCuV|qm>BUxq~k0TGqp)mvo=IGTO?S`oP-ngK#jcz z>iEO^&AxP-;fh_3vo65}biDJSr-bLlC(Mg~Xk|0= z7gx71gsx{sOr)Uru(zBI(F-@DjvPj0f~KYVhA@T358BkqszjFUrx=c+Kp^ zU(0Q3>^!^W#Yc$f(ROBh7$zGk0OBb**cwJjRf{7t_r}Or2GF{pH>>eO)zMl8b=PQ< zv#rb}4@%7}P;6dUyjK?>_62lV+a$DY&w%HHAsJc3^We)$$wZQJ}uuFCVKYiK!_4negrdi`3gQ2;oo0prA zVV0b9=S_Kc`GKC}Zn{0K%;Xu?-?}@0u%kI7@i9ytnWi1-;8tgE`#sz?&UH}cc97JG z9<$RytS;Wq?TGnT9GHz+aA>yjbFVC5`&H)T{;pR$d zpw`H0WjLD?)nv3EhZQOHVoh0S6+_}Dz9FA=nsF^zzZPoE+jp&A zkH;=}6x|WzyZ0P)|6MZFE==^u+sj~3Z4$^-)20IE{mQ~R)|ibYqXG!sj~UmcO{U`( z@zAQ}*v0X~@Kl=0v5@U}YO~UV9C{;hIlrvi*vN6nd6S3JUbygmd?XiPp<((;&*of5 z>Me#)3_=@ja*$cCPuvKBx830YoshNUR2I0~in)ev6cdQP1`t2^;GZrX(j^owITo&{ zVCNS2tycp7<>w7|UY*j^@Hf$ug8~p-xx?+~N!Zh`!~g9MfukqfW;6KK)eNv2e*-%H zxmLArQ!tQ^$%$0gjRh6&YMzZIkd_HmUzZ|@{F89lcHo^2|BR^xxPCW)@tzdPiLTNL zwUQts;nRj7Bo?(z<#Ku8lwLJ1Zq0;N*;+1=TlwBlbce1k#Ru2P=4ljl8EYh{n-glD zWvShJ^u^X6&Y;r?ud@iM?b^&@6q!;bXwR|B2gc?;0N32i$ed5~qI- zj(e^%{C9ui_Sa9Df9GkpcRw`Wx=M#z>6#K^ffY#X;tS)qT8Q|dF@!uZm6Q$4(qmAs zxbdOi@80?v+>5s{SPV?z`UXp?DUf2&yMByx>T~+-y65WbZQ5+c=e9GcFe6dX^xcUE z-GfAj8e5;kIP5$z?KVkH0+xw5u<$=fgDM|O5{_fhsf!$ym3ZR2#0syEs|FC`P%v;-CUNDQ!rQiErK7;B!UuCu}W zw7*IL zU5Vzx3TU^=I3p)jVx+bh_jAPC0M&-MO0u3l13dAX`@tiIFW%zz#}8OWk`2d~B$Quw z^!*){y1Z+<*|+z(-E~bo5B`xCfHgtqrz4Ydt?4n{&(Z0+ z3~G(}_sQ-BFv_c)019E5uFr5?q}y^GNWzTvrx>CZo!Zw($rvvdl{fuUPvdBWYmi4l zXqdf{0%z8-n3>s^Z-ak(AMDn>z|L(o-*$WXrXAG3j0*j|W5D;GUf2__!{0h$ICaJ` zH%O1ne>MJvfLmzUospD_b$ z3vAbdq7N613J7HEWAHh$L-VB5L zU(%n_P0F}snz=|xfod#OO9!tZKs3nc7}R-QrlE6nnB9-A_;KGb7=JB^$bg|P0Z;It zXIcAdFTsVW5?S~Enq{7y_5DT)CATsFs~rQyX}JA^xvvx z-u5*R7NBWJ8&4w%ofJ#KI3&?bbZ3cDMQ>e{u9j18-{d|2;CABJd@~!AD8<;x2UC>2 z)C5KJ!_)+Rovie1V_su%15l>;TX}+gtS!P0Z@dh4_jSORZZ+I-bq$kI-ex*;A!}Oi z!X3~5686Xe;ODQx-#r%J&1d#oD*R83%m15I+M%T?=vRYKWp8=sy&s^rg>LY$* z{LAY&xM9Ad`L^hSA10fr5+Tap@;vvl)nvGC@YZyk)_iC9|D+juEV`D`l1PQWW2Qfs zcrFs-?lX^jQY%MEx1#i~nrsG0yp`*vj5?CIq?xon&Mz9C$%^Qt5|>2=U2wxPp5z+h z=H8kr1WWW0ul011R>6i3v7DnZGYttKYW6xhCit9l&A|=UkqTOlfT3(nezMa~aUpRn zlx=kuKJcw0@IU(r@XPmLM^C!nd5vMu_R1<%y=Zfeb~7l<+-|+naOEE0>OHXUKMNc> z0{>_Y030if8$+h4U7@}88>^`Jym^O7uV`+tJH2)r8kD8;w1Th%CliIwY=MeYVjN*T zRe~|_hrf{ys?tv7>BC^i{#dxzH7GSMctdc;FrKny^-(5FeX2OVkr6=v-8D&#l?Yo! zUv8ChNBe5ZJd|Q8epQQ2Dcl@Aoe zTf3JFT5#&UgN?eHT!xIm;iP5!w)kfx#bbtA>j@)tJvJw4g+|Yfe)6cO(W``Yt{S94LA`}I>0;zJB+#2Q+Om_>p)`N zc-_mj4(4sOQvj5y37D8s&2B>01#(TIF;ii{uyPQy@k#+`70=z#=vZxfNV$V~$+l3k zDKL0f>B}d&!IQ0xS~)eSNpE$bl19iXB;?q#8bkT%%kEFSX86bVIsVB5z(rg6LoaC# zEmv^9k0KYLc^7SS{QkX$D|Q>c_Y830pyR}8flLB2V`=6iH|_NftY-Ju30m zxpPs()6q3XASkR$@2+#xS&O_<{%nXF=tu&~_0`pbJXl)D5;cDgX#!G&BBL#p0a2!I zYBS+4?E;k&WD>r8mU0rL60@Qc7n$e_NRpIZA~W)cA`0mrdyT{^bZ>p;PF8HUl z4PPbi?98a@CTNPyl#EG5F!iZbP5%(wIWcY7(wvweerx<=c8=o76qYqn(T(bq=I)9F z)LvNBsre0n9td!tCb~ghV9p@5c|kpcrm2Qz2^HVIZvX%w07*naRKVw9>E&=*8j?Oj z`Sp`qEuri82ND7 zk`6~n!fLQ`<{77pLNy5EWwN75v+=hxb8A8u18r<6ui?bS3Jrj-%m8=zcb;;4>22Vl zdkkN`ZMupV)^X4Ez>Sv!kG}%@!E^8f2Z7ZUnRXv|rH2(-Ko&w}k)bu&ZxntVEy0w4 zHf-_gSAN_|)<{}vt0RLl+d%2>2skz_eVg-HI#X^kzV;F7`lQFJWqjtxrv-s5W)0q=1997EVLr#>k;L-$=$w1Dm1J_>${K0+ko8_zXk6GAF zK01p2V%3`o+%C?)oW(5Z|yb`}-S zi(7{XcarMp++bL=XiV7ZTEtA=E)ofw%8oWlx|1kLL7NR;g`FUu9$P*vX`j1&bOw0w zE#TNG$Gb-jU%JhG?{4ODj4f=+F+}PXB;?|qu&>+>T(!sWgJ*%CyyQ5&W>}fk12RS^ zGRB74X4z4??^?-ko0?9?)*hK_HM~|Wfd=BfQGLoz3nZAnur_*M zaIrdYTCSoik4!j3Ji}pq;l6bh_{!~uuigo}?Mh(lDkNC$)7UJ%7OzA>;o%SCzVc_@ z1l~CUoLND{fI0vtn_uZeCq{^Oc@Pn~cvM#k8f%rJ*+M;&&ITGqjc zDwmJ^*_w^Xl0hp=n8^Z%_tvbP3zU3vk=x~HG6FCGN|Wss+b}~sS!!+F{I$ct>1W*D zJ_@`4M&Qn?4Hs<_YHVt0U5bYg>Gmz=pSc#cZ56os68I0E0}j3)4nHHBay~{NPa4Dt zA;>3&j7M|OklHq-s`BvgaT}@perIF3aP=!KB;6R=uxz0+I7uYJ%DDp%f*3_)V9aec=}1z8efTU%?Cd2HQn53sgw_S~C}A02Ri{1w=TC*v0gS9$@$ z`cnUn_&2N@eNx>Ejaru^Rz~O278A87$!$4?iDoV5@X5s;Ix_bH=GjqS?O0%2xnyt(nkH2jG))C;y3CCQIj`c>Q^ZlgV&Qv;R_D-#T zmLIUb--qVukDBj6Us7aY7`23HfZ7Jb1Zln*Ti_i+eIS7ji$?kqFVHIBHL1Z1sC9A% zLAFo6Z7+~in}$gb|3FX!luS&@G9c_`1u0j9k_T0{N}x8U%CzW z`W?fsqhT{6avpN#DZj(V9WNY$fA=ZJ6R*OytQzKd2SA~IN86P%$9~V!~mz`whHH6fHRRpMq z7iYS?wJ^TqSm(v4h{(;$6iY8ALq)38cxp;aG^cAFxaz4=12y7+@|hg9o3i}3592XT zsR|hJE}!m%;XZZUH5#Isj4f8cbKT`G&rU$j$&pUkb7P8Gqr4kKq>M!>a$~4YFwcag zmCcIn^dw|kIJJ@g3|pc)ppm!cKn!NSI*)aF_Ls4n{Ou!d58VS?v6~y_YMonzb>0t> z8#BkufIs@2`Dbr*`>S6#ez-q=$2t>02Jm5wnaM3AXd4cFn2#ry93M#4@ttiui;b$sV(_}}k$d~n>beT%Rjl=r#9zk*_SYtbk$0YTN-LuC5I)Rrmf(~{ z`FzxdcFKvKu%$VL;AYPI=VH}n+Gv6rS1saVF@w!KYw+@jixdFuFb7MmRU;Bckqb?l zyI~|adbgaW8{D9~U`&=()g1t#G3PA;I1U#LB=r5(71)VW!1ta3o_rPfSKkEgz5bjE z>YN|fUJCq+uNl66C-6Ug2YBm9WJ9cR_8FA2Dfturu#xG7VaCS`@-EOJr`PpfkMOJ} z3S6U|W)lj(<)i`HDswe?!o)1AZR$fYN`X9zpGYH`f|VyFj5nuJoi)&OG@NNV zHyJG!)ftc*khPhbj=Y9JI+S^<0VX2^B!<*jBQfE)Vh`}Iz5(2R<%R#mw-X-$KYhvZ zAAbfvd>lBlX1-;MLqFug0J@R;(<_wl716mW?BUl)6Aqz#*DB$gY8?Q?SwJtV5q$ z{Jd+VC{r@yE(1M(i>lyl372tb?$)E%N=+Kal^<*JEmJfMHct1k3x;21kxGZj`s=d< zV(uKGF>{+(wr$DCUH!uWe6;2`bj0v)zVG<#jgGI~X}D=`Ihf5)!BeEce7CK_Z{7?0 zZy$0z`hwx7F98QX0Jd$hRF<-G@6IF|>8`Un3L{u(ZF0W{oq9B3x~Mx&Q{2H3_h3+-}Mqn6{G{w7&G9F?M9+mpp_KK@$TS6qds-gRb&y#~tenVAdbQvqbzOX5_Z+inF9ATpr`jZ^{~>S`e+MO`|#5 z7+3pjNk=-y$v%s7j(@$o%ru3%I*TFBc6ee4RdyJkB*G8&!mJnE5`~QSbTIvI%FnqZ z?j+VQi>CwvLFHO1U52WKZ-727jC7&CGqhEglWt65!W^u6(j`4z$ngQyL%b>lB(=9> zNvV*Sa;%D=8hxm-F6j`|>ToZt2Fj^qO2y3_3tEey-F%iBsG^h?XhSJbSUC#`JGDh4 zGMb_GzjTsd8fW^AVd-ZG`+x0S;P@%vy+>Gs%*hSv_eudn+Eo|wo0LL_t^3ZSRnD?05Kr2M#clqVS5_?+$0Kg#adLZb($ z%t2GyR%qrB479AWDrW1-QEQ#sgN=MJMV_3Rl0i=T0f*U)BD3nITa*OT=jACGD^~@$ z*(_7Y8%8+1LL(e05??ILR%Y-AZU!E>3ApbD;Oa|8WZl$FZ|wz(qo>!*pL`Yg@e7Wp z-*CKl4A{B~o3YfES1b$vYG6*4s+CF@riL17-P5)${g2mJTW5VUUaJ^lW%1P%MxY^wDUN&uys2x3Y zyqaJ=K98+&;Bv^!u63Ym-5b44j$*XfZlFXZl7W89j7EyuA5(IY+vmp7oIXXSc2a`y>d-*a|CuNae=&3TKlahG32#uP6e>TAGBkG7LZL2_u=9h{# zSgQ%h0qoog96JR(asYV#U?9aES354>1l`%G&`+3ueBE z^txTV!|b-J%pbl7_{_Di*$l$N3!z?K!K-0yVR-kbz{g>tYEF83&7@Zgs+uU%Oy;btQZll34 zUrV*nj69NXR@1j?Z#rUBs?A-0Jws9fsSlcKprLhDQzn?|vwa6EkWKPYY5hbw}(Y=~5SXc_#0pj;ZTM zN1iX^4>uhNQYsP_75?78{_MRQJe~V6}x6D*R+n-|mel98J z2DFL-(`JhMc<(Q+nUT?PkAF*^_XRA4+<-gnb;>+^7izOzS$rk-XHjhH!k6$mF+hU` zO=zlG>2a2NxX11k`f&1^LOV~l3`OwJ$*{b?aq#k*NLPc#h^;l?Q|%a!(6gTBGz$OY zCR78gbORO+{Aj=7(EEL3b-uZ2dZz_#-N@J3|+oa&K(~6>+ZhR?V){!2X2l#F2?>Ytiu<; zpFIZra6fSPL$1Pf_0J>zRku`_K`#chnJjHw8@EM{iQ&yX2E{s$_}59!xXq)N);5@E zEJqrFd~jyYCp~&=VVq{ZpJ$aGN=bNwl2&JlmWq8eVA~<>Hxaum_cRn_E zZ^fxRpQS+^Bt=a2pbXI9ot5s_?Exv_00YW(vQc9a5uf8jYo?f?0~qC4QArz%l0)9H zJr>@iS4L0nea)OPjpkQ&?679|9D&vbv~3fZ!eaadGO7%?(k>1x7H+Q{Hb3@+SDuVF9QG1PaUV%fcZ+)ho{4Im$@6- zoeFK^k>y6EN$8p%RYE(!IMi~RAMxH{V@E`A9L(K=s0o$ws7+zAUe(~{*C&@8(|q$5lSXb(X{Vj z(s`**VpHXsm(Q@MJKaXk8~#aq0ai^}QpyjFCf;h%GNG2v&Nrh!m(S#N<-#>y2Y`7c zX}f50@2qPc=<5;LQ>A}1rPE7~e6rh&Vu6%GlN*Q&`S-{dn6~Fl0bbbQbRv0OvWgCF zs07w9h$;;cpO&w0rTq+y;uZ#F!-lkMt7}@<&uL>h8`3sItF`%A5G@o;-jvgq9uioc za-wl^1TOV4uA2pb5*A><%8Fra9XRv>aO4E=!EwiX$IS1$F@ElpPq)yWvYT)eX2$z% zs}ZGNw9TsndNc4a@K zHu=~fZN=z)L0U7_j?uf1RF=|MAjVofw|nK2E0iX*-G;*;Q6Sk;jff0jnPmqx!BNtY z1m5m*w9D?)X<)}z#{)M3pS#6y|Bb+&9dc5}3pVo@!~(+y#~n|<0sQPG$4_4d))t2O z48AhkeEee>Z15o!?EqfkS#tL9=DBV~2PO;1*|XqsUJhn4HW&ZOy|Tik<#V#7T5E>@ zF~{iMJ!*(gxIs42O^A6OGt2=ozE%*c`2#IUpeStOocpZ|kQH0IvkjCT^@tw~)PtXz z{;gKKQYcR~a7b;y;Qi~JSP3xLmJcH-$t~}QaZ;K+71_!hE%6yoMkza%JbG$KQyR7w zvCSc}A+L+KD8Wul%UN#&(>Gu9{^UvMvxbCu4 zIE!ixHgUgF4^Ye;mtJJ}!v}!tE;a1l;r85{@DGjyu-tiUZI+(aJAI;#N|~jbVGrY@ zWYeC!T@+tdJr4v>r4fWBM(XcGh?aSyXL)zq;e zFFR;dJ~(5@7!ErRGR>S9%68q|u*f9C<-1^aUuSse9^j5^%JesT2Roo;a6Keu4LJCL zVyV2YBOM$AN=}fB6l^Wfz%mnX}sqBMo1EHg3VY_XfjXK56(*KXV*E=~#Dw z2-U0DNotE~h57K_SBv-D!UDh-J^Dg=)H)lW5rg*X9HjG^aifl;B~gk^weu4%oU@+l zM(ec|a|@j6-j?h%m1ZJ3o{m6ThJFFBiPR$tWI0^41#WyF5d&7s6GA zeG*G5!06|eiL)Ne^+LhFme49wqz~*C(bMTQnab=!i z)Tzi?3}Rg$;xJRF0}Xl%=s~*;`lfT5GM1JbSd3}V1#UW^o^zSJWEV2meEN0A|L}jqAHFYsmUY**PbSM{9%jHp`+%FT0KWSa@Z$sKYYVrP z6%n%GX-~z}fKlPN9-{SKriOTAzSm{VB9lGDGoq*WcSnEZBG%R7Y%tmwdm4tNr*K+e z(*dHtO?9AsuIZ+)NzkK9_aGoXH8x@mrC8q*FvyzL*WFiUhA-X`U3diIQ)B0Ii7#R@$GvJU%a)Y>wHw|^L=Cx%-6m;huwM=aM5dJ%VCaVwVqoa9 zIVqiru1-0W!;$mNb)=3Os4sD$vEgz_Tz%2dmi<#dMgY#s*4E;p>n}5V_+I$k*BP$Z z16!GeTGbkDo~{)6kQeZ$UU&Q3XMyK`34C}mW?Mf5gU0zT4E$5o$MsdY{i2wG_BE2i zBjQU7>=f)qBjuEqkFz=J3m5-*4edP76YHqWqlF)})9LqdQKEW0Qi0+Yu3%l946ybgQ(W%z+Zj#D4OR#&9^aWXDE{A(RY zo`%(sjyTpx8VMpDQe}Ob@+o!Ckroat9cN3}7b5-{7n3_aVaR8QRJMQPWQH#Wf{|Q; zs5uuiYu!pDUXg=gEcj4h5}N1G(p9y|1vYqrb-US}8j)nN*gk=dKvp;UG);5sEN3Jc znn}qqRkj<}`8r06twb1Y*EvY12d}-%T9>9{`DYB=pye1ezz0Uz57W0Y++31eZU9-Q zVf|*rC6t_{*n$sK$T-XdFkp2aQNr_Yy1#t{_SOf$H|_?m++)}>&xek1Z8r(MtF9iv zefe&~H+RFXz0`5Ze)Gp)0^U339V2p*X~rzQf%ap({i6UvQ96^W1%m1QQRs2zGB$2lGj}5_(vyg2sBan zMim(n_oy_m68|)GB_6zuirjCY6w5i(!KVTL>fFQwp#)C~{a{d6MV852GNs6}{@g2f zZN`o{JFZJm5qt#Dh1}{aId1<+uakce9+?(pUqXm@`1(hQ{@U+yh zuAbJBCjr$Vt0`iDCin(bjVSDy3bY~TjOssuiNMWuD+dS6fK>yI9yk2;lkRUHaeV6@ z;P$IvJGXX6zS$9bZSFC1+^vo}lv085OrOgCFwhd=WM@b7-)c;j%K zw{_*yg@0{^&^Tns0R$oxHu)~+7(3Cd9OnQT6N6aZVW2Del;B?)agmQqXj5(YpV7$+ z-Rtq;B4Gevj(KUSI^c!^6JN{1H{Y{~tN`^Q$%wkyo*A4IqL9U+Oz@*wQB}l@im?dM zN$ zz|Fun?>5}D_Y;YHsmIQ3!0+ud+;qo*Z_M%a%nYk@_?3HrKmC^56R#M4_?+YE*MaR@N#Nl%UHW(eN0i z8iJUfI_>trO~BXgGTV0paLG==hwd%E5bEVjv9GTKKYr2i=zhlw2Z0l(f{}6BKTg2G+2> zsdJt^5T9ip9DJU6fk8E$2CQrE8MT|!yB#4qL~j*0?)n)KY7M+@O|JLpMhPrOX;#~ zZDSmIz7F2kO22!>Zo^majEBITZ6q<%bGJ9lMN#hXbs}d$ zr|Ykm%*U@VIthZWHCu6#j(&ZH6Z8k3ABCbQ=gW&6LeA;aWrq1VY_W*DcXn=z#{xfi z8*t~fz}D5`9h<%JtwR|@>^Ba(|MW%R7q0;?zXhB=l0Qh{6a9k1P9gWm9 zjC|PySE|&#QLzkj`7i=-#F(Z1#h^?>$ouW6-(ZElMjjetVlC>kFG8ERr-&3{ILF4! z-*Ul2i)Uo57ABAh)WXNk;$uCSdI9hIw6ip=HhK8?VobLh#6qPjU<#)^RbAkz{YgPe zj?K#^>nshH0%oVwr&85S1bg*lX1-v9;i5^CNb0 zFUO9phR@vs+<8r8PWB%(96kzsv<6$5b>G^e!!)Ffb6aHw6^Zpx(Zyr+i=I#us{By;j&$& ze@yXiwli;~YMean_R?FxqX&Q=KIeXV&1_{B1ccN3wBp|YClyC`!{?03`7sBRn3RNv zZB&Zb(ua^UI-R$yI(X|Du?BSOLKT`b z-MrE-^@~4A^eTzxya}EZFy@rIE{u+>H%lC-+J#RIB;r zVB{PKov7d=9Hr|@V*{-{@JWCTUm#E(53U9}%?+>~B8)v79?brdI)g z^~DuL4h2b{;HMc9;HJKRWw<5y81cZBl-WFH@#1ef{`HX>k^y{2{W$cq4`ZH?$%h!vzu$k$~JrYoMqe8gb zm9m?uQQB3sHEQ-6PZLlEE|%7(4Pk&}06XT-BF=B*{|WT|6JzJV>iV)3CKtApXqWG> z@_~rlPy%T@EPMfsS!ZpS*SD6(xX57(R_U6;{(N4Sr}Kuhh)13Z{L_C!{P_<8|Lxxg z-u{MPiRstm*f!vIZV$ZcO~9Z3koel&#Pctw(Hh(a=y?Ci1ZM^41oVS$dj(oqGyv&z zc(2ud7Vg)+K7=pJ!JBHBbH=F_f;M2h8ZVkNk4Kpoy zbskTgBmVH~#9a>q=U)J?KLbDt{#wF6%UpDXsx2$4B&i=pN({` zALih@eEm;T^*DfsQ&7?5X+2Q6mWyrh&^*6xI57QLZsY$}cbU94aJM z?oxQe-45~m%Yh$1M*L5IM11Zx@V7r0k-xMh;JCIW_;o{S#QWX~{)10OeDIyXzx^Tc z!-v6_F60jGaJDuo3}$yJ$B4}B4mdlkwu|CYFJE`ZcV)NsUjPAahN7YL%0@6JYy_+h zkTzv?l)3K0b=@+3&4mm3w7n0%JK_(13;57Gfj8U;$n-`2wRG-XVh0cr=UxbY^GC!t z?gbutlJUYTsp{K?E}y)H@Xysk5{L~RANGyVM$ozx*r8>Igpg2lFumr|7&`4EP(7p9 z4*o%QKR{x<=-bY7mSfL}W#(l8TDGO@wp>bbo?Z2wS*G)tsS%@eQ)R{xFz7sR->)$o z2zQV)zz0J-MmsURBUKfb)+yU4PU&del!*HE*e%h)QdwinA;N#6C9yX=pkm&b*(Ph} z7cl}DW=Uq`b9ElEC{cM=_E4P=0w=gQt(bJn4p2Q#8Dh~e)xII2=-Re)f)_3UKX?dn z?nT5i&jFwL5b?gZx@r~IvIK9`L?Xt~7V)+>0&lztxcNrl%{KveJU~2o9ymT?EX&Uj zJQUmss5?9;WOUhXv17E1z$o)Cdyv!3uL;-)Jb5(-)tj%~dP}&Ayrk=v1v%wdM2Ws4 zvtRzUHzGdv{=jE%%^$n5#*GsDtH7BX>F@a|@vZxT?>_)M^dzxu!7Y%_G4GDUYY+e0 z|ULjp|KR)amk3C9zqkjt*@gyF;^{ zXjL3HP@e8l)?lsM>H`WLa?w%~Gb3cxZco-|b)w(6><-x4y3(~4njC}H{HtvnE-0&9 z_sxv(&z~E{?M9kB_MB0#MhUD2?aE$s+qgE=wC5^d*#dmvap3$5#8c-3pTCXq8*c;N zc%zzn0}QW`vzUGLv&X@YzBlm38xS|Y4*2%{0p6DQBqCeT}(e>d5tHfLq=a_~Zw&#QyF# z=lA0oAJ^<~G#mn&h!u12kfjLF9?^~%_ms?^6u0W}9Rm)p{2^GT47&b`Dfy%& zPw#qZs56?8TK7r}EeZ=Pf$}6n#%M(=FX6yD_RfJ@DF$9P7^JLN%#(_0dMb%r=8=w6 z=0HNbzYIf&2-Ti+b?S6DxUq>yrq`bZ&%YG-+TDzMe@cGwV-cUfEh2YJhdV;ox(six z4B*{wA^!8X1U~#O;D7r!z|YSEuUzB++ll_dedw?A>fTV1>}T&#!q!c=p)do;m3z6P zv6O!y6e;=DM-z)A7tJW24-4*w>kzlT6ZlU)1KjrRX_UBT)e{|}ztJ&p?nU4)9wh$P ze*>O>0dceiuRqiH*Rb{4#J_qu{B>ySmnaHn>ZMoX28d`zrNO(%W5071gx*uVcJU7i z=fs zgAPG%Vs1Bh@~Tq>wsMu z-d^0?=2&+C1h{QmTG&(1A^y*=gAYD|_|nIT+uw6-oyK2w{H^x^|J{E9{K*}GKfaTE z>U>0PxB^fXxyP6G4sAolrzI`iDbp_H&r5eH@03dk&Y_OO#FSlvi>~SW{?U|RYrPt* ze)$6Nj+=ph_$lCbJ`&>=!&lqU9Q)xTz*oLceCK}PA{j?VL4(~P;GYOkm7pRXfzU4J zXIXt@Kc{HpB7PsHVyMn(A4}}x&tALu2Wa#3n(=23J)O7o&CAM2qO-wR%6j^&uR?1M zpdt_)%|NWoDwF3Z<;kF>1fifV!=H*ZlrtdPk? znR@1M!LITlFGxKq6>a*-6jbvo%Iw53@r%087~LrgfY?gOtk~6mfj4IPYalB>WgNLp zW8LuuKe`PfVJUF6mRzR9sv^+zRDI`Dji}&czq6%#M5R?p8ehJIxZ?rx(Weog`7rU1 zKC@v%MY*a%{ndo`*FK!UCJPx6@%kHpFMbTT?LC1ne;@q%Jpjr6g)U#Ky0|_ThR}gt zYk`A*+G6SqS}ZE>`$pDrK3XhY1T5k!8f|mRA$N=myS&ZtbGHS)^fBNAZwGI>-mc@C zKH*P?dj^%TJpCN-mG6_^djNR+8RDf^5XVPY!s$c6KaIL%72QEs-)K?47;E*|YWjFT zMKQ89>r{_g+(F@=6w0@)kvR5vz$oPbKu|Fs?P7A`9dy5>>ivwJig?o#tpQUK-|(Tu zos@Rh5rjY)M{QeQJ9lg9rdeO!8%I^zT1QWLwwlT-o|@tb(qW}onp5d?j2vmhv9Cmv zZ|p3t87yJCP*VztRZ+^&^Sho`5doG0p;r)&23Cn@Fi($QX$zpFo+?BnK0Kub811`rg)JcAePTaB!JQFlPKjfwTLN$adk?Z)dO-uq_Y3%3EEzYTcZ4f}!9wd3)ckta3JzeL>g z2=Hfj5#Ra|fB?><&WKqDhktq%bGa2va5(uCW(6>*XBF&)H+T$Svm0G=`%}i)4n+?R z|Fo{-Fjev{S$Y`CY%`9ePn!-V*~{W{0;mDhU0xLrUCzUdF8h`ojaku@>W4$iQK+^U z#uQ9zyJ9hkyKJ=*5Ie-HUhERWEaBvR}_sa0Z znd7XYef$2rP3#|hD)uw1A{wKkA+zfo>PR6}I173J} zBwLjm1+-X6wLEyis)apzI?B2Va1Gf*K(yVJEx2~C+=~ce2RW?QT_5qW_XGd&X9Mqf zOHz2Pd8)%{WNFf+9dZ7};8*VgzVdy>6XzIbj?e}~z-j5B;a|Xd5WX&&iD;jkW@9_A zR+c$DZ#lP`j867+1o(l<9-x*56`r<-S}I*&i-mJn() zeREb6=IwF56tb}(t=VoIR;FnN3jqBC@xe9n*=BN8&ofnzOEOt==KVn zG5=;2G&j?>7m>RHf=654y74D>5cfX@{NBfa&)v52?bUYtt@i=%du!l3_XA(O3%K`Z zl|=&0Dstro=exmA&c6%u0Br4Rdc10_r^!}`fB?4wBVN1&yzi}u-~VJj4e!Tjg0-OAFHAt zApRY9&ZnS)%YeF`wedYS;w;}bDx4?-BAUm#s`hAh-^Ci#!3w48(UC}x+D@-|H{SN8 z4tavuxz#XLM=|gQC(P$=S1qkmb!BV8O2t$a06iH93e$&FJU-ZVGj6OwYMJkO&u9_rYz5v|+ zDEa(LfybW(K7TvoZEsk5QP-?I?6inL1Y_HPn{Nty;dbC1H%EN+F7n^s2VA%qINIbS z;MA{_rd>8ixzX!$h?hqSv5h4eHL!&vS&6u`%O}5HcNX!54+DSuqk#{-6S(e-2TPVm zoPdC&W4H_7p?S2 zo`_JU!R{-jaSeYM=Adpvm~(lws{%za?lYtHBOl8pPoE^Zla7ukOCv(-NwcvDX=GY1 zW2qTimiXw``6X4X7_%h3IkBmjhQ0tq)w&d4c4f{u5Hc7$7n3k-KD?AZ8mXvjGc4j1 znqylwjSK)sTj0V)#zRjKuUr70JRkVPE#z(QLS)~!$mBKi>xx6_HF3k)z(?Pkf8Y5g z;H!7#Pswf@;>=OmW$~?dwe74ID1l8Qi1l72$d&6g7M`ZgK_a|FEq9f9Be5aQ#% z0o?L-ENu&)eT|L^hAg~`mx6ac!uZ;_2IipZ*Z=rq@-76uhSU z?+Tt5O*QJr&de#1=6Vfa(x80*^$zF7GVl z;w8k*Hvu1bJK{5$ne)ho&TF2L7n$;uBoR+PPu%xY#Fy_R@AwIE_84(|qyPjCfS^C2 z3Nmnz_}7$q67Rg2Mm8$Vt1C-h3=}FwbMVnfbQ!^^i!_R~>-PXi(7BGnVA)rpav#JzDhR{E=^@!p8W< zghC=7$~=>jqQc;#JfzUXHF4%-l7|0!0la^hSCkU;y}ideA6DS2*eH70e|Nsh%bFS`1YG4;8tQ>yHW-^SOMgGf)_6# zo;*kV$(`WWe#m&{dEkb#ZQsJ3ih$+L#5&p*4-Wq@#+!Zd(joWV9T1hxio{S^x-J>} zGKM9dbcv^OgqK(E4hjDPb-U}5-_zlg62)vp8r$^8@l1#5R`)PhgRGwv6!$K>t)r%v zzG<4hPzL~5PqDzl!dPDr3!RHS?9Dzx^UIKQI>(3*<+CN9B}MeDLgI#kffAzYw8idP zt072|L~Zp93PIdEgvyPhnFn1P3nR5%C!g)loy-MpxYC3)(RQr*ZrAg8I!#(XR6(z? zrj{N^t;Tn=?4^5(gp!i#ZkIZ!_W_(e2A_YK@s+;_-thqNkAD~Z*!!=U|NfPZ_q-YS zFaIOp?|u~cAO8q=^yy^aQ9q2POlY^d9`=*s8hE3s_Jh}C#t6Xn_BRIq@!vyy=v}~# z*Ikq4{>sOTF9YBD5%Dj-MLzW$aN%O?&K@V~HW$iIqs>ot;-4J`4iNw9U$-KP1nJOP zH_RVnam{>|Tnf<1&@Mu_4baj<=Z$=1_Bwh^>=H+--CVSUz-hMQ+*3ciW7M~bNaLG(5ku*GZ}G{7Wj?V; zc3f5`j!_^1pn^FrtXM)hwOG4=#>s5W2RVyyRtLxQg#ayDDZ8~C0z&Q&Z7lIy2PayQ zj7(0u4R>@YonadqJG&?b<>E;275Ln3h~Id7 z;Owy@O$Q0rvdpenR#g1>G2*LtA-?w$@agA(-457Pj&Z?4?cb$T8KV*_#5p+p)ABe+K0 zvY?b+JtS|zRzc5YBF$O6Zbra8W=OH-x{E8yy4bXbplgrjE}!OU5-#`HczQ?jpKS;W zeE?hx1kzJ;7o3qU(nMSo zP+8C{qfiUW<;>I_?FH!1x^y@AB0*!q*xr4hX-0Nh{&Zh=*_mi@7&xkF9(;m${w3h& z=fRKPg82OHf$Q{?hP`6zwK%E-j|k%EC~(`m18=wixcMgVTlX;@dJ?#Jkv!UJiDehk z13918FJ%M4-Htfg0-yRI;!__CeC!tRU2n1nidRR89VkZwAmWt^;GGZTZ`KjCqXJ|M4J+kvm^&iC2dQ6a6)Nya7{ZKetkv1L1`HWKCMh7GUHR&R^%zCv#aaMJ+pC#l&x%TJXBL53p- zJsplegG_nUrEI)DcLQ+ab>R6Ifq!>5;+{uROt-xoc+2a7@tv?sG!#LYJaKJ>1@?|lOJ$om4@*x+<6*PHXB z6Jg|UtUq}UxbLUHzy2=qaQzJJ4c8I79fS>8B$t|5KZKo80NXo`4i*3AmAxIXFC8@4 z-?%J{vR|*wHYyMxE~|m3d5E#K`Lr7D|L$TE^k(-7yWcxzZ|@kDa?)wJcWeX!!L zWY|y+FIu#gP*Gr#+u^h*$f6VP{PPXVlfo=-Po3XXWWiL<3^a3Q!Bxqb<6P-v8iO*9 zRv093fJ>hWTBck45KwBxRgqj=Y`D&(>X!^Mudfnj>8<$FL{e@WlRkuMu^iV4fAb+J z$0VMA3H+b`nE2dn!0&!M@ZopMJWz?*wc83mvI>yEP1g}$xIN<5cM*U3{lM4nA)Y$d zuA#H5)xM?lZ+czeQy&8U)6WC>gDPq>#F+2e9PP||z4!kN__MztzHu+&;x6TJ?5zQ% z4{~jpsilDrB@T>$r5yah;@@DHJbiSAEBB^deu*~aQbx&|Wu>EzQ2YD16FEx=am5dc zj+Ej7;vWc9$z9e>l>u^nEr8ZC{i|w=t)~{a)dY%=E}tU+yRwCoQYmRbMykgsdbfqX zFm)UfQ}FRU;H@mTOZ7O^M<{w!phiCjg0Q{1=TlOz6S{~tV!3L{ z#6q+6YB`K^uAhdVZw2wSPgEAc&J-A3=`FOA<_2TPrS=j{;nnKpVA+n8MGA^3&Qum> zTx&D7QUTD^yS@XsCa|8$5IK^XzTaetd>g+Q;C5*jd?zF0@h`><y<3LRMM`%yn!nTDu335?@P4rP z*VW{Tc~3Z!?zTItCo+piGh0Mt#Y3N&v`REj?~qW2*!I4}A>bd6?O#CZwiX3+Qe*+7zWN*^ur2SpsPJu;|#&epemG q|800|}cxwJjQ4n1$T46(@p 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%0Axm!T)wwsX$WPj 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∈Q(um|*HKlwuq57@tGk}q?`gc4wOzN_z}?604i@9ou1M0_Cg!c` zwrn`9=*T|SmAa~2%FB{hHTFC5W!%`@lc^Ip7~VFOJwBhJOwmZr}lV@s9%k z+*G^b3MFf)9DZeC3r<4B_6IBcP53ty2f2ZNl;>IT9OxW$25x*m_{V!*CcZQi+4e9z z?WUbn5w}-m0f2%VZftNM~v&70Cs$%Efj+d0FJDU<%C+Mvp1RZkh_$ zYC1?}^@P6Y=g24udUg{uQ1Ar>d>EIG0R;xarE<=~JWopDRhOToA)##uWOIMe?OEMU!zjU>|MUqF6^EXjj8?v_&oR-GrZ@%&w@ad^?INQaY@Q4|jBx9FPxo zpVihnVvDUv9qKO3*IL$*LmeA-#%k0BkQf^oNQEXd?wF&bu{C5O9OI4ZXK~k?3ORs5 z-tNY>Aweq#9(xC6A`!@mkZTyA`k?j!N!~jZXL#=HVj{Lr`6$3erAD6xk!0$9mh=>J z2IJ`G{-oib4dEl=A62-h|0Bdd?T-6i_}77d>?-|G_-7mV9{AU9z(0Hl{vq&h{)!77 zPz{H#@UO_sp_RbDz3+SD9}~e3#6R=(2K+d^TH)puO9DV<;X5F`5qK$%cB-nKrUCkR+@$%zMfT$Dt+x?A^^-}(q-_- z%e;52*3L)90)?kGTnUz{fO>@R#n{8fT)v*wRGhCWZr2<-RS{GOdGh*nIsolpp2AqC zv2f`$L0Uuv;a9V+w)_JMz;%LMMy+MKP><>do+OD3k*Oy=Txo0aP5XaZwj{G`b8FvZfV%>p zgI~JO{=c%S1H3k!X$4ZTQL*oEW%C2#AAZF6=LF~@;@{fs2jd^_`=Rj!N2praM)$c-?pY~ zw|Byb{wfaWdn!P&jxC^sKrqB4q+^mP&2hbB1_`h%8?);KnTDh}xX}&ZQIvU~tWE1@ z)A80{1o$I4lVKJ3vH;CmfVzlFu^{Te%wsKt(twKN+Lj$NuC1=-zvou`CVwcfO>dF) zWM7Nf{fKN^h02AON0uMmg>Y z?q0>A?I(E-)bLoL_#mpwa|N7To;Ajc7_R4Rdy8L;ILvq3;b@MFMN+)I)0PI`uM+wD z#6JPSpB4UX-c*SZIu87kYf=9iE>ynw2M#0rtB-(xx#s-+;Ggvq_*e85;U9iL{5xip z%s&AChSWbh{2Sh0<6ldx9|8aLN8%ry{`y*odvKgrGmRH!6+>!v;!_#sN?Woj&p*3zxPX3SGwmh0& z*)?R*ntIj0Gz+dIZiNT?t0=Nw+H6;|-8+6&$+;#lR;?oUEM-`S>Z0m48j$o3H&hp>`)RV!=Fszyog}Gm?qt?q{D$N!AU#ShQYw z(&;;bi95L+%A^sME>s(O*d+gAIP=I^8c28ghQ=TT&YZ?oo zrNE|zXMv#;C3!SeZD0Dn@XtZ*4ft0&XVH8#{G+H#^96i}2IF58z!DTE{IkP9%3wzQ zKJd?%w0im9KmO6mPmL=QuB%#miGPd(e=hikkAQzwQp*S9pLK=rjel~I#J`q_pkHFG zg@nyKY27RRJ=w{B?1r}lqv^I|SY8^wov&xK(uoVNJ2Y`i%D3J6LM5*ys9LAY^rs|x z%4+Bd(^XLFwHeO8MpeE$WIsOQ>DlUnHcZv1je7-p3xruiQ7h)D*Hm2=52^3*fq+lR zxxD=1J3=5p7=;Ztlna~%LhhPZ?nEwkZmv|Pb0JVjhPu&9o8{ISnDlCz`S=;wFWTxt zuY!uQW`e3=%W14)-2qE7sU1fP8$%1}>F~l8nZrvW(%QIa5o|dyg6+{s z=Uqu})hM${mp}fIZsnKOtIgC3md)vLH`s@bo8B`32PcRE-ZLNeHYSd112A==+D*!V zMxIZ`ghZQ&9q?GSg{w;X6R^4#*p9lnlbR*{$Ay1O`VWSGqX>5*{@n5JqL?2V|GFAY z{L`X+h_BLgLo?$Ue@^&U9|8Yn_7BHDIs8Y#zfP(T`~y()>etz|-CEGGWA{2QaZ-sK zck8!0VZFq&FLz;;l4MTR!J0P1xT>cz?R<(_tGMlbl?Zt1S^W~y`&C#C5; zS#7Wq3Tsw-K=#l(rHQM}vCO3ft0CMNOxI2hngMD%r2=;34M!vrxildCQ)l~0C5s%+ zK5^Nx!N{SGx_TgJmU~k$7P8%p7$A*paJ5Emj0=&uv~H(fB!{O$?dY-u8y&&brAv7; zb9k#`((tzHE|b>d64icjYp+~0?o`}b6P}?z#F@ot+W}JU2^vyhb~no1!WD0Q_rj%e5PPB>X#q z=v#gMW{zXAqzs*3+p4oP$XoRMz2!0c>dLbX4<{ zAzN1#mqkf;39I<W@acYd${=PubIql+YiciUvA+Wg%uZn_snuU( z^C%Apvf?Bvwg7?)&ts?r%AN9hp+0+3p$=~*C$7!&<>08OGP>MbRElQX>ejrq+h6W?P9e^AfG$9_vkjDl6YP z#lnzV9;$389cr^Og)7R+7gbtv*ce*+ztyvgBXn#XZp7QoL-(!TUpzR||(@Z*p3dN{f95`>U2C8<1AkIgR{^nya_&^sUjvUL#!?FV5g#r`PZ| z{@wWZ*5luc0VEZJ8B_ue8>m{T_A0vAhhmClG{K<(HZlEYnYcCP&Gy2jz4N@ynBB57 z`4O8tf3LyF+G(3z;i$dS*&E|?x%^fH7I4Ds(ZWqy+)q=FMKYx&Lt~|(l|erG#hR}) zYQHv4`2naEFmo-n*7nLmI1qnSH{Yqg87;CJSjVd+M#@-}YT4B}W$5l|E*`F$<43C3T+Fex=omHTSTh9uByYX*zaO2-wi+}x6otgdB*;|INKIY6K zGvopv1~+IGdTmn%zluBVR>Dc3)yMEjn`}wzTCNbT?YI)AHlmPOsm(IVth*_u+>ij+ z8!V0*Yl&pghuc=0QrW=SF+sg{5e_Ni=nWDGt2czI70Hb%hGa=ikBll9g@eHkqYX{V zS%7CQrv8ZKF^6QBGPffa|Fj5=<_p@<>k4N8v=cmnk%JNz z*4RpH%kwCt97j|Q8MLeAb>sZnouvwtF3jld&g^iHwE=C=vfGygM-v(?t{>{}QVfUm zJ3vI(mG}e|U<7UMXgeU@`+l@LU}HM>rx^gb&u>k&+_to!(Cm}T3>8#q@dn8?v9fkC zCM!{92&3E*6vJTA2K%yT>4T^77!+Z9p0HdtS+a8D-;IB7KmN_@dT1F&mIw3NmN}9) z6GH}6d6dP%Q9{=Zp#8R;vSCDi6UIQm;iwM}(K{M5=RJ3PWCd91YQe@_nbAh}Bw2+K zl-uM0JD&l+~}(t=Z@YXwK#J%)CfS7Sm`ZG94Q&F37$th zHqAlz@1YS_<83das?hoUwMLwgiUOg-r4xNoi);Ty_XXo59ec@3@i#mXrT34mEfa7{1sb$#B(pc3`#14tRFVx#Rz zx-+K_NM`f@Z4Vc#{EUgFzEw$36@eyeS0nrf)TL9V?Oic zF|_KZ44{^B9Zt?5*fMh>7xzriS8x2g@$c=&Kj1}dbOVoa^&%~FcXTKu@#yebPazP* z+t%D=>2=&y+$vz(2ie!#^9TBlOu8}I^mb6JUg7Y?u*$8yY^Ur^ZQjk9R88ZRV_nuf z?Y^Q8=cf!Wsj0Tq#tCRbu_lylv2(n=rc?IpPO5FIL|#xs3GSfR;oXlF1k|zS)Z(nf zEYVCFlz9ZGA7dPw(%!%piHp{6sAH|$awZY23a=X*372G8$pZBSGRr(R;M&1pvr}q7 z$6yqc82uzPE&%GZ!uH-{xrJ?&M~S{yQL+T>^QS*dmK|>#dSXnz!Ji$T|~%vQ|~vW|KYckR0Ok z)p+0dcjMnr1OC1EagFLSZ4G&))@8sx{qVKP{y`0=>qs_VnTgv=Lw;G!I}oqPES2&i zE#y<%34#(866SZyHhnH?TRicD%ax0YRn%HUiY_$2#&6{{+H^`QOlJN`Ysif@%kpN^ zdAb6*W{T=Z(Z$qQ>Ah*KMZ>Wk4Zk#a8bM}UJH*}}SWHCK>P`plhcWBgS1fF?p_p~P zge9syPl$yz!*X+~!rnjD+YctrmNj{lY1t@cr!5;pJMPRGdNU;gZMnc>UuCa9>g3QCl>XpOQ8ngVf7}LG|Ud5p$d3DxuOsOWeF^g7bp?{yY`$w zSBfqRcJf~}MMk+LsI=tzI`{*gJi73mAeL4T}NQf)(%F9M`AQ$Tgs00uQVZ?T)KwHL)$96Uu*li@3y|p1tH#O;v!>n}1h#RKDHb8%Ob92p0KQ-R$R+Sv>xJd&1);?)I zNUWAIbSc|or>(`8TV5z=UkvTI)OMnnsLOSBORzEj8+R_YfCYTKwwe-}3U!jz4&6#< z&|@bhK8`gm@wcKtbN@#NQO{7d3uHO&MrzGL6U?x41bT>1wmSpJ0fY`vPzQHxIDC#w zHFY!pX}!Em@PiBk+HFhxqmHb9hFxxf?d0EF*caAd?FJWDkk2t7fxN~wu>%Oi2_ohC z&;jD{x0nb0dF~Z|l-FTvk>s4<7kz}Wr@VNtL9-HXnt+4!zzw>%BmmJe<*QUU(P=)r zRFOCS-S}q-zMc37V7^%{5`KgN3(ml3MI~YIXQUWn($~r@%~xdHkeCmb635YU0ctlo zghT*-W$Mtk6^G{5;1WI+yyUJhb(sVzGi^ZIlJ>@{&35oW1wAR5BioySi55zsf}g+; z1tI`i7qOx4RDv|0Ye)vg5G1`GJ^^&R1?va`$qPq8p`XSO0;W1l5<~-dCbR9_vV-PY zejI&dp_*XnsgQTu95W0wFmJu8Fd4<5Q!cWTG*JMkN3>G4GyZtq`=E2HneKF$A)pn~ zF`PJ}h22F`p$uSvQv?xjXufVQEF&RzPF@$L2tsPdp|u7qP`U)<>!~;ni@4Z^0dIV5 zoyXny!tPi;$P@GV2cYG43Gm5Ln5wnaA!n8B2gq>DMW|K2(#wh=5SZ7<(>8AWyYcTW z$G--;UMBnCx`T@xjam{gfhO^)ts~L1Hww);OUfrl)r*KhRN>#hH zB-hPfySngv)WxXRK+FH%QjXN_3b2{1!fduZyqPh~9?W*3O3sz3kjjK+JI!`xz!e$> zp#&NY*A99Zp4{7n9jgY~F-=Tv&wi2a| z<^4 z$3MJ;U!i0KB}bi!D%D(~hU}9lD{wTV2lv+D0q|jZqhyD9tdLCr?THd=YMGKgfF|3m zLrg8x7P8@H1sxa$a4Soy!5ds%0Rz7X^ zeTfv2zz6iU#nucOcszEGwEnG&cp)HR^5;d9-nqANAHujZg@cId&Li^!gc7D;jiJ4vZb$03KR}xbaVNzVYv^#=k`z@J5!X64o?(bR?chLqeg8Oioe>N-ItT4i7Ch z%{@8;-q2Si+%(P#(Wq1fxlW(*<+3+w%+qSHzH94Lj1br6g!kn);g6a+-i*^=D94eI=zkV5Yt z+uQJB(MYS?@~pfk#fYdFI|!1}79)CVPSqQ;s|LFu*#&_yOPiI)R!wU0L8T&Hcs)}5 zcDT1pCY{J$1nypuolhw$#JZr~cmfSim6CzjLOZoA`x>Din|$Ql3vl`!uHY1qwr$)c zU?sgFL>66Ar(6OalQMGc-UgMHL}X9ae|oR1ka+k+X*-Rme zkfl!YdJgB=5bNXLxc*It8i&J6oU!b6WEG;(RIb9;H&?{_yJb%S95B-EJSdP z?`gkJqK3XI^z!5scm9?LN>GMz>UhK5Wd62gL1-c4^>MW0N^8!8czrDCiPlpF4Y+lP zgZ&n=2{PuBb^eQENtFI#w*^`HLLk(lk3F|g)sO9N-}sl}vJ_^x8~=Xt@vmOCHP$iF zxE6EQ&GVUh>3qt?jfSHQpf-n$gP1gDR+W2&TsTISxlwQhl&c$OShfD{yHA4+d=TiZ z8D}LWdga8z^{|ABYORqa-Z^$i6Htq86fw548RiBEhLmo-nS;jUmXT>l*(~41E>2|8 zZ6~AUAj-OSlDj;lvluEj{*4`PWh9z%M|J4VV%!}hrD*1uh-RYh)|TOS^4VY=A!@j) zhNm&8kcWaRS<0}a1j~qtd5(fj!-wGK!)Urb&0591-De>qz2f8v-IGk$JXo%m_ z>3XHibfQx7AT*xZm;D721Y%2y+880WjVeMWdA1l7GL6w>FI0$s7;puB5~>12d(O$F z4KBKfni}EJ71Tx$P&50U>1y(7F21xz0(J z>&IvB$`>iR@=OPa5(QfNpxD|mQ9F{u5~3R?S19>LTvo4SlVIH^5_q&ja6_(*#*?OR zUQ9LZe`uO}*}gkltm{i9Iu0}#nlC+AcrBHIJ)Os3;T2qV`HcNa1gxp%k2;D(!jn9n z+6uBAjcMucnyK5+jjqRrhSbs!thDDK zYB3P?^y@eencny(Dc<-O=OoLYWc&kgd>=ZjDgSHk=MjSr%46Qc>6V9_SX}_0I{`u9 z76O+!L4bj2ta(>WpPmL{`lUwU8cfO73m`bTg`;fV{RtEkd*-QYJ0SI zSD=)se@{(GGr;8o(;TFPGq$8LBa8#=n%@TYb^I>1uBQ}yJWW0|S|oYp#hN1$YOTMT zYbOlm#xp)>Vqp^c(c-c(U%s(8Y|t!YG9j}%v<6zjE5~Ow9DDD&s_~>|whRFSGdK6C zgg(PoTM3TPr14N?E1&|{B@f5!4ZugR3m=`TW|s=0wGy+)a_v3tDlFNvZ2o*Q=^$Ud zLwHzLn89!$>y3Xm{=KF62LO+?F3OJoOowC3`H+|iYWE1*eEZkKs45gAz=|$nF`*mF zD;hqt+PP&wZS+CUGh({Eh5-@|G6a>nVAX?1rOlj;(wlw4!ATWcbb}aO-Pp1V<=M2+ zX8h#Hno@c58|`LDDoQ5uW}-mZ=^Rd^Yi&rD)|E-ywjpO!S5~cvz)jV! zxDW{nYP}R>@f1Ksxk!=2vt`|-?apBr9!zlowgt(T9!4Gd4NTl2?Z;UnSb=TNv1JCk z0<;B%6)nXJgHH*I)NYOJerc>lLD<~n$*?JsXY8(MM&60n8EQ}YSJTihkng=pFhiM z%(nBI`%1Eb5JU8jOFIx**6YJJ{@wT|T3)g7la7DrMn5!wlxBw7WUmgdTs|&ffXB1* z1l5pjz&W1rmadlIZLPYGHr<71C6~RZQ@;0NOLVd-z7C3f*wIgiJMLVVIAoy9WEi{o zqk^?o#&|vGPM*-9+s8#TuK2sxP;4;O<=m~@3Zo50bXsQ@s4l;f;VP!BN23meBWq#O zi#`Zm!V^lC6|DG1Lu$&ReHooEXpOsC_f)fUXlEEu+VHdXQoX#@(I_>&{j4}K>3m`B z)ck9q`=RfBjQAKvm}*r})!V&f-G%CY;`s#T4H7bGnH+%eQ5&Jw>DR*qcN2c8xF`&s zgOGSET9~2q1w~ssf4K6A)amuL!ASD4C<=u~8BARSx1A|TQl-@^<0pAJMAb8-*>)%;ok}o} ze2#myD4=E$JDDwE&9rsOYjq*1N%B?W0CoxzO+#A(A?1TT*CLL`|HF6Ec?2XwvHqoO z8mg!|pdO`czn5oUa{!I(L@PRdepF~^z03N#IMHPLy}_oKke|S8-wP*Rg|2p^nmDf+ zA63uiXzj_Cyo_5r!r=fF7d9RYN(#J+Slffr^ci%Z4swL6QVMQAwXAmxdS>X|ycetZ z=-g4aOM0=*9m3-rtqoLIsHH+wnpU$uauXE+hHVLHWwBoIfwI2`UWt%~dIW7;<9v&w z0xm$EeVUVg?;OwpKjrMT(hJ?bxyf9PtwK$6!KRR#pvOteKN=6+jB;(M=t3BTynNID1bC^&u&VVwCb)yzwC~7ekE*Zp(X6r1^PMXji zkwbW)f5TXoPWL))8H#&twaQLsKHVJLZ8Lv&ZO|^Yq$PVv=A^H% zI7b~1G*+#&y4qF&-1L|9Mo6_QJIg)6$;Oi{SfKSQwAb-jiI+1kOqnsNFFIl2NHRgM`&N^o9;1 z@|;fK!x_14PuvE2Wu$^>caT}VxoUDI?F${hY%T&Dg`tT)dakKFFUyF~Lv+Js*lT;s z{Pgh334v4N7qMXt^79^3cyBK2@)|zzb=(7q| z;KGBoFJCXkVSm<6v76#pQI*UFkbsn%n2k1C9&VzsK=?~`O3SY#EZP*Iq#OTk{Ck`6 z58$g`y9jZcY*5OvssJaZe_!*kk81Nn$YL931*ulY zt_@*9Vfb*0EjcScr0r&(=FEQV@Ck`y6`P6D9=mMWQyKX82GnBgYD1_m@Q4VotO`|Y zre4E3vvv!}aUcM*89)}uy$CCZkQ3SP;cRogPV#QS^maQb$=GX|U8r`yQ#m;Z1MO{1 z?aUZ{Ds}g6seRV^y@^Mp)No(hvW`OPB1gc0DeAem3`1ol{ab2Er-^xZz0~j}M`Bfix z6WZjNJ2Ubx+H@|f9M&y|P5#&LpbBhf+`s$5U5E(4qOI&C=`9PehbhZ^u)-`=R%?K- z_~7WZWmuvQum+Sj6cAXy(DH_ZK2hsDLI24Wy)k*>hdFrQw*u43w;5E+pj>ec6nX8^ z`-ZYN9kclfecIIgt^blHjVbr52;sqi+OktKM>1XhN4B@|or!#f-+A1pE3fUY+e=Dv z0Thrcu8PdxxF(%TaXFd1Q+QjF2SlIc@~TCv<3WLisganIiHpBg53qLw2PU}w#MOzWn^8p6_{0sj`QRz6fboOjQHV$50bC z(r%*YRG_UI5O`B0T+lK$2|HMw%07j%jC4ie(Ig$2GCmIEME~T5jd=RZxqRGA38-EJ;ujWDe^p_f@6Q_PAcnqs^WIA4KcMsf{?{j%gTm z;J_LLG`38PIB59@)t7wT-{}E^C+y!HPFr$L;Z`4LVK(Qow)b(|jl`zOop?Gv&FH{j z%47SvNweQeV*@^x1uWb!y%+!hAOJ~3K~y#haRd@zc)A;QV3B$Z4Yx1VsU(w&hm=G) z(4Ia$e?<_Ne9KcG#Kg=^zgd6jwbn&9S6I4?&_WGrxRS_2{PXdRe>eWU-T3#?o5Z4@ z+6O?B@y(Tj7htW-%9wMWwlVkdw?Qx4Cn5YvR@WxiQjiF>DRQe9vDL|;OU+R+g{R(9 z1Hf8dQxp~V16(_4(y?+iCObR;TaBEEugx^pRWXVL&S+Khc-jKNpBwiK76l${5`PZ^}%8)JMSKNO_d0{-zvXhDWRV(MP3aC%H887{nUR-X;s=>FlkI&vN3IFD zPc>9lK!SpR?a+p~C^ABm%(jB2&P!lLzpQP>+j>k>XW*~4&n)IBh^sW&0Jx+LJ=q(D zA{*ty=xaHrKDt&-_T|N4`R?#a*ANgg@EYD^Yidbv*l~F>GFFSZjxM=(1<7^FmJMZ7 z6`ZQn4eX+bY3W_4hw3E~p+rJIX#h9=`Ij62Mr>~#{ymHs!C@5;! zZAAuM7_lv^B>zZ71Dbg6>dw5@DKZQY$={kX9=qJkz3>#Pjd>($fZ?g3P6rARaUIgX zPXIi_c-vyywoh4yAFz6+?9!KomQ;2HXz6$=Ac>a&?n%A`Ib?|bO zjQ|p&neSX(@C6beQ$MwrBz9e0>5?cZ6DZ{7I|UH$P3XOELH@PujdikXJA|Dj=dK(7 zf~On*e8Jm_e=p5!Uk$^#i{hwNM2D!z&~Pu8XwAHt3=^f1Zf}+-rYyPW?RCqUXHkaL z4pTQzE?R22+Y+kd^Nv=N_kVy4J>VV*I(0aeNGbii3ZMEslv5i(G*eq~d*|fF@yJnI z)Wk~3AvbB+M_Q;BnszIOxZOk*F&|;(qy?Izb|CXaE3jBhZ@2(Ajx>~9cL9&5T9`CA z&|HMD5h6E;IAEX^Vlr4Q3te4&GUHqnFG*+_4aul#4*OHt=2OM0P4YcjtD-R4cBs95 z%`(dfwM9E*?=|_7tBG;}qd;uIUqE-puWeb||GkhaUolWin>~E3T`X-SBorGoH)Bl? zC@qG&qmVk9kR#)j23_H$ADQJ5?zT^NK-ISLs9A4_`8v_uEEReSwQ=L0 zM0n%h+lhbmlIXL@6Kg`T8Zg7Vb0*z3)wC1q*7chJ$!$JV^e47FU_Y)&gIPO3k>ekg zPxx(PH86(ofcqG$AUQ)Q^7Z0NU4g%<8c)=-I@_Yq96w-c8RMU$wm7v|H zbcgL%v4oZzP0VAXOe1A@^01*UT0gOQf;wlrHVE>OK|sLsP_inmVL|$Eql)s+QmFuD z-Mx{8NZurKp`Jn1R^{`e!x7a;8|dNj+d8;p{$BF(9-Rg2*YMO^5lRo9rfIxmZrzu; zjz~insD9x2$oAeaC4@3DXeAefaxt?~UwY-$DicEN2FnRMO70EvAzDY>SYUv_*cdMy zX*(O?g4b0LbS%)v3%-@ycDVQyJ@UyBH-1 za*}7@NgycyBT2crvU6yArB8bqu+J+2Eu1J~aIvdops>x9$B{TT(}U+KnY-5hEG6Es zJA0zMi~$buRu)%LX=(9rJW$SMUKuTB%F!hCo>{8c4_}uPct*$CBiK-w1xK3=m?W&R zi)pP82Lb0yOmc;1#LBRucF_enRovV=I4v`JiIybSYVRU3cJvI=ePGlg zq=%TU=7ZnJ${ICC4V|8KOTfcwodtq>-5jYR6v2SB_M=tt)>!F-I>~sY;I^OA(JOgt zEya&^U3`?g4_zhOz%ijD9IXvvH&-L<>0^I4{@wT|PV$Pk4F6st^Yp)$fz4h)03;dA zXHGF?EA2#04p1_jtIIXpTb$l951-n(!Rb8pkri{_MLxRH$L=TVCD1V2ckQ30_Ev92 zvY#Qc^ftJQtTwSsv}d^8pw$WnDnSe&W<_gV)SZ5%^1TMB%|xTX(3V=O`%DJMRoj{O zxkw?sG7+pL52Xe`?-G~$mvp!-AcujB69h!Ta{<-b5yEJ}i`TJzKXp*WtyJ>rcFCu$ zCp11N=-{G1jq5mUNsOpuhixNK`_|dH!eQ;EBSEYtu2ws5Mqdz`%~Tr*MT1(&(j|ks zG*;qpJX%CA{qi6s?b8cf`=MwT(*%YZsL=xDZm8c|>I7dw6gFen0OZH6$2o>S4*9QP z(9DdxEsRSfN3SGIK*tbnv4i#?&~>og(bTco1m(uR5ao@3l+9a)e^uD;Y+Q-B)UT8< ztxL+gnt-QgzC7&-D^F5xc+=6!v0|hnbroIM>n^qzr`fcikIP;ky!hOp+#YIvIr^J# zIu6I0pplO63%0S=TM1}@*>L5{l6U`AmNtM5tQGPZh49^1kWnqM=?gwsu3;EF#T z|EAv3oySJ_X~m^$pT}LYw;1fh4)gVllVfvZr;nzuT(MKvahUD+&50*v^q?I@m(2c9 zcs?s8o{e(|x3GQZ#;;1y^v>hd4^#ZQ)CcedLWc{{+mr}gNPz64a@F&{U8@5t=M0~q z$48L)Kr<}KXvW?#%Yp@>J^Q91-lVUAkyBjRn%wyVyBbyfm^fD62>HfCq~7@VK@#$8 zH~#6|Pb~fc0A8FSE~oNkJwf6HDca>Ilnnu*$ry2KD=1x}vVpb?k^?pjIlEb*+XeTL#rBE3H|D>4FO?+gcOMRf>z1O{88M2C7fXDNI|k7<|0s zGv|~oHxc0^2T<`TZ;GlB>($xz%zK-5w#HM-iI=sXgI?4-4p zwp#msD=3MaUZmA&nu6RNlT@otwcd}-Rb6Wj3c9k1qt`jN?6XBiooaaoO_`dSYKDAh zOas%-^{RsFh`=jyda#s?WYgnKAc!u4C|y=1@FQXCwsnRQ1#3ldvCqC1e?Y{Ie>eX9 z6yRUKoJ>pb&d6eW(LtBoix9-*i6hc;h1B%WY(b8N=-FRhAY z#>Uv;} z3p9p@op&{@BY;oQZMtAmBO)B+FZ^@;8$y<2xZcZ3*(N)X#j2KTb>W?5G?P70;_IM= zWqX;p>=x7fmA5DADIy*BY?o^(inN@)%ZFS%oJNhY1qKBen?`Lbarip~YU{U)Y9z5) z8H+dMdUT0FfkpGDJcLZdU%Abx*^Q{x)p($!-a3pSs=H`|dkV?&Ixu?e!N`=lIk0Qh z(*Pdj0B33fH`eTOV-e|jZacP=xImz(=7_-RO%lP4e>eWE)Za4vdy$Wgd^f)yDrwSm zg92Mdr{x$i`!K3##S{?xgn(lfUt7K&f=hW5rDqXNnnzHs?d@>0P1;>(N>y2CqU|{d zymIO-6liEs3_#knh}~qAoCJUp9AYz#t+ls?u*|KEJ}pxUJS~n&k*osT+=ZQnER}vzaW#&R>swa*3`5<|XzrFGhrg;HKUC#oP_ zivv{f$l9XPep-UHWnWTDr^6xv$zD=+V%RJUOcKt~(sUo;mDv2pCJGgSx?8}hIX&dX z$Fq8tnjxd#da-NmOMtH%TBuhv!`D@hc#wq6GkKhWKKxh;!5DK zdkohAhnCiWt3^W!U1&WVYk|U2qvk%AWNM@wsHH)gXk0OgFORz9VoL(d8IR~H<+5*M zekpnXOM{a!*ShMaU25%w7`CltO_TT4;$>~AHxa+4;@Bz^B5(m%TCn{20J8310YruH zs&eYIhQk)QBZf(<+fM7$v-iekYTnLbpaa+8Mv0H?1<6R<}by&JMu!0)hO(!JCK!$)#g>|Hvese|wq1VF104I%U$o zrKr~M?enSQ(qm26Wz9k5l;`MMcHO~B>rsQQ<++3pqvhqFq_*kO-$#O`B2k?a+7u%A z^0Mjdh9-^eH~!uD_txXzY#!kap+g$0_FQum^Y4+D||WRaeMM~ zxlSl0yF37IH1K!?IGp3RK-wMBE9Do4Y|J}Qc^K-PHZX?8?Koj{RB=CoChba=Z0r#` z?>NF3V6Wr6nP&B1FN~~Q6(Ck5guu9>6iO(`ifhe&?YX$Omp0jke$OG;QZ*^603(RfII9>)ncJzNkkuj1q^IO~cx~kv5Ah zcyu(7vim$43EPW|09nXr*tU;iY zT=3l@fFFD+JTd}*n-2s=g&nGpC)p3ye)Zknz`~{(6ftbyuldTXhD&4fcJvf0CoJRL z6Ae=jK`mXy@8v~S@rsL7^{7YLY&%1WEPpv&z~Jh_fK0^>=K=drRM@zwwTU%jOxPG9 zco4{LH=TO=;$MC7?{DHCX8hq{f)0?-{06qtHNKB!!BHg*z!jjQ7*+G%5WyCb7fzvj z{+6>y1~XC!Ix1|&xaGPF2Z^wNZQrm7cX90QT@tH+R|_(yb;P@a6eZDB-g9c&J5LjX z9;T_SEv4P$%yiXMs?(I ze~hIRFxvi!V74|N?j;CBWj{0#bkm7xAY(8`9gI?-Yl?B|#iDHZm@rjsKWSW-k25?L z0Ktc@kr@RkcBO(yOgMm!0yQmd>}HziHRyaC!3=DG8ac$AG(~d--#V8{w+l9^j z@`CuGfv7UXpbzcl_sb%RfwpEz=lG_2bOQ*KHdAZXz)D=*z4CYUy9;fImHCM1CDO_b zI$^n?sd}$!SZkxKdMJ%~ zfGYyS^?gDv@-+a}$hhq)Y}brP2Wz(kiqzFDq+xy5^k7VVa?-LhQc+P&Uiao@$Vc`= z={EHuuST1#Skacq;*G8r&Cc>Y9k$3AZ}(N)clQm(-{sMz)^Ycmkn33RHFiWD3}dx+ zoR#9;Y79zjg9MX+^vMg3ggdCKGZHL-Ye6WEXro-^NI>JmF=jT8X@@7e$=c6YS0aM8 zuFpgR1!IB~y7Wz34=7UhQm8V4rw|UR>ZK1BDyfivnk1kFrbDnkkrS^k{(bT9Z{pv~ zOU7Wo+01opC?G+Vi5B_gwWUYX^yDzOO6Z$lQ^sjt@UPVkZMu1B)ki^`b-hl?WQbsR zz326QjudcJ=waq@*S4tix%Iamoy2VKE;eHjC|3li(k)V$~X ze2}N8NrRis`oAK(kbRXMQHRvPmta~liJ?+e4m$N1m z7(Ie6v=cgTxt-9Hv2cMDZ>#DE=@W?hIxg9DHx`+dyM+P_>d|aglB3R=_coW4op7Sn z0%g%{Xs;VzxZf(!bXKkPPkYb?dUU5hH5nIxlm`WAPw5~Acgjd70m)@yqs{t|{gAJR zOJMk}H#TavZt>aYdPGO4*B}c2T|e33VP^e8U(mb@tTC{a?vohOZqIZ7O+U-M!Wvx3n|(C=n^kb18;wGc2vIjnDUw{yaDE4=U31IP4*5plBL+c8LJsPG9wCOc_=&h9K{Are z8~_4av_vKmU;O*x-`~f-s{6w-&~0?779*9Gu!ZFloXosT9TOAUqhSY!^IqasfEUdT z9C>I867-BPqp79CY^ZNADNhST7H(zNY3)TDPc0sQOY?{lf$L^?q z^w^EiUiyriTMLCZP4H-W1gHSA3W2OEkAJja^B8$}Rk73jSoxzpnnd|?;s%B3K5qkj zeF)a}$1d0qe_JrvH!{*pvFJKY6(3zTIc>eE1T+{Gf*^cqiRAEC11cvs8lzG$AR0=G z0M;t+Rim}XQdl3rsg?k)0;bWVj3w4zbSj^!W%Ta~ka!{wYAFQ_OGCb_z*Ez&mOX@t zv6Jh`!|X%GrZ2B4iQ2v11a+Yqy0gb2hyj4b7_(MF(%(af5`c(O`A&WD?~8wb9{+w+ z(vR@ML%rVs0-2$hYT6Hdo`~>~(^cgb9-Si;*z7bkpu;0$KBk%hqar7OR-fT6!ryX* zK*dxdj16B95KNNzq%Dk@n>-ZcR8_y^_@l9hKAq^0n8$Qv`w#})RH$}k88jpW@|S2_ zWJ}*+<#(MJrDYips!?rjU;sO`y%Fk%54Dlb6>Z`t3~n^H7bPWBaQHO8ziwVOX9;!f zX+p-sLqVgu@M@9lHzlMfhTtB}EgzhD&%6nPyIxeq5z&INbgA(&Q;bbmOH`bsK~PVN z$b+TnVPR z9LxS}v`yP~-1A}TO_2$qW46CEL`0IYT7W|fv}x)qy1?jt;! zgCbRp*Q?DH_(3(_52(t7cRR~18f?h$nl>;yqm^VZ#?>lX(aS&|TM|7_%srTPg5&wF zt75AVdxE3m);rg0#Ta8twgMtzk5nD{g`wrpy* zA76pGRGOLv6E(}U__yb^je^=@cbKwu=viu2mQqKqg=^w-AKK{+TZU^kK<~9gz_epRXYx4KDFwSoRP>CEzrwfr+QJ2HTYYK-rG)B{Pd~we- z=zii9Xbf|m_)q+Rlwhmk?V*%Nq_1)UlHXPPFBrc0sEk-NOgwn&CnJmiu2P&Kql$6H zeDwEWV`@Y>-`_v7@%I>m`vaHdKo}is`zFz+h2)g!Df1YIzj<)ykoo(|9nnvhdfLo$ znFl!IJ1H`uF^%oowZiK$8~>pYcV0BjKz05noV+XG{gq6{-@MTCu6@XS6yUExs}i}^ zZh*N2CypP#=OWH6j@S=z{oB`OZVm}JIN0;4qX@+bR`q!Gy|wCWHxMZE^~Jw0{yoOv z{y%_!Rn_w|%68@`Y{+GmGMaY#;1V6*^_^wyI7@6&ocx3!>aEI#Xs&*BEgtW8pNPtZleK<=tMJxKM0`9=2a@ILrn3u#< zRL`L#%L*p8N1Rp~)1CN4ft=WqSn%3SrI27m85xsS)aIt&gA z5WN$B1U8)Dr7}%((X)dBD;lJ%RES=-*jCPVtd3WA|}uBwLP8SAiR1llQ` zb|R35{IC!YJIA+Q{QKhHe*^z|dVUhTXmsgl+GC9Dw3#*ViUmAuV+D>Lee6EfG6XWU zr6??8Wp%a#f-&P@apSIa!=xL85SwQmX#j~_A%pe6wN_Vg2B&k_N*=;8se>rx>p~IJ zoirXGvAG7g7X9z5wvGLXJY%a;4C4N~HbnEWmyed?^sz(Ym5nA38~{UGI*@_|>VbI;I~112 zkC=oXGBo((D7wK8Y zvk@L7Eq_sSd4atU3L^Wmo;RhkdG~td*ovkCX?-S`=UT#F?yhb|L1XbbUvrj+W&YBIaUK$m@7 zjp{Qxw)?<=&8rR>LHq8Iv`oS4RV3`C%5zx23hdMlTeXkRi*@=0qt7A?iG;-i62T@^ z3yrBF2>CF_hn`M*@%H}0tmP?kLlwia2Du*egJ0L_)7)kUaURI2NV(21@@1|N6)*L) z(XWYNxL=jNfII07N{x&WEL_a9D#o)>9w9YWfAQ~&e}5hSrlwwzc4~pQ{R4L2?;h?G zQh!5X2bpw4PZ-LFm`c*r*y(wJ@V9&o2H3|YJ{k&=AtcH<-@ptSvx&^E+|t6d$8(Qq z?n(mkT663gV!Oit023lfL_t(Yh?YU#u1>2Uazn|;J%?(-QcBTGN35Wq_mia0im`OM z-{l~6L|}W~`JTA#?C-KoS!WaxtW$n_LemwL@^%b-r7u(Gxz?t^@BQhl4?wq#V$D~m zYdFDvIC>luEF#2zhTeq`4<nS(F>{WbjSA6FI}Yy~#PX-o2CTrEH(c!4DK z5jXRQc30Q_^kY|ZkH|pfwwm^X#OJjAC<$8sXMeN_c_!4aLq>5B2qFq zHvCJx1`$2czm;Ne2cV6uxZtHcOa{xXU$4MNgJ2gBS87RFJRCB_3cu;Jb3KM8j3F{j zNw-nKpynTZWmnc3#i(wb2*zZDw=g5A8O+oXtPnQoANLt8gYk5%k^;wV8vz_@YT*4t z$Dd06;@=nl{x<&A4~>hMR&f5EOgk?_tv?(IG<_q^V{v@eo; zD3$Ag)uC2LSYPQT|ZJWUk6av}s z@D4{EoX=TBH{nA*B07DO$I*7Dd`32H%7IJ`RJ5uctzaq0G7m&c&2Gv<)QKjv%jteq zAlpapZphZCPg#-^JD~zM*C7m$XiZ z<1~ST4-L4hqMPN6+@NWSLx5VHvQ#k>spxTRSWp9*NbY_+a!1H^AaMT9#+s8-KZyUj zmZXE_z43DIKxrW78q1K;%GqGK9FQl@NXHVeC!k*W-LPH^duHGRH6UAuh0<`8i5trg znEv)1jUZ|q10mnL`RCAS5s@~)IyVI+T>5!HyH+eqkA)URCshbf=FsLX?xs()WZZpN zQu)&~<+^*XygFs)k`eoKQP`(+#|~+BEO$zC$Qe;_O+$&p<2{^|J;n~pA1#yqCTE#p z&=)^#v|cL|KcO-`Ou)40fsY>Jz^JLe`1i%X{{sH~2zBy+F46$v78x5V;0P;>gnw*m z#+E}YolsL}p3|QUk4rGtHJM^^N0Z_EY3nHNO9)O)ms?~*smf?$k7ZCTHRwy_GytsB z4S^x^bE`$xlxdfrR}gt!c8+ET=XPix5^SkvNPFOTRT=GkBLI1J47)ayw5Xy$(6HAC zN_t3_I(5XnY%9td+z+-2+9GAm`6#j3T^^VN~ z!EB+A7qVcsutKItT|^p5M?wvVkdZoOc^b46;e>MVDnah>F|m#{_6C`cy~4J-g`vp9 zcd0|c%&_(gj=uQ!#XqzEm+;U3eCp%%nFrX~coAmhZ2Gx6mn~b=(TD!rx6euPMlpdz z=ChZ`$$zn3O%x5y%UlgI+2|Ke(^T(d@y`#&)pS;>EN)nUffd_@EZn}|teh`uYED>( z2$VeJbx$tiJ;XUUtYIS!JL9BBd6LR`=q~Bisuj}*tFltc#kNLvNMD%N9EX&T*9r6 zMrl)>?L4yGY&l*V@&T9UhANwGs{;@fp=<YfW2JhD#pdi+AQPw80C@=aAo~w%{Gre}*W*_H@ zTxU%tsx2aCD_Zko+66g$fO?e+X}VH+;0?9!-O#XrdMi+?4F{z3fv(UwJ5?+MIl z*(N&)6?<_+2~B1Y5EKFNf&|{2hRzv=mKYjpT{EZd0tq~jO~I;ouHg<$jJsmgHEZnN zxS+I+&Evw;HT|~e3kmIt+en_!3k-`6umQB?gw<&}f$C~*O)=cqEJ~8s*fsD}HoLPh zaO`nH*|H&WjxEkx4Y8+X{W%x+m-4@%Q=A=Svc^3!e9-(ct8Q=5Hf0D2$hPfR27w$w zk3f8gFCH@huO>sNTq2lPC~&GCZ!Do8r*+sy)PA(ADFe(jh&^E^wEOXmGP=mzwVj^6 zx)%a(Ci>pf;IVX^$^3sj0>?6@^2L2n9~$VM@iD+I3oTKoEitmlN9&16(aV%w*YQPx zf~a8=zoo7MPEj_Io=`4d{QKhHe*yn~WYNda$Dr`4*K8B=x(%JUyqjSAKZlOG)Z~ZB zS##kSA)>>Ya_APS~<xZ9SWIHPt6;ovEE>o>qHYyNF|X!X2LBa z%l)VzCROI|AfMrhS_zMCyON`m`B>8k!@@HIM+{yR27*^_rn#c@39j7sgUV9${Gkuk z3k!zTvqz%lRh)el*YKFZ2AZ~kl!~kRxzA`t+>k%ewfihS!ZLt8ACd4y8yk36Y6~FqLUY;v$|fBhRG20m(O8tW1Itq1WdkEk z;=$nK#2|HXUNZv1bX=9Oq`DlK8QgRhNwJFR5;lM#5i5l~fthnUNqQio?`?KZJZ{t> z&6@vgSIa+(0fU(vyD$RTrqim(d!Wz~03(7n z+Y&z&U!3z8Iua|G_#vwr!Oukd?qMCgCpeT-uTjs~n2E(8A`VDd5LIly?zM*_S@(BO zy9`Li4f>sAyh~#F;q%%cXha<}26e0!Rq?@v$#!=LT1Go2nEc!VdM<%wwgo8@y&k;mG_JW5T zx(Se=c?=FVN&j_Y7r?WmJyeWJG?Y8QU0O~W#yt`!#~CKrn^)mQsVyFS@$ZX&e;fbi z)hSAn`&#Lbq*~RDZ>wYppzrb8bVrvzZ}|Mm2L3ti>@RJXae)%3xQ9z~y$|f+hh9r= zx?tQ8O0A=-pwlUo3AG_Th(GdjN@``F(gLw34rPcJ;RWYAk%0V>fn?l+6i9F(_NnN( zjvtNvHvW!@Z%j}kTF$KwY8c-_m`_m#MfvyQl$>)~|B0)#1C7~tuOpHAxZ}6=+tTP( zYjJcvN{I!hZ0-3)2Px^u;;g*2o^^~fMOT#na2ig=_UsF2^Omc&@xJc82N;0_S^INw#J7lWpOmP|dfHoP z{fmF~#lJs_e^sxlA11Uj%+x)b8vD$+&~bZDF+C_zed?9S+D2FeQ9mB@2-*K?8ff7h zA5~9ARzihHecVYX)mc)QIv#-ttucl-RXqSX`H|V~OBgcPF;Q|1TaOQ9(y&nwsFYJJ z#VoBeZ?omxq_g#SgtJj^TNz|rh(Oe~TD3tr_Y{+7wCs+r667T^^@extY&J@@Q1JMy zY-l=AXM~H~7&OMPSH&A(n8!>YPjCgh8GuHD{}lF3DG;!W6X2?LY3Z)O$Zxe%cgT!| z3kht=O3Ny*1|K2k4Bf0B9$;_#rqzj@p^$5;`Za80H^Jxp@>&^;no(L&TMA=^K-t*D zp0+qW>`LtS?x;t@9#&A>EqEkJ0Dp>G+aTea;ibj4aG{J9A~mZ{-2Gd%CK0ms{NkS^ z`^CRMg@031^8@0ytFU6J$7>ZclutC9mZ65l@^csk#jU>OT675D{Q3rS$FZ}5zotK8 z{&bxd46ZrkUL(yws3;n%^eaoEv@5g8hUR3nOIoD9fzaqrb|{bdvhqbb8cJ&R7>%NW z&t+Pk*wRbjsLpH8Op-#42e8Wp%MCl(udG?vrQ;g5@SserR}r{}vJ!)NZ$k(rCu%ti zq&`zqFF3Ftef|VJsM0#Y9T%)e5>1(vv0vZLusYTqTvm-S1p_Tk6<~KPkP+C<#?p2@TB3YnU8VCQ5r%Z4<2bc9r1lMO@F zmNE-X%4zpcPi81Xki=SL*!Z_3$kfo-eNUwZ@pFiTGA5iLPQh80CmG{5O4Bz8CP$nW zcJ&CQbYK;L$j*mrwrT2$_ygf3&@p6g6=>$cz;UXhGKI_APmg$@*%N4M%$anRQyfZl zQAI16xi-JLrkZrz0OTAZlCb?O2l`A_M%@73{9HISmpmw*hfI%(U>&HOx}?HGzyW^a z_q8`AE&10M|GxP57x525{8|2ym#hK{=~KCT0s>nd7qr#TB+)jR()wx0daHkOnVwGr zatBC%u?Jf0xD++kWM;6>K?iSsPxWFNCOssI9_2$z*3)3#X4OGv9-u=r>BOcH-z^7* zpFbdA7&lhZ>?sBoQ_Xu~scB3w#X%fuRAo*NYr4j*GR!?-yfb7l6Nj9Lc7gpABInjv zmJ$8HEl0byGttS+rXy7v-CCxuxwY)DOiO3I1`DY7+3 zZA89mLTj(6(R}GI{?Tr~_!qk1+#37;8vkA;BB-rIh9hBF-T3yY=M9j0IqBSWM>bRQ z82B+sq0e%pi(4E2tlf+_f^BdbvSTTA_x3Z&yD?rhaW0#8#D^z&Yo;we)Zyw*w7o=C zSHFY*-(D~<%qPqdaKou8npj7g^gBTxC_-xvRK*x$mxs{UzqIjlq`4qB0U;-|$> z3Er4=>nEUwxg|_*6*l^QC8G8Sdpk;p#Gq-ds!<2cD>WYa!yM#8TTGRbRd!~%kLi#+ z)UxEpiVcIGbR47JmbLUQdJt&_i?=XEyb3CKbYwDJ)4b%QBk0tO9#c})6H(?CQ&mVU zlVeGqvtI!V9w=MeFwINL+a5$fu0qMdE~YG(8-fO`zdc<#Ig_MDFh@bqX6`Iu zZHS8a83c8RMP;>{-xvS>I{wxF0c{DpC}nsVYybcN07*qoM6N<$f@4wI(*OVf diff --git a/static/images/static_avatars/emailgateway-medium.png b/static/images/static_avatars/emailgateway-medium.png new file mode 100644 index 0000000000000000000000000000000000000000..348f0f2ff5b23f2ddef666601337c4b333e2743f GIT binary patch literal 160025 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%0AG`3%gqF%<@ZmE_k3Aobh{7?1F;2}|2aCs zK;eMG@5O>;jDoK=2INNqa6P!Jb(;J80!$wh{>DKnM(IpW$94IiF&}Fr z8b@NgMeUbaD;1AGR@d@(V`4mZnphRNdFuS=zU%+u9q@mkS^@j=P9*OeLG>|%BO^<^ zWCgR+Rk(liN+B8|g{wNlLAdrL{q42gbE+!ur`LDtczvnJ(4`2REfF66047B(_DhIlWAMKvY$ zumpOd!_*{gD0o~mbwW^5D^EpP)B@*lz;~B(m))hkskYk3lQpY75J5p4SkB@2f5j1m zTOEVZ(xDk;e=LXTpE0%jQveD>&~R*7gr<7g6d=q<3hOIR`bz?W)(izDw@+;*T15%7 z$kmLofW%+e?V0C|7MA|6enpq-On7x`xUpr9x>F*;0Z+mfjf>9nx%_ZA55o0$oEJJ< zbrx3FB*;zFO;HlT>a*1DM-v6?uph55P_!Yii8273BGJ6ES*|{RXliTfPT6UO9;8`1Vi$^f-x01e~dp_&#$e%nZ}M$Dbvh8hhgqim;7)RV+`Cd zHo(K*Q@(K&CvO?izVFI5I>&%rL0g)QZyxO}Zcw*x6VB|n@iVe#2K!|Rp5i2MbF~|r z5};)NJl3-b**zjLgN_fIlKx974hXlVJi{cIWI6sDk_-n1{=>z!b%UB#{wI9~w5l%1afki`wNEmtgbL zlnbU4cp5CZ&b2U$G&l@5>Msf`KuQ%v@yscKkh0KR<0^V9mm!cMS8PwMC<8N$Cv#@q@XG#Co$-YF0Bh;4L zb7tb}zLOut!^InhHqUJ%Lu)ZJ3?+0T>BIb#8^TFU&KYer11nbkRoX7{r_Lvd!ZDS> zk+lw&9RB}x`0#^0*dG{Oo#=NO+0Nw5sR^eDOS+Q8C6qIw=%AxumdVVPB)5e?w-koS2Get zcERxuAo0z%&6IEoMI@(`hLL}ej=5n~Ap-GSE1zubtJzM3CouI}0#m=_0`@iu$2gks zncJMbRMcpM^-G;`DyxXSnzki--Q%?Bv~S(?&<<5MQG)BIZ_Z8MFyF<%=|if+7j0qtsdDn}ED55SN2(koPf7kXv-T%#oUy zXVk(u6J4?BxH7j0hEE9>CR}{8JiqPF6~AFU7nBa^Lu8g;6O5BSm{Li!U##;;ZfLnL z5-cI!2UxwN^%2t&N}n;1ASv41102#Qx#?{8FCIQ$V%1UGCJ#-J;=} zBNjHwFNN-|k^NG?7Dn^zu~DfZ2HVq#Q74#aKLbL-2ErHxjtSF9pw*Ai3RTzgHW5N^ zNhz+O7$%gOLuKG?iJd&)ciNR<_N0XQuUWz!=xM*Z7yAHe_>hetK6qADj4nNW?-^t3 zP>E6u)0l%&*ppduRjL>J!^MkRC(DtnI4OleYk_Nlj1#(-@*oM;@@XHdMZI;XPn2wOSjnKpK+#6w7= zN({80?w^{7*HfRNZ>`TI^}0x8Vq{h*{?AiuOjut*OjXrU<)cyI;RZD}*S&O5)-=?r zzs+`uR?9IwG3+V8(#&0%0A<)^%?P$Y^4z7u1N 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

H`ZIL7YWnbVTYIYh-2BePw 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

9u1$#B`DoEpbfI|K*Vuar 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 F{{w9}q>lgq literal 0 HcmV?d00001 diff --git a/static/images/static_avatars/emailgateway.png b/static/images/static_avatars/emailgateway.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3f840c4c847789291d20e5e5544eca3a5800af GIT binary patch literal 5651 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 literal 0 HcmV?d00001 diff --git a/static/images/static_avatars/notification-bot-medium.png b/static/images/static_avatars/notification-bot-medium.png new file mode 100644 index 0000000000000000000000000000000000000000..74edb620e52e0dd92db0f70defa79b4bcc94b1de GIT binary patch literal 28115 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 GL;eqO<7I*X literal 0 HcmV?d00001 diff --git a/static/images/static_avatars/notification-bot.png b/static/images/static_avatars/notification-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..e179b5fc8dc2c0d331cbd4feee869d096006e360 GIT binary patch literal 4875 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

    @ebeeQ6k~vmA;bK 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 BqUZns literal 0 HcmV?d00001 diff --git a/static/images/static_avatars/welcome-bot.png b/static/images/static_avatars/welcome-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc7f27c56a7a57f54f41ee7b007cc0ef20241a2 GIT binary patch literal 9065 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 TeAAxK3V_O64TWm(hv5GQ$vk4L literal 0 HcmV?d00001 diff --git a/static/images/welcome-bot.png b/static/images/welcome-bot.png deleted file mode 100644 index 4a65e65d97b9fbeb72f37f2d5bfe9245b7d83a43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81296 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 str: + system_bot_avatar_name_map = { + settings.WELCOME_BOT: "welcome-bot", + settings.NOTIFICATION_BOT: "notification-bot", + settings.EMAIL_GATEWAY_BOT: "emailgateway", + } + return urljoin(STATIC_AVATARS_DIR, system_bot_avatar_name_map.get(email, "unknown")) + + +def get_static_avatar_url(email: str, medium: bool) -> str: + avatar_file_name = get_system_bots_avatar_file_name(email) + avatar_file_name += "-medium.png" if medium else ".png" + + return staticfiles_storage.url(avatar_file_name) + + def get_avatar_field( user_id: int, realm_id: int, @@ -63,9 +76,8 @@ def get_avatar_field( """ # System bots have hardcoded avatars - system_bot_avatar = SYSTEM_BOTS_AVATAR_FILES.get(email) - if system_bot_avatar: - return staticfiles_storage.url(system_bot_avatar) + if is_cross_realm_bot_email(email): + return get_static_avatar_url(email, medium) """ If our client knows how to calculate gravatar hashes, we diff --git a/zerver/lib/storage.py b/zerver/lib/storage.py index 956b62ed96..abd7b1d06f 100644 --- a/zerver/lib/storage.py +++ b/zerver/lib/storage.py @@ -10,7 +10,7 @@ from django.core.files.base import File from django.core.files.storage import FileSystemStorage from typing_extensions import override -from zerver.lib.avatar import SYSTEM_BOTS_AVATAR_FILES +from zerver.lib.avatar import STATIC_AVATARS_DIR if settings.DEBUG: from django.contrib.staticfiles.finders import find @@ -40,11 +40,11 @@ class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): # use a no-op hash function for these already-hashed # assets. return name - if name in SYSTEM_BOTS_AVATAR_FILES.values(): + if name.startswith(STATIC_AVATARS_DIR): # For these avatar files, we want to make sure they are # so they can hit our Nginx caching block for static files. - # We don't need to worry about stale caches since system bot - # avatars rarely change. + # We don't need to worry about stale caches since these are + # only used by the system bots. return super().hashed_name(name, content, filename) if name == "generated/emoji/emoji_api.json": # Unlike most .json files, we do want to hash this file; diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index a6acaa5021..2b172c0668 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -1532,6 +1532,29 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase): result = self.client_post("/json/users/me/avatar", {"file": fp}) self.assert_json_error(result, "Uploaded file is larger than the allowed limit of 0 MiB") + def test_system_bot_avatars_url(self) -> None: + self.login("hamlet") + system_bot_emails = [ + settings.NOTIFICATION_BOT, + settings.WELCOME_BOT, + settings.EMAIL_GATEWAY_BOT, + ] + internal_realm = get_realm(settings.SYSTEM_BOT_REALM) + + for email in system_bot_emails: + system_bot = get_system_bot(email, internal_realm.id) + response = self.client_get(f"/avatar/{email}") + redirect_url = response["Location"] + self.assertEqual(redirect_url, str(avatar_url(system_bot))) + self.assertTrue(str(redirect_url).endswith(".png")) + self.assertFalse(str(redirect_url).endswith("unknown.png")) + + response = self.client_get(f"/avatar/{email}/medium") + redirect_url = response["Location"] + self.assertEqual(redirect_url, str(avatar_url(system_bot, medium=True))) + self.assertTrue(str(redirect_url).endswith("-medium.png")) + self.assertFalse(str(redirect_url).endswith("unknown-medium.png")) + class RealmIconTest(UploadSerializeMixin, ZulipTestCase): def test_multiple_upload_failure(self) -> None: From 068ab6e11e0e291885dc2afa20b83120d29505ca Mon Sep 17 00:00:00 2001 From: PieterCK Date: Wed, 23 Oct 2024 14:08:44 +0700 Subject: [PATCH 015/276] avatar: Add checks to make sure system bot avatar exists. This commit introduces an assertion to verify that the avatar file for system bots exists and findable. In development, it'll assert that the avatar file exists in the static directory. This isn't done in production environment to avoid unnecessary overhead. It helps verify that the protocol to fetch system bot avatars still works when making changes during development. In production it'll check if the avatar file exists in the STATIC_ROOT and return a default avatar png if it doesn't. --- zerver/lib/avatar.py | 14 ++++++++++++++ zerver/tests/test_upload.py | 25 ++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/zerver/lib/avatar.py b/zerver/lib/avatar.py index 33533af0fc..d13584efe2 100644 --- a/zerver/lib/avatar.py +++ b/zerver/lib/avatar.py @@ -46,6 +46,20 @@ def get_static_avatar_url(email: str, medium: bool) -> str: avatar_file_name = get_system_bots_avatar_file_name(email) avatar_file_name += "-medium.png" if medium else ".png" + if settings.DEBUG: + # This find call may not be cheap, so we only do it in the + # development environment to do an assertion. + from django.contrib.staticfiles.finders import find + + if not find(avatar_file_name): + raise AssertionError(f"Unknown avatar file for: {email}") + elif settings.STATIC_ROOT and not staticfiles_storage.exists(avatar_file_name): + # Fallback for the case where no avatar exists; this should + # never happen in practice. This logic cannot be executed + # while STATIC_ROOT is not defined, so the above STATIC_ROOT + # check is important. + return DEFAULT_AVATAR_FILE + return staticfiles_storage.url(avatar_file_name) diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index 2b172c0668..ae216361a1 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -26,7 +26,12 @@ from zerver.actions.realm_logo import do_change_logo_source from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property from zerver.actions.user_settings import do_delete_avatar_image from zerver.lib.attachments import validate_attachment_request -from zerver.lib.avatar import avatar_url, get_avatar_field +from zerver.lib.avatar import ( + DEFAULT_AVATAR_FILE, + avatar_url, + get_avatar_field, + get_static_avatar_url, +) from zerver.lib.cache import cache_delete, cache_get, get_realm_used_upload_space_cache_key from zerver.lib.create_user import copy_default_settings from zerver.lib.initial_password import initial_password @@ -1540,20 +1545,30 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase): settings.EMAIL_GATEWAY_BOT, ] internal_realm = get_realm(settings.SYSTEM_BOT_REALM) + default_avatar = DEFAULT_AVATAR_FILE for email in system_bot_emails: system_bot = get_system_bot(email, internal_realm.id) response = self.client_get(f"/avatar/{email}") redirect_url = response["Location"] self.assertEqual(redirect_url, str(avatar_url(system_bot))) - self.assertTrue(str(redirect_url).endswith(".png")) - self.assertFalse(str(redirect_url).endswith("unknown.png")) response = self.client_get(f"/avatar/{email}/medium") redirect_url = response["Location"] self.assertEqual(redirect_url, str(avatar_url(system_bot, medium=True))) - self.assertTrue(str(redirect_url).endswith("-medium.png")) - self.assertFalse(str(redirect_url).endswith("unknown-medium.png")) + + with ( + self.settings(STATIC_ROOT="static/"), + patch("zerver.lib.avatar.staticfiles_storage.exists") as mock_exists, + ): + mock_exists.return_value = False + static_avatar_url = get_static_avatar_url("false-bot@zulip.com", False) + self.assertIn(default_avatar, static_avatar_url) + + with self.settings(DEBUG=True), self.assertRaises(AssertionError) as e: + get_static_avatar_url("false-bot@zulip.com", False) + expected_error_message = "Unknown avatar file for: false-bot@zulip.com" + self.assertEqual(str(e.exception), expected_error_message) class RealmIconTest(UploadSerializeMixin, ZulipTestCase): From 433968cbb69852e603dbc12345d87576ef081530 Mon Sep 17 00:00:00 2001 From: PieterCK Date: Tue, 22 Oct 2024 17:14:45 +0700 Subject: [PATCH 016/276] storage: Reformat hashed medium static avatar files. This commit reformats hashed medium static avatar files("-medium.png" files). Unlike user-uploaded avatar files, avatars served alongside static files are hashed by Django. To implement the URL patterns for finding medium-size avatar URLs that are hardcoded into current versions of the mobile apps, the medium avatar files need to be adjusted to include the "-medium" string just before the file type. For example, the URL format will now be: Before: welcome-bot-medium.123123321.png After: welcome-bot.123123123-medium.png --- zerver/lib/storage.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/zerver/lib/storage.py b/zerver/lib/storage.py index abd7b1d06f..0465ae69a8 100644 --- a/zerver/lib/storage.py +++ b/zerver/lib/storage.py @@ -24,6 +24,25 @@ else: return os.path.join(settings.STATIC_ROOT, path) +def reformat_medium_filename(hashed_name: str) -> str: + """ + Because the protocol for getting medium-size avatar URLs + was never fully documented, the mobile apps use a + substitution of the form s/.png/-medium.png/ to get the + medium-size avatar URLs. Thus, we must ensure the hashed + filenames for system bot avatars follow this naming convention. + """ + + name_parts = hashed_name.rsplit(".", 1) + base_name = name_parts[0] + + if len(name_parts) != 2 or "-medium" not in base_name: + return hashed_name + extension = name_parts[1].replace("png", "medium.png") + base_name = base_name.replace("-medium", "") + return f"{base_name}-{extension}" + + class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): @override def hashed_name( @@ -45,7 +64,11 @@ class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): # so they can hit our Nginx caching block for static files. # We don't need to worry about stale caches since these are # only used by the system bots. - return super().hashed_name(name, content, filename) + if not name.endswith("-medium.png"): + return super().hashed_name(name, content, filename) + + hashed_medium_file = super().hashed_name(name, content, filename) + return reformat_medium_filename(hashed_medium_file) if name == "generated/emoji/emoji_api.json": # Unlike most .json files, we do want to hash this file; # its hashed URL is returned as part of the API. See From ec437fb770a4e4aa6aa53a178c111afb4ee5a371 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 22 Oct 2024 20:42:19 -0700 Subject: [PATCH 017/276] settings: Replace deprecated STATICFILES_STORAGE. https://docs.djangoproject.com/en/4.2/releases/4.2/#custom-file-storages https://docs.djangoproject.com/en/5.1/releases/5.1/#features-removed-in-5-1 Signed-off-by: Anders Kaseorg --- zproject/computed_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zproject/computed_settings.py b/zproject/computed_settings.py index 407713c3ea..e66b949b08 100644 --- a/zproject/computed_settings.py +++ b/zproject/computed_settings.py @@ -615,7 +615,7 @@ LOCAL_FILES_DIR = os.path.join(LOCAL_UPLOADS_DIR, "files") if LOCAL_UPLOADS_DIR # ZulipStorage when not DEBUG. if not DEBUG: - STATICFILES_STORAGE = "zerver.lib.storage.ZulipStorage" + STORAGES = {"staticfiles": {"BACKEND": "zerver.lib.storage.ZulipStorage"}} if PRODUCTION: STATIC_ROOT = "/home/zulip/prod-static" else: From 14db6e8c147e94fa0316b3c97ad13dbb327ef802 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 22 Oct 2024 21:21:51 -0700 Subject: [PATCH 018/276] capitalization: Avoid bs4.MarkupResemblesLocatorWarning. Signed-off-by: Anders Kaseorg --- tools/lib/capitalization.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py index 5d4394a65d..17afcd37f8 100644 --- a/tools/lib/capitalization.py +++ b/tools/lib/capitalization.py @@ -1,4 +1,5 @@ import re +from io import StringIO from re import Match from bs4 import BeautifulSoup @@ -182,7 +183,8 @@ IGNORED_PHRASES.sort(key=len, reverse=True) # text using BeautifulSoup and then removes extra whitespaces from # it. This step enables us to add HTML in our regexes directly. COMPILED_IGNORED_PHRASES = [ - re.compile(r" ".join(BeautifulSoup(regex, "lxml").text.split())) for regex in IGNORED_PHRASES + re.compile(r" ".join(BeautifulSoup(StringIO(regex), "lxml").text.split())) + for regex in IGNORED_PHRASES ] SPLIT_BOUNDARY = r"?.!" # Used to split string into sentences. @@ -241,7 +243,7 @@ def get_safe_text(text: str) -> str: This returns text which is rendered by BeautifulSoup and is in the form that can be split easily and has all IGNORED_PHRASES processed. """ - soup = BeautifulSoup(text, "lxml") + soup = BeautifulSoup(StringIO(text), "lxml") text = " ".join(soup.text.split()) # Remove extra whitespaces. for phrase_regex in COMPILED_IGNORED_PHRASES: text = phrase_regex.sub(replace_with_safe_phrase, text) From 6a4c4195f5585f7162ac5eef21810a08be490553 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 22 Oct 2024 20:30:20 -0700 Subject: [PATCH 019/276] ci: Enable Python warnings. Signed-off-by: Anders Kaseorg --- .github/workflows/zulip-ci.yml | 2 +- tools/ci/activate-venv | 2 + tools/ci/setup-backend | 3 ++ tools/python-warnings.bash | 81 ++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tools/python-warnings.bash diff --git a/.github/workflows/zulip-ci.yml b/.github/workflows/zulip-ci.yml index 9f3eea647a..c6f215b8a1 100644 --- a/.github/workflows/zulip-ci.yml +++ b/.github/workflows/zulip-ci.yml @@ -217,7 +217,7 @@ jobs: - name: Test locked requirements if: ${{ matrix.os == 'jammy' }} run: | - . /srv/zulip-py3-venv/bin/activate && \ + source tools/ci/activate-venv ./tools/test-locked-requirements - name: Upload coverage reports diff --git a/tools/ci/activate-venv b/tools/ci/activate-venv index dcaff5ea72..50e18883cd 100644 --- a/tools/ci/activate-venv +++ b/tools/ci/activate-venv @@ -1,4 +1,6 @@ #!/usr/bin/env bash +ZULIP_PATH="$(dirname "${BASH_SOURCE[0]}")/../.." source /srv/zulip-py3-venv/bin/activate +source "$ZULIP_PATH"/tools/python-warnings.bash echo "Using $VIRTUAL_ENV" diff --git a/tools/ci/setup-backend b/tools/ci/setup-backend index 48777fe336..9e39a56cf9 100755 --- a/tools/ci/setup-backend +++ b/tools/ci/setup-backend @@ -2,6 +2,9 @@ set -e set -x +ZULIP_PATH="$(dirname "${BASH_SOURCE[0]}")/../.." +. "$ZULIP_PATH/tools/python-warnings.bash" + # This is just a thin wrapper around provision. # Provisioning may fail due to many issues but most of the times a network # connection issue is the reason. So we are going to retry entire provisioning diff --git a/tools/python-warnings.bash b/tools/python-warnings.bash new file mode 100644 index 0000000000..80a116641e --- /dev/null +++ b/tools/python-warnings.bash @@ -0,0 +1,81 @@ +# shellcheck shell=bash + +export PYTHONWARNINGS=error + +PYTHONWARNINGS+=',ignore::ResourceWarning' + +# https://github.com/disqus/django-bitfield/pull/135 +PYTHONWARNINGS+=',default:Attribute s is deprecated and will be removed in Python 3.14; use value instead:DeprecationWarning:__main__' + +# https://github.com/jaysonsantos/python-binary-memcached/pull/257 +PYTHONWARNINGS+=',ignore:urllib.parse.splitport() is deprecated as of 3.8:DeprecationWarning:bmemcached.protocol' + +# https://github.com/boto/botocore/pull/3239 +PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:botocore.auth' + +# https://bugs.launchpad.net/beautifulsoup/+bug/2076897 +PYTHONWARNINGS+=',ignore:The '\''strip_cdata'\'' option of HTMLParser() has never done anything and will eventually be removed.:DeprecationWarning:bs4.builder._lxml' + +# https://github.com/fabfuel/circuitbreaker/pull/63 +PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:circuitbreaker' + +# This gets triggered due to our do_patch_activate_script +PYTHONWARNINGS+=',default:Attempting to work in a virtualenv.:UserWarning:IPython.core.interactiveshell' + +# https://github.com/SAML-Toolkits/python3-saml/pull/420 +PYTHONWARNINGS+=',ignore:datetime.datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version.:DeprecationWarning:onelogin.saml2.utils' +PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:onelogin.saml2.utils' + +# Probably due to ancient pip +PYTHONWARNINGS+=',default:DEPRECATION::pip._internal.models.link' +PYTHONWARNINGS+=',default:Unimplemented abstract methods:DeprecationWarning:pip._internal.metadata.importlib._dists' +PYTHONWARNINGS+=',default:module '\''sre_constants'\'' is deprecated:DeprecationWarning:pip._vendor.pyparsing' +PYTHONWARNINGS+=',default:Creating a LegacyVersion has been deprecated and will be removed in the next major release:DeprecationWarning:pip._vendor.packaging.version' +PYTHONWARNINGS+=',default:'\''cgi'\'' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pip._internal.index.collector' +PYTHONWARNINGS+=',default:path is deprecated.:DeprecationWarning:pip._vendor.certifi.core' +PYTHONWARNINGS+=',default:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pip._vendor.urllib3.util.ssl_' +PYTHONWARNINGS+=',default:Creating a LegacyVersion has been deprecated and will be removed in the next major release:DeprecationWarning:pip._vendor.packaging.specifiers' +PYTHONWARNINGS+=',default:path is deprecated.:DeprecationWarning:pip._vendor.pep517.wrappers' +PYTHONWARNINGS+=',default:The distutils package is deprecated and slated for removal in Python 3.12.:DeprecationWarning:pip._internal.locations' +PYTHONWARNINGS+=',default:The distutils.sysconfig module is deprecated:DeprecationWarning:pip._internal.locations' +PYTHONWARNINGS+=',default:ssl.match_hostname() is deprecated:DeprecationWarning:pip._vendor.urllib3.connection' +PYTHONWARNINGS+=',default:The distutils package is deprecated and slated for removal in Python 3.12.:DeprecationWarning:pip._internal.locations._distutils' +PYTHONWARNINGS+=',default:The distutils.sysconfig module is deprecated:DeprecationWarning:distutils.command.install' +PYTHONWARNINGS+=',default:The distutils package is deprecated and slated for removal in Python 3.12.:DeprecationWarning:pip._internal.cli.cmdoptions' + +# https://github.com/python-openapi/openapi-core/issues/931 +PYTHONWARNINGS+=',ignore::DeprecationWarning:openapi_core.validation.request.validators' + +# pkg_resources deprecation +PYTHONWARNINGS+=',default:pkg_resources is deprecated as an API.:DeprecationWarning' +PYTHONWARNINGS+=',default:Deprecated call to `pkg_resources.declare_namespace(:DeprecationWarning:pkg_resources' + +# https://github.com/seb-m/pyinotify/issues/204 +PYTHONWARNINGS+=',ignore:The asyncore module is deprecated and will be removed in Python 3.12.:DeprecationWarning:pyinotify' + +# Semgrep still supports Python 3.8 +PYTHONWARNINGS+=',ignore:path is deprecated.:DeprecationWarning:semgrep.semgrep_core' + +# Various warnings from setuptools +PYTHONWARNINGS+=',default:bdist_wheel.universal is deprecated:UserWarning' +PYTHONWARNINGS+=',ignore:setup.py install is deprecated.:UserWarning' +PYTHONWARNINGS+=',default:Unknown distribution option:UserWarning' +PYTHONWARNINGS+=',default:setuptools.installer and fetch_build_eggs are deprecated.:UserWarning' +PYTHONWARNINGS+=',default:The '\''wheel'\'' package is no longer the canonical location of the '\''bdist_wheel'\'' command:DeprecationWarning:wheel.bdist_wheel' +PYTHONWARNINGS+=',default:Package '\''integrations.:UserWarning' +PYTHONWARNINGS+=',default:Package '\''zulip.:UserWarning' +PYTHONWARNINGS+=',default:Could not find libsqlite3:UserWarning' + +# https://github.com/scrapy/scrapy/issues/3288 +PYTHONWARNINGS+=',ignore:Passing method to twisted.internet.ssl.CertificateOptions was deprecated in Twisted 17.1.0.:DeprecationWarning:scrapy.core.downloader.contextfactory' + +# https://github.com/scrapy/scrapy/issues/6450 +PYTHONWARNINGS+=',ignore:twisted.web.http.HTTPClient was deprecated in Twisted 24.7.0:DeprecationWarning:scrapy.core.downloader.webclient' + +# https://github.com/adamchainz/time-machine/pull/486 +PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:time_machine' + +# https://github.com/zulip/python-zulip-api/pull/833 +PYTHONWARNINGS+=',default:distro.linux_distribution() is deprecated.:DeprecationWarning:zulip' + +export SQLALCHEMY_WARN_20=1 From 70d9d8c09e4b1ce959b13a4db4617f3d7bacd0a1 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Wed, 23 Oct 2024 07:10:15 +0000 Subject: [PATCH 020/276] message_scroll: Remove unnecessary `setTimeout` call. Since `scroll_finished` is already called post render and we don't have to wait for anything rendering before calling `unread_ops.process_visible`, we can just directly call it. --- web/src/message_scroll.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/message_scroll.js b/web/src/message_scroll.js index dba55a6565..8057faaf98 100644 --- a/web/src/message_scroll.js +++ b/web/src/message_scroll.js @@ -96,7 +96,7 @@ export function scroll_finished() { // enter the screen and become read. Calling // unread_ops.process_visible will update necessary // data structures and DOM elements. - setTimeout(unread_ops.process_visible, 0); + unread_ops.process_visible(); } else { message_scroll_state.set_update_selection_on_next_scroll(true); } From 625245af50368f559a945317534279e177e0676e Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Wed, 23 Oct 2024 11:59:32 -0500 Subject: [PATCH 021/276] left_sidebar: Maintain Channels hover state while showing popover. --- web/src/add_stream_options_popover.ts | 5 +++++ web/styles/left_sidebar.css | 30 +++++++++++++-------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/web/src/add_stream_options_popover.ts b/web/src/add_stream_options_popover.ts index e7b4f2c5d8..cb5d4e5ea7 100644 --- a/web/src/add_stream_options_popover.ts +++ b/web/src/add_stream_options_popover.ts @@ -31,6 +31,8 @@ export function initialize(): void { instance.setContent(parse_html(render_left_sidebar_stream_setting_popover())); popover_menus.on_show_prep(instance); + $("#streams_header").addClass("showing-streams-popover"); + // When showing the popover menu, we want the // "Add channels" and the "Filter channels" tooltip // to appear below the "Add channels" icon. @@ -55,6 +57,9 @@ export function initialize(): void { onHidden(instance) { instance.destroy(); popover_menus.popover_instances.stream_settings = null; + + $("#streams_header").removeClass("showing-streams-popover"); + // After the popover menu is closed, we want the // "Add channels" and the "Filter channels" tooltip // to appear at it's original position that is diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index f8ee22fc60..54ec179b83 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -1556,16 +1556,6 @@ li.topic-list-item { background-color: var(--color-background); border-radius: 4px; - &:hover { - background-color: var(--color-background-opaque-hover-narrow-filter); - box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover); - - .left-sidebar-title, - .sidebar-heading-icon { - opacity: var(--opacity-sidebar-heading-hover); - } - } - &.showing-stream-search-section { /* Open up the stream-search rows. The 10px row maintains space with the streams list @@ -1610,10 +1600,6 @@ li.topic-list-item { } } - &:hover #filter_streams_tooltip { - display: flex; - } - #add_streams_tooltip { grid-row: 1 / 1; margin: 2px 0; @@ -1632,8 +1618,20 @@ li.topic-list-item { } } - &:hover #streams_inline_icon { - display: flex; + &:hover, + &.showing-streams-popover { + background-color: var(--color-background-opaque-hover-narrow-filter); + box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover); + + .left-sidebar-title, + .sidebar-heading-icon { + opacity: var(--opacity-sidebar-heading-hover); + } + + #filter_streams_tooltip, + #streams_inline_icon { + display: flex; + } } .stream_search_section { From 9a72d6e72ead220f78c055e77814cdc24c91b977 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 23 Oct 2024 21:30:10 +0530 Subject: [PATCH 022/276] user_groups: Do not show invalid subgroups in typeahead. We do not show groups that will break the DAG constraint on being added to a group as subgroups in the typeahead shown in the members edit UI. Fixes #32087. --- web/src/add_subscribers_pill.ts | 36 ++++++++++++++- web/src/pill_typeahead.ts | 10 ++++- web/src/user_group_edit_members.ts | 5 +++ web/src/user_groups.ts | 24 ++++++++++ web/tests/pill_typeahead.test.js | 8 +++- web/tests/user_groups.test.js | 71 ++++++++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 4 deletions(-) diff --git a/web/src/add_subscribers_pill.ts b/web/src/add_subscribers_pill.ts index 452085007d..46f7544330 100644 --- a/web/src/add_subscribers_pill.ts +++ b/web/src/add_subscribers_pill.ts @@ -11,6 +11,7 @@ import * as stream_pill from "./stream_pill"; import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper"; import * as user_group_pill from "./user_group_pill"; import * as user_groups from "./user_groups"; +import type {UserGroup} from "./user_groups"; import * as user_pill from "./user_pill"; export function create_item_from_text( @@ -51,17 +52,28 @@ export function set_up_pill_typeahead({ pill_widget, $pill_container, get_users, + get_user_groups, }: { pill_widget: CombinedPillContainer; $pill_container: JQuery; get_users: () => User[]; + get_user_groups?: () => UserGroup[]; }): void { - const opts = { + const opts: { + user_source: () => User[]; + stream: boolean; + user_group: boolean; + user: boolean; + user_group_source?: () => UserGroup[]; + } = { user_source: get_users, stream: true, user_group: true, user: true, }; + if (get_user_groups !== undefined) { + opts.user_group_source = get_user_groups; + } pill_typeahead.set_up_combined($pill_container.find(".input"), pill_widget, opts); } @@ -91,9 +103,11 @@ export function generate_pill_html(item: CombinedPill): string { export function create({ $pill_container, get_potential_subscribers, + get_potential_groups, }: { $pill_container: JQuery; get_potential_subscribers: () => User[]; + get_potential_groups?: () => UserGroup[]; }): CombinedPillContainer { const pill_widget = input_pill.create({ $container: $pill_container, @@ -106,8 +120,26 @@ export function create({ const potential_subscribers = get_potential_subscribers(); return user_pill.filter_taken_users(potential_subscribers, pill_widget); } + const opts: { + pill_widget: CombinedPillContainer; + $pill_container: JQuery; + get_users: () => User[]; + get_user_groups?: () => UserGroup[]; + } = { + pill_widget, + $pill_container, + get_users, + }; + if (get_potential_groups !== undefined) { + function get_user_groups(): UserGroup[] { + assert(get_potential_groups !== undefined); + const potential_groups = get_potential_groups(); + return user_group_pill.filter_taken_groups(potential_groups, pill_widget); + } + opts.get_user_groups = get_user_groups; + } - set_up_pill_typeahead({pill_widget, $pill_container, get_users}); + set_up_pill_typeahead(opts); const $pill_widget_input = $pill_container.find(".input"); const $pill_widget_button = $pill_container.parent().find(".add-users-button"); diff --git a/web/src/pill_typeahead.ts b/web/src/pill_typeahead.ts index ee0eb356ec..fad05a283f 100644 --- a/web/src/pill_typeahead.ts +++ b/web/src/pill_typeahead.ts @@ -222,6 +222,7 @@ export function set_up_combined( user_group?: boolean; stream?: boolean; user_source?: () => User[]; + user_group_source?: () => UserGroup[]; exclude_bots?: boolean; update_func?: () => void; }, @@ -252,7 +253,14 @@ export function set_up_combined( } if (include_user_groups) { - source = [...source, ...user_group_pill.typeahead_source(pills)]; + if (opts.user_group_source !== undefined) { + const groups: UserGroupPillData[] = opts + .user_group_source() + .map((user_group) => ({type: "user_group", ...user_group})); + source = [...source, ...groups]; + } else { + source = [...source, ...user_group_pill.typeahead_source(pills)]; + } } if (include_users) { diff --git a/web/src/user_group_edit_members.ts b/web/src/user_group_edit_members.ts index 9a6b12b0c0..d87e8ab3e6 100644 --- a/web/src/user_group_edit_members.ts +++ b/web/src/user_group_edit_members.ts @@ -44,6 +44,10 @@ function get_potential_members(): User[] { return people.filter_all_users(is_potential_member); } +function get_potential_subgroups(): UserGroup[] { + return user_groups.get_potential_subgroups(current_group_id); +} + function get_user_group_members(group: UserGroup): (User | UserGroup)[] { const member_ids = [...group.members]; const member_users = people.get_users_from_ids(member_ids); @@ -141,6 +145,7 @@ export function enable_member_management({ pill_widget = add_subscribers_pill.create({ $pill_container, get_potential_subscribers: get_potential_members, + get_potential_groups: get_potential_subgroups, }); $pill_container.find(".input").on("input", () => { diff --git a/web/src/user_groups.ts b/web/src/user_groups.ts index 2172384c00..492b5afd39 100644 --- a/web/src/user_groups.ts +++ b/web/src/user_groups.ts @@ -311,6 +311,30 @@ export function get_recursive_group_members(target_user_group: UserGroup): Set { + if (user_group.id === target_user_group.id) { + return false; + } + + if (already_subgroup_ids.has(user_group.id)) { + return false; + } + + const recursive_subgroup_ids = get_recursive_subgroups(user_group); + assert(recursive_subgroup_ids !== undefined); + if (recursive_subgroup_ids.has(target_user_group.id)) { + return false; + } + return true; + }); +} + export function is_user_in_group( user_group_id: number, user_id: number, diff --git a/web/tests/pill_typeahead.test.js b/web/tests/pill_typeahead.test.js index b8723fa3a3..33fb250c8f 100644 --- a/web/tests/pill_typeahead.test.js +++ b/web/tests/pill_typeahead.test.js @@ -465,7 +465,11 @@ run_test("set_up_combined", ({mock_template, override, override_rewire}) => { }) .filter(Boolean); if (opts.user_group) { - expected_result = [...expected_result, ...group_items]; + if (opts.user_group_source) { + expected_result = [...expected_result, ...opts.user_group_source()]; + } else { + expected_result = [...expected_result, ...group_items]; + } } if (opts.user) { if (opts.user_source) { @@ -530,6 +534,8 @@ run_test("set_up_combined", ({mock_template, override, override_rewire}) => { {user: true, user_source: () => [fred_item, mark_item]}, {stream: true}, {user_group: true}, + // user and custom user group source. + {user_group: true, user_group_source: () => [admins_item]}, {user_group: true, stream: true}, {user_group: true, user: true}, {user: true, stream: true}, diff --git a/web/tests/user_groups.test.js b/web/tests/user_groups.test.js index 51825e8112..48a6121eb6 100644 --- a/web/tests/user_groups.test.js +++ b/web/tests/user_groups.test.js @@ -583,3 +583,74 @@ run_test("get_display_group_name", () => { assert.equal(user_groups.get_display_group_name(all.name), "translated: Everyone"); assert.equal(user_groups.get_display_group_name(students.name), "Students"); }); + +run_test("get_potential_subgroups", () => { + // Remove existing groups. + user_groups.init(); + + const admins = { + name: "Administrators", + id: 1, + members: new Set([1]), + is_system_group: false, + direct_subgroup_ids: new Set([4]), + }; + const all = { + name: "Everyone", + id: 2, + members: new Set([2, 3]), + is_system_group: false, + direct_subgroup_ids: new Set([1, 3]), + }; + const students = { + name: "Students", + id: 3, + members: new Set([4, 5]), + is_system_group: false, + direct_subgroup_ids: new Set([]), + }; + const teachers = { + name: "Teachers", + id: 4, + members: new Set([6, 7, 8]), + is_system_group: false, + direct_subgroup_ids: new Set([]), + }; + const science = { + name: "Science", + id: 5, + members: new Set([9]), + is_system_group: false, + direct_subgroup_ids: new Set([]), + }; + + user_groups.initialize({ + realm_user_groups: [admins, all, students, teachers, science], + }); + + function get_potential_subgroup_ids(group_id) { + return user_groups + .get_potential_subgroups(group_id) + .map((subgroup) => subgroup.id) + .sort(); + } + + assert.deepEqual(get_potential_subgroup_ids(all.id), [teachers.id, science.id]); + assert.deepEqual(get_potential_subgroup_ids(admins.id), [students.id, science.id]); + assert.deepEqual(get_potential_subgroup_ids(teachers.id), [students.id, science.id]); + assert.deepEqual(get_potential_subgroup_ids(students.id), [admins.id, teachers.id, science.id]); + assert.deepEqual(get_potential_subgroup_ids(science.id), [ + admins.id, + all.id, + students.id, + teachers.id, + ]); + + user_groups.add_subgroups(all.id, [teachers.id]); + user_groups.add_subgroups(teachers.id, [science.id]); + assert.deepEqual(get_potential_subgroup_ids(all.id), [science.id]); + assert.deepEqual(get_potential_subgroup_ids(admins.id), [students.id, science.id]); + assert.deepEqual(get_potential_subgroup_ids(teachers.id), [students.id]); + assert.deepEqual(get_potential_subgroup_ids(students.id), [admins.id, teachers.id, science.id]); + assert.deepEqual(get_potential_subgroup_ids(science.id), [students.id]); +}); From 29aad5c940328e3bb8fd916a3908c28d5abd04e8 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Wed, 23 Oct 2024 10:57:28 -0700 Subject: [PATCH 023/276] user_groups: Clean up members field comment. --- web/src/user_groups.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/user_groups.ts b/web/src/user_groups.ts index 492b5afd39..d31e700e93 100644 --- a/web/src/user_groups.ts +++ b/web/src/user_groups.ts @@ -14,9 +14,9 @@ import type {UserGroupUpdateEvent} from "./types"; type UserGroupRaw = z.infer; -// The members field is a number array which we convert -// to a Set in the initialize function. export const user_group_schema = raw_user_group_schema.extend({ + // These are delivered via the API as lists, but converted to sets + // during initialization for more convenient manipulation. members: z.set(z.number()), direct_subgroup_ids: z.set(z.number()), }); From 93006ac4cfe332e7b3d3e7d912545c3b9f3d75e9 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Sun, 20 Oct 2024 09:26:42 +0000 Subject: [PATCH 024/276] realm_redirect: Change bottom text. Fixes #32042 --- templates/zerver/realm_redirect.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/zerver/realm_redirect.html b/templates/zerver/realm_redirect.html index 9ffedcb22f..0f7dafdd3e 100644 --- a/templates/zerver/realm_redirect.html +++ b/templates/zerver/realm_redirect.html @@ -44,7 +44,9 @@

    - {{ _("Need to get your group started on Zulip?") }} {{ _("Create a new organization.") }} + {% trans org_creation_link="/new/" %} + Create a new organization if you don't have one yet. + {% endtrans %}
    From eab9a3e55282eb8ac4c667612ea1e8bdf61298b4 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Fri, 18 Oct 2024 19:02:10 +0000 Subject: [PATCH 025/276] message_events: Refresh `is-followed` narrow on topic visibility update. This adds live update support for `is-followed` narrow. We need to render the narrow again instead of just adding the relevant messages from the updated topic since it not easy to determine which message we need to add based on the selected message of the user and which messages to ask from the server. --- web/src/message_events.js | 41 ++++++++++++++++++++++++++++--- web/src/server_events_dispatch.js | 5 +++- web/src/user_topics_ui.ts | 10 ++++++-- web/tests/dispatch.test.js | 1 + 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/web/src/message_events.js b/web/src/message_events.js index 4433787310..e8550acb7a 100644 --- a/web/src/message_events.js +++ b/web/src/message_events.js @@ -39,6 +39,38 @@ import * as unread from "./unread"; import * as unread_ui from "./unread_ui"; import * as util from "./util"; +export function update_current_view_for_topic_visibility() { + // If we have rendered message list / cached data based on topic + // visibility policy, we need to rerender it to reflect the changes. It + // is easier to just load the narrow from scratch, instead of asking server + // for relevant messages in the updated topic. + const filter = message_lists.current?.data.filter; + if ( + filter !== undefined && + (filter.sorted_term_types().includes("is-followed") || + filter.sorted_term_types().includes("not-is-followed")) + ) { + // Use `set_timeout to call after we update the topic + // visibility policy locally. + // Calling this outside `user_topics_ui` to avoid circular imports. + const msg_list_id = message_lists.current.id; + setTimeout(() => { + if (message_lists.current.id !== msg_list_id) { + // Check if the message list is still the same. + return; + } + + message_view.show(filter.terms(), { + then_select_id: message_lists.current.selected_id(), + trigger: "topic visibility policy change", + force_rerender: true, + }); + }, 0); + return true; + } + return false; +} + export function update_views_filtered_on_message_property( message_ids, property_term_type, @@ -47,8 +79,11 @@ export function update_views_filtered_on_message_property( // NOTE: Call this function after updating the message property locally. assert(!property_term_type.includes("not-")); - // List of narrow terms whose msg list doesn't get updated elsewhere but - // can be applied locally. + // List of narrow terms where the message list doesn't get + // automatically updated elsewhere when the property changes, but + // we can apply locally if we have the message. + // + // is:followed is handled via update_current_view_for_topic_visibility. const supported_term_types = [ "has-image", "has-link", @@ -58,8 +93,6 @@ export function update_views_filtered_on_message_property( "is-unread", "is-mentioned", "is-alerted", - // TODO: Implement support for these terms. - // "is-followed", ]; if (message_ids.length === 0 || !supported_term_types.includes(property_term_type)) { diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index a45c015768..dcc5b28fa3 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -1009,7 +1009,10 @@ export function dispatch_normal_event(event) { break; case "user_topic": - user_topics_ui.handle_topic_updates(event); + user_topics_ui.handle_topic_updates( + event, + message_events.update_current_view_for_topic_visibility(), + ); break; } } diff --git a/web/src/user_topics_ui.ts b/web/src/user_topics_ui.ts index 7b7cf7990e..02db6e10cc 100644 --- a/web/src/user_topics_ui.ts +++ b/web/src/user_topics_ui.ts @@ -33,7 +33,10 @@ function should_add_topic_update_delay(visibility_policy: number): boolean | und return is_topic_muted && is_relevant_popover_open && !is_inbox_view && !is_topic_narrow; } -export function handle_topic_updates(user_topic_event: ServerUserTopic): void { +export function handle_topic_updates( + user_topic_event: ServerUserTopic, + refreshed_current_narrow = false, +): void { // Update the UI after changes in topic visibility policies. user_topics.set_user_topic(user_topic_event); @@ -41,11 +44,14 @@ export function handle_topic_updates(user_topic_event: ServerUserTopic): void { () => { stream_list.update_streams_sidebar(); unread_ui.update_unread_counts(); - message_lists.current?.update_muting_and_rerender(); recent_view_ui.update_topic_visibility_policy( user_topic_event.stream_id, user_topic_event.topic_name, ); + + if (!refreshed_current_narrow) { + message_lists.current?.update_muting_and_rerender(); + } }, should_add_topic_update_delay(user_topic_event.visibility_policy) ? 500 : 0, ); diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index c0dca90fb5..2656f02ea2 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -34,6 +34,7 @@ const information_density = mock_esm("../src/information_density"); const linkifiers = mock_esm("../src/linkifiers"); const message_events = mock_esm("../src/message_events", { update_views_filtered_on_message_property: noop, + update_current_view_for_topic_visibility: noop, }); const message_lists = mock_esm("../src/message_lists"); const user_topics_ui = mock_esm("../src/user_topics_ui"); From 75d834dcc3f1637f70712e4128ab1fe202bda270 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Fri, 6 Sep 2024 14:38:40 -0700 Subject: [PATCH 026/276] styles: Remove ineffective animation of display. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s not possible to animate or transition the `display` property, at least until `transition-behavior: allow-discrete` lands in all browsers. We already take care of applying `display: none` in a JavaScript setTimeout (see alert_popup.ts, hide_error in ui_report.ts). Signed-off-by: Anders Kaseorg --- web/styles/alerts.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/styles/alerts.css b/web/styles/alerts.css index ca3df6ae90..2bec56e098 100644 --- a/web/styles/alerts.css +++ b/web/styles/alerts.css @@ -214,7 +214,6 @@ $alert-error-red: hsl(0deg 80% 40%); /* animation section */ @keyframes fadeIn { 0% { - display: block; opacity: 0; transform: translateY(-100px); } @@ -232,7 +231,6 @@ $alert-error-red: hsl(0deg 80% 40%); } 100% { - display: none; opacity: 0; transform: translateY(-100px); } From dfc311ae9627e3bc867c730f53416784293d03b5 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 23 Oct 2024 12:10:35 -0700 Subject: [PATCH 027/276] landing_page: Remove CSS transition from .for-education-pricing-model. If this was doing anything, it was probably just slowing down page resizes. Signed-off-by: Anders Kaseorg --- web/styles/portico/landing_page.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/styles/portico/landing_page.css b/web/styles/portico/landing_page.css index f33cf53571..3d75c18c58 100644 --- a/web/styles/portico/landing_page.css +++ b/web/styles/portico/landing_page.css @@ -2534,10 +2534,6 @@ button { text-align: left; background-color: hsl(0deg 0% 100%); - transition-property: all; - transition-duration: 0.3s; - transition-timing-function: ease; - .bottom { height: 275px; From f8f511adedb3d2de41e0072d6f737930a70227fa Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 23 Oct 2024 12:24:34 -0700 Subject: [PATCH 028/276] footer: Remove unused CSS transition for #footer li. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This doesn’t transition anything. Signed-off-by: Anders Kaseorg --- web/styles/portico/footer.css | 1 - 1 file changed, 1 deletion(-) diff --git a/web/styles/portico/footer.css b/web/styles/portico/footer.css index 0efc433503..7dcd7e48a1 100644 --- a/web/styles/portico/footer.css +++ b/web/styles/portico/footer.css @@ -77,7 +77,6 @@ line-height: 20px; color: var(--color-links); border-bottom: 1px solid var(--color-footer-background); - transition: border 0.4s ease-out; } & a, From 7878b80934f6e52765d42dace950661fb94a1717 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 23 Oct 2024 14:32:04 -0700 Subject: [PATCH 029/276] comparison_table: Remove dead CSS for th.sticky. We do not have a "sticky" class. Signed-off-by: Anders Kaseorg --- web/styles/portico/comparison_table.css | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/web/styles/portico/comparison_table.css b/web/styles/portico/comparison_table.css index de76a5bb94..50a2905f07 100644 --- a/web/styles/portico/comparison_table.css +++ b/web/styles/portico/comparison_table.css @@ -228,11 +228,6 @@ letter-spacing: 0.1ch; } - .comparison-table th.sticky { - padding: 8px 2px; - border-radius: 0; - } - .comparison-table td { padding: 8px; border-top: 1px solid hsl(209deg 40% 40% / 30%); @@ -345,8 +340,7 @@ } @media (width <= 730px) { - .comparison-table th, - .comparison-table th.sticky { + .comparison-table th { font-size: 14px; line-height: 18px; box-sizing: border-box; @@ -378,8 +372,7 @@ } @media (width <= 500px) { - .comparison-table th, - .comparison-table th.sticky { + .comparison-table th { font-size: 13px; line-height: 14px; } From f023fa6fc0739a82c918b5e7523dde91180a59f0 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 23 Oct 2024 12:41:16 -0700 Subject: [PATCH 030/276] styles: Be specific about which properties are transitioned. Signed-off-by: Anders Kaseorg --- web/styles/app_components.css | 13 ++++++---- web/styles/compose.css | 13 +++++----- web/styles/image_upload_widget.css | 2 -- web/styles/inbox.css | 3 ++- web/styles/lightbox.css | 4 ++-- web/styles/message_row.css | 13 +++++----- web/styles/modal.css | 2 +- web/styles/popovers.css | 2 +- web/styles/portico/activity.css | 4 ++-- web/styles/portico/billing.css | 3 ++- web/styles/portico/comparison_table.css | 15 ++---------- web/styles/portico/email_log.css | 2 +- web/styles/portico/footer.css | 2 +- web/styles/portico/hello.css | 24 ++++++++++--------- web/styles/portico/integrations.css | 20 +++++++++------- web/styles/portico/integrations_dev_panel.css | 4 ++-- web/styles/portico/landing_page.css | 12 ++++------ web/styles/portico/navbar.css | 12 ++++++---- web/styles/portico/portico.css | 14 ++++++----- web/styles/portico/portico_signin.css | 12 ++++++---- web/styles/rendered_markdown.css | 6 ++--- web/styles/settings.css | 8 +++---- web/styles/subscriptions.css | 6 ++--- web/styles/widgets.css | 5 ++-- web/styles/zulip.css | 8 +++---- 25 files changed, 107 insertions(+), 102 deletions(-) diff --git a/web/styles/app_components.css b/web/styles/app_components.css index fb020742ad..cb32085483 100644 --- a/web/styles/app_components.css +++ b/web/styles/app_components.css @@ -362,7 +362,8 @@ div.overlay { opacity: 0; visibility: hidden; - transition: all 0.2s ease-in; + transition: none 0.2s ease-in; + transition-property: opacity, visibility; overflow: hidden; .overlay-content { @@ -669,7 +670,8 @@ input.settings_text_input { .animated-purple-button { color: hsl(0deg 0% 100%); - transition: all 80ms linear; + transition: none 80ms linear; + transition-property: background-color, box-shadow; box-shadow: none; /* This color just passes WCAG AA */ background-color: hsl(240deg 96% 68%); @@ -716,10 +718,11 @@ input.settings_text_input { .color-animated-button-text { color: hsl(0deg 0% 100%); - transition: all 0.2s ease; + transition: color 0.2s ease; } - transition: all 0.2s ease; + transition: none 0.2s ease; + transition-property: background-color, color; } .zulip-icon, @@ -1177,7 +1180,7 @@ input.settings_text_input { color: hsl(0deg 0% 33%); border: 1px solid hsl(0deg 0% 80%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); border-radius: 4px; diff --git a/web/styles/compose.css b/web/styles/compose.css index a28a76699b..a0896f4dba 100644 --- a/web/styles/compose.css +++ b/web/styles/compose.css @@ -240,7 +240,8 @@ color: var(--color-compose-chevron-arrow); text-decoration: none; cursor: default; - transition: all 0.2s ease-in-out; + transition: none 0.2s ease-in-out; + transition-property: background, color; &.narrow_to_compose_recipients { background: var( @@ -320,7 +321,7 @@ grid-template-areas: "message-content composebox-buttons"; border-radius: 4px; border: 1px solid var(--color-message-content-container-border); - transition: border 0.2s ease; + transition: border-color 0.2s ease; &:has(.new_message_textarea:focus) { border-color: var(--color-message-content-container-border-focus); @@ -905,14 +906,14 @@ textarea.new_message_textarea { flex: 1 1 0; border: 1px solid hsl(0deg 0% 0% / 20%); border-radius: 3px; - transition: border 0.2s ease; + transition: border-color 0.2s ease; background: hsl(0deg 0% 100%); /* Give the recipient box, a `
    `, the correct styles when focus is in the #stream_message_recipient_topic `` */ &:focus-within { - border: 1px solid hsl(0deg 0% 67%); + border-color: hsl(0deg 0% 67%); } #stream_message_recipient_topic, @@ -991,7 +992,7 @@ textarea.new_message_textarea { background-color: var(--color-compose-send-button-background); &:active { - transition: all 80ms; + transition: transform 80ms; transform: scale(0.96); } @@ -1461,7 +1462,7 @@ textarea.new_message_textarea { font-weight: 450; &:active { - transition: all 80ms; + transition: transform 80ms; transform: scale(0.96); } diff --git a/web/styles/image_upload_widget.css b/web/styles/image_upload_widget.css index 3a75d37826..e09931caf7 100644 --- a/web/styles/image_upload_widget.css +++ b/web/styles/image_upload_widget.css @@ -5,8 +5,6 @@ box-shadow: 0 0 10px hsl(0deg 0% 0% / 10%); overflow: hidden; - transition: all 0.3s ease; - .image-block { background-size: contain; height: 100%; diff --git a/web/styles/inbox.css b/web/styles/inbox.css index e3862c968d..cd56924f12 100644 --- a/web/styles/inbox.css +++ b/web/styles/inbox.css @@ -79,7 +79,8 @@ font-size: 16px; color: var(--color-icons-inbox); opacity: 0.4; - transition: all 140ms; + transition: none 140ms; + transition-property: background-color, opacity, transform; padding: 5px; margin: 0; /* = -Width of the button. */ diff --git a/web/styles/lightbox.css b/web/styles/lightbox.css index e8f078bd23..446a43b3ca 100644 --- a/web/styles/lightbox.css +++ b/web/styles/lightbox.css @@ -59,7 +59,7 @@ opacity: 0; pointer-events: none; cursor: pointer; - transition: all 0.2s ease; + transition: opacity 0.2s ease; } &.show .exit { @@ -183,7 +183,7 @@ cursor: pointer; opacity: 0.5; - transition: all 0.3s ease; + transition: opacity 0.3s ease; &:hover { opacity: 1; diff --git a/web/styles/message_row.css b/web/styles/message_row.css index 4332696f45..dc74d9dbcc 100644 --- a/web/styles/message_row.css +++ b/web/styles/message_row.css @@ -428,7 +428,8 @@ .message_control_button { opacity: 0; visibility: hidden; - transition: all 0.2s ease; + transition: none 0.2s ease; + transition-property: opacity, visibility; width: var(--message-box-icon-width); height: var(--message-box-icon-height); text-align: center; @@ -527,14 +528,14 @@ .unread_marker { margin-left: var(--unread-marker-left); opacity: 0; - transition: all 0.3s ease-out; + transition: opacity 0.3s ease-out; &.slow_fade { - transition: all 2s ease-out; + transition: opacity 2s ease-out; } &.fast_fade { - transition: all 0.3s ease-out; + transition: opacity 0.3s ease-out; } &.date_unread_marker { @@ -562,7 +563,7 @@ } .unread .unread_marker { - transition: all 0.3s ease-out; + transition: opacity 0.3s ease-out; opacity: 1; } @@ -657,7 +658,7 @@ .edit-content-container { border-radius: 4px; border: 1px solid var(--color-message-content-container-border); - transition: border 0.2s ease; + transition: border-color 0.2s ease; &:has(.message_edit_content:focus) { border-color: var(--color-message-content-container-border-focus); diff --git a/web/styles/modal.css b/web/styles/modal.css index 65626713d8..f2a8043542 100644 --- a/web/styles/modal.css +++ b/web/styles/modal.css @@ -406,7 +406,7 @@ border: 1px solid hsl(0deg 0% 80%); box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; margin-bottom: 10px; width: 206px; diff --git a/web/styles/popovers.css b/web/styles/popovers.css index cf884abedf..329b6e3b92 100644 --- a/web/styles/popovers.css +++ b/web/styles/popovers.css @@ -753,7 +753,7 @@ ul.popover-group-menu-member-list { opacity: 0; pointer-events: none; - transition: all 0.3s ease; + transition: opacity 0.3s ease; &.fade.in { opacity: 1; diff --git a/web/styles/portico/activity.css b/web/styles/portico/activity.css index 933a7e28fb..b64ea94cec 100644 --- a/web/styles/portico/activity.css +++ b/web/styles/portico/activity.css @@ -145,7 +145,7 @@ tr.admin td:first-child { color: hsl(0deg 0% 33%); vertical-align: text-bottom; transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; box-shadow: inset 0 1px 1px hsla(0deg 0% 0% / 7.5%); @@ -372,7 +372,7 @@ tr.admin td:first-child { border: 1px solid hsl(0deg 0% 80%); box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; color: hsl(0deg 0% 33%); diff --git a/web/styles/portico/billing.css b/web/styles/portico/billing.css index d42cb41253..e51e501ee5 100644 --- a/web/styles/portico/billing.css +++ b/web/styles/portico/billing.css @@ -59,7 +59,8 @@ width: 120px; height: 70px; background-color: hsl(0deg 0% 94%); - transition: all 0.2s ease; + transition: none 0.2s ease; + transition-property: background-color, border-color; display: inline-block; text-align: center; cursor: pointer; diff --git a/web/styles/portico/comparison_table.css b/web/styles/portico/comparison_table.css index 50a2905f07..e6330ef150 100644 --- a/web/styles/portico/comparison_table.css +++ b/web/styles/portico/comparison_table.css @@ -130,7 +130,7 @@ align-items: center; border-radius: 3px; cursor: pointer; - transition: all 120ms ease-out; + transition: background-color 120ms ease-out; } .comparison-tab:hover { @@ -139,7 +139,6 @@ } .comparison-tab:active { - transition: all 120ms ease-out; background-color: hsl(0deg 0% 100% / 70%); } @@ -178,7 +177,7 @@ } .comparison-table tr { - transition: all 200ms ease-out; + transition: background-color 200ms ease-out; } .comparison-table tbody tr:hover { @@ -199,7 +198,6 @@ font-weight: 700; color: hsl(223deg 40% 30% / 100%); background: hsl(209deg 41% 94%); - transition: all 200ms ease-out; } .comparison-table th .icon { @@ -537,19 +535,10 @@ .comparison-table th.comparison-table-feature { padding-top: 28px; - /* Don't apply transitions to the - padding. This prevents the "Features" - label from sliding into place when - switching over to the All view. */ - transition: none; } .comparison-table td.stuck { padding-top: 24px; - /* Don't apply transitions to the - padding. This makes the stuck state - look more precise. */ - transition: none; } @media (width <= 730px) { diff --git a/web/styles/portico/email_log.css b/web/styles/portico/email_log.css index 7258f76f67..a0c8bb0b9e 100644 --- a/web/styles/portico/email_log.css +++ b/web/styles/portico/email_log.css @@ -111,7 +111,7 @@ border: 1px solid hsl(0deg 0% 80%); box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; margin-bottom: 10px; width: 206px; diff --git a/web/styles/portico/footer.css b/web/styles/portico/footer.css index 7dcd7e48a1..83443a1a38 100644 --- a/web/styles/portico/footer.css +++ b/web/styles/portico/footer.css @@ -171,7 +171,7 @@ mask-position: center; mask-repeat: no-repeat; mask-image: var(--footer-social-icon); - transition: all 150ms ease-out; + transition: background-color 150ms ease-out; &:hover { background-color: hsl(238.6deg 84.31% 90%); diff --git a/web/styles/portico/hello.css b/web/styles/portico/hello.css index 7dc0ab38ff..2f15b76a9d 100644 --- a/web/styles/portico/hello.css +++ b/web/styles/portico/hello.css @@ -165,7 +165,8 @@ ul { "pnum" on, "lnum" on; color: hsl(0deg 0% 100%); - transition: all 0.8s ease-out; + transition: none 0.8s ease-out; + transition-property: background, transform; cursor: pointer; } @@ -197,7 +198,7 @@ ul { } .client-logos-div { - transition: all 0.7s ease-out; + transition: opacity 0.7s ease-out; background-position: center; } @@ -264,7 +265,7 @@ ul { line-height: 130%; /* 33.8px */ position: relative; opacity: 0.7; - transition: all 0.1s; + transition: opacity 0.1s; } .screen-2__tabs ul li label:hover { @@ -282,7 +283,7 @@ ul { border-radius: 5px; background: hsl(0deg 0% 100%); opacity: 0.3; - transition: all 0.1s; + transition: opacity 0.1s; } .screen-2__tabs ul li label:hover::before { @@ -418,7 +419,8 @@ ul { } .quote__text { - transition: all 0.5s ease-out; + transition: none 0.5s ease-out; + transition-property: background, box-shadow; font-family: var(--font-ss3); font-style: normal; font-weight: 400; @@ -448,7 +450,7 @@ ul { bottom: -37px; left: 0; mask-image: var(--quote-tail-mask); - transition: all 0.5s ease-out; + transition: background 0.5s ease-out; background: linear-gradient( 0deg, hsl(0deg 0% 100% / 49%) 0%, @@ -525,11 +527,11 @@ ul { .quote__source a { border-bottom: 1px solid hsl(0deg 0% 100% / 50%); - transition: border 0.4s ease-out; + transition: border-bottom-color 0.4s ease-out; } .quote__source a:hover { - border-bottom: 1px solid hsl(0deg 0% 100%); + border-bottom-color: hsl(0deg 0% 100%); transition: none; } @@ -622,12 +624,12 @@ ul { .screen-4__desc a { font-weight: 550; border-bottom: 1px solid hsl(0deg 0% 100% / 50%); - transition: border 0.4s ease-out; + transition: border-bottom-color 0.4s ease-out; } .screen-2__desc a:hover, .screen-4__desc a:hover { - border-bottom: 1px solid hsl(0deg 0% 100%); + border-bottom-color: hsl(0deg 0% 100%); transition: none; } @@ -669,7 +671,7 @@ ul { border: 2px solid hsl(0deg 0% 100% / 26%); background: hsl(0deg 0% 100% / 7%); min-height: 80px; - transition: all 0.2s; + transition: background 0.2s; padding: 12px 16px 14px; .card__text { diff --git a/web/styles/portico/integrations.css b/web/styles/portico/integrations.css index 67c044604d..16dde09952 100644 --- a/web/styles/portico/integrations.css +++ b/web/styles/portico/integrations.css @@ -122,12 +122,10 @@ $category-text: hsl(219deg 23% 33%); display: block; color: hsl(0deg 0% 33%); margin-bottom: 10px; - transition: - border linear 0.2s, - box-shadow linear 0.2s; + transition: border-color linear 0.2s; &:focus { - border: 1px solid $border-green; + border-color: $border-green; outline: 0; } @@ -191,7 +189,8 @@ $category-text: hsl(219deg 23% 33%); padding: 6px 10px 3px; margin: 3px 0; border-radius: 5px; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: background-color, color; color: $category-text; &.selected, @@ -261,7 +260,8 @@ $category-text: hsl(219deg 23% 33%); border-left: 1px solid $light-blue-border; border-right: 1px solid $light-blue-border; border-bottom: 1px solid $light-blue-border; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: background-color, color; font-size: 0.9em; &.selected, @@ -317,10 +317,11 @@ $category-text: hsl(219deg 23% 33%); border: 1px solid $light-blue-border; color: $category-text; text-align: center; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: border-color; &:hover { - border: 1px solid $border-green; + border-color: $border-green; } &.legacy { @@ -507,7 +508,8 @@ $category-text: hsl(219deg 23% 33%); width: 165px; text-align: center; border-radius: 5px; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: background-color, color; background-color: $light-blue-border; color: $category-text; diff --git a/web/styles/portico/integrations_dev_panel.css b/web/styles/portico/integrations_dev_panel.css index ddff1aa909..4a82a9a51a 100644 --- a/web/styles/portico/integrations_dev_panel.css +++ b/web/styles/portico/integrations_dev_panel.css @@ -24,7 +24,7 @@ box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; &:focus { @@ -68,7 +68,7 @@ border: 1px solid hsl(0deg 0% 80%); box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; margin-bottom: 10px; diff --git a/web/styles/portico/landing_page.css b/web/styles/portico/landing_page.css index 3d75c18c58..9b07b4d117 100644 --- a/web/styles/portico/landing_page.css +++ b/web/styles/portico/landing_page.css @@ -317,7 +317,7 @@ button { box-shadow: 0 3px 10px hsl(0deg 0% 0% / 20%); - transition: all 0.2s ease; + transition: box-shadow 0.2s ease; &:hover { box-shadow: 0 3px 10px hsl(0deg 0% 100% / 20%); @@ -681,8 +681,6 @@ button { .message-feed { height: calc(100% - 20px); margin-top: 20px; - - transition: all 0.1s ease; } } @@ -910,7 +908,7 @@ button { width: 155px; height: 220px; padding: 0 5px; - transition: all 0.3s ease; + transition: border-color 0.3s ease; margin: 10px 5px; color: hsl(219deg 23% 33%); border-radius: 5px; @@ -918,7 +916,7 @@ button { } .portico-landing.hello .integrations .integration-icons .group:hover { - border: 1px solid hsl(170deg 47% 53%); + border-color: hsl(170deg 47% 53%); } .portico-landing.hello .integrations .integration-logo { @@ -1079,7 +1077,7 @@ button { font-size: 3em; text-align: center; - transition: all 0.2s ease; + transition: background 0.2s ease; } .portico-landing.apps .other-apps .apps .icon:hover { @@ -1582,7 +1580,7 @@ button { font-size: 0.9em; - transition: all 0.3s ease; + transition: background-color 0.3s ease; } .for-education-pricing-model .pricing-container .price-box:focus { diff --git a/web/styles/portico/navbar.css b/web/styles/portico/navbar.css index ef94e1f49b..91e2cfaef6 100644 --- a/web/styles/portico/navbar.css +++ b/web/styles/portico/navbar.css @@ -194,7 +194,8 @@ details summary::-webkit-details-marker { opacity: 0; height: 0; width: 100%; - transition: all 0.05s; + transition: none 0.05s; + transition-property: height, opacity; } .top-menu-tab-input-unselect:not(:checked) + .top-menu-submenu-backdrop { @@ -238,7 +239,8 @@ details summary::-webkit-details-marker { gap: 16px; opacity: 0; visibility: hidden; - transition: all 0.2s; + transition: none 0.2s; + transition-property: opacity, visibility; } #case-studies-submenu { @@ -382,7 +384,8 @@ details summary::-webkit-details-marker { "lnum" on; color: hsl(0deg 0% 100% / 80%); - transition: all 0.2; + transition: none 0.2; + transition-property: backdrop-filter, background, bottom, height; overscroll-behavior: contain; } @@ -504,7 +507,8 @@ details summary::-webkit-details-marker { position: sticky; top: 0; z-index: 1; - transition: all 0.3s; + transition: none 0.3s; + transition-property: background, backdrop-filter; height: 60px; overflow: hidden; display: flex; diff --git a/web/styles/portico/portico.css b/web/styles/portico/portico.css index 062d8b1e82..19398902d4 100644 --- a/web/styles/portico/portico.css +++ b/web/styles/portico/portico.css @@ -533,7 +533,7 @@ input.text-error { font-size: 0.6em; margin-left: 5px; opacity: 0.5; - transition: all 0.2s ease; + transition: opacity 0.2s ease; } &:hover .realm-name i.fa { @@ -684,14 +684,14 @@ input.text-error { padding: 10px; border: 1px solid hsl(0deg 0% 93%); border-radius: 4px; - transition: all 0.3s ease; + transition: border-color 0.3s ease; & a { color: inherit; } &:hover { - border: 1px solid hsl(0deg 0% 73%); + border-color: hsl(0deg 0% 73%); } .avatar { @@ -837,7 +837,8 @@ input.new-organization-button { padding: 2px 7px 1px 8px; font-weight: 600; font-size: 16px; - transition: all 0.2s ease-in; + transition: none 0.2s ease-in; + transition-property: background-color, color; border-radius: 4px; &:hover { @@ -1103,7 +1104,8 @@ input.new-organization-button { right: calc(100% - 40px); fill: hsl(0deg 0% 100%); z-index: 2; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: right, top; cursor: pointer; } @@ -1117,7 +1119,7 @@ input.new-organization-button { pointer-events: none; overflow: hidden; transform: translateX(0); - transition: all 0.3s ease; + transition: transform 0.3s ease; } .sidebar.show { diff --git a/web/styles/portico/portico_signin.css b/web/styles/portico/portico_signin.css index 2ebec58c3e..6543ee5df1 100644 --- a/web/styles/portico/portico_signin.css +++ b/web/styles/portico/portico_signin.css @@ -209,7 +209,7 @@ html { transition: color 0.3s ease, - border 0.3s ease; + border-color 0.3s ease; &:hover { color: hsl(156deg 62% 61%); @@ -428,7 +428,8 @@ html { border: none; border-radius: 4px; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: background-color, outline; &:hover { background-color: hsl(213deg 33% 31%); @@ -547,7 +548,7 @@ html { background-color: hsl(0deg 0% 100%); color: hsl(0deg 0% 33%); - transition: border 0.3s ease; + transition: border-color 0.3s ease; &:focus:invalid { box-shadow: none; @@ -555,7 +556,7 @@ html { } &:focus { - border: 1px solid hsl(0deg 0% 53%); + border-color: hsl(0deg 0% 53%); outline: 0; } } @@ -571,7 +572,8 @@ html { margin-top: 1px; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: color, font-size, font-weight, transform; } &.moving-label { diff --git a/web/styles/rendered_markdown.css b/web/styles/rendered_markdown.css index 91b26f4ca4..446df3579b 100644 --- a/web/styles/rendered_markdown.css +++ b/web/styles/rendered_markdown.css @@ -349,7 +349,7 @@ float: right; width: 15px; cursor: pointer; - transition: 0.4s ease; + transition: transform 0.4s ease; transform: rotate(45deg); &::before, @@ -360,7 +360,7 @@ width: 12px; height: 3px; background-color: hsl(0deg 0% 83%); - transition: 0.4s ease; + transition: transform 0.4s ease; } &::after { @@ -917,7 +917,7 @@ &::-webkit-scrollbar-thumb { background-color: hsl(0deg 0% 0% / 30%); border-radius: 20px; - transition: all 0.2s ease; + transition: background-color 0.2s ease; } &::-webkit-scrollbar-thumb:hover { diff --git a/web/styles/settings.css b/web/styles/settings.css index b659e7938c..df3d8a01f9 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -868,7 +868,7 @@ input[type="checkbox"] { position: relative; margin-left: 5px; color: hsl(0deg 0% 67%); - transition: all 0.3s ease; + transition: color 0.3s ease; &:hover { color: hsl(0deg 0% 27%); @@ -1494,7 +1494,7 @@ $option_title_width: 180px; color: hsl(0deg 0% 33%); border: 1px solid hsl(0deg 0% 80%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; &:focus { @@ -1709,7 +1709,7 @@ $option_title_width: 180px; box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; &:focus { @@ -2025,7 +2025,7 @@ $option_title_width: 180px; border: 1px solid hsl(0deg 0% 80%); box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; margin-bottom: 10px; diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 521bb049c6..15cb0f7edb 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -286,7 +286,8 @@ h4.user_group_setting_subsection_title { cursor: pointer; - transition: all 0.3s ease; + transition: none 0.3s ease; + transition-property: opacity, transform; } .user-groups-container .user-groups-header.slide-left .fa-chevron-left, @@ -328,7 +329,6 @@ h4.user_group_setting_subsection_title { .user-groups-title, .subscriptions-title { display: inline-block; - transition: all 0.3s ease; transform: translate(-13px, 0); } @@ -1229,7 +1229,7 @@ div.settings-radio-input-parent { background-color: var(--color-background-modal); border-top: none; - transition: all 0.3s ease; + transition: left 0.3s ease; z-index: 10; &.show { diff --git a/web/styles/widgets.css b/web/styles/widgets.css index 3c579cceba..638984a15c 100644 --- a/web/styles/widgets.css +++ b/web/styles/widgets.css @@ -130,7 +130,7 @@ border: 1px solid hsl(0deg 0% 80%); box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; border-radius: 4px; color: hsl(0deg 0% 33%); @@ -218,7 +218,8 @@ button { padding: 4px; padding-left: 14px; padding-right: 14px; - transition: all 0.2s ease; + transition: none 0.2s ease; + transition-property: background-color, border-color, color; &:hover, &:focus { diff --git a/web/styles/zulip.css b/web/styles/zulip.css index f0809a58bb..d25201ba15 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -438,7 +438,7 @@ body.has-overlay-scrollbar { text-overflow: ellipsis; /* Button curvature and transitions. */ border-radius: 4px; - transition: all 100ms ease-out; + transition: transform 100ms ease-out; &:hover, &:focus { @@ -773,7 +773,7 @@ strong { .new_messages, .new_messages_fadeout { - transition: all 3s ease-in-out; + transition: background-color 3s ease-in-out; } .messagebox-content .slow-send-spinner { @@ -918,7 +918,7 @@ div.focused-message-list { border-radius: 4px; border: 1px solid hsl(0deg 0% 80%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; &:focus { @@ -1295,7 +1295,7 @@ div.toggle_resolve_topic_spinner .loading_indicator_spinner { border: 1px solid hsl(0deg 0% 80%); box-shadow: inset 0 1px 1px hsl(0deg 0% 0% / 7.5%); transition: - border linear 0.2s, + border-color linear 0.2s, box-shadow linear 0.2s; &:focus { From 2671a5c32c13a76f54f9c3cd4a86cb91faa45060 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 23 Oct 2024 12:41:57 -0700 Subject: [PATCH 031/276] stylelint: Enable stylelint-high-performance-animation. Signed-off-by: Anders Kaseorg --- package.json | 1 + pnpm-lock.yaml | 13 +++++++++++++ stylelint.config.js | 2 ++ version.py | 2 +- web/styles/components.css | 8 ++++---- web/styles/compose.css | 2 +- web/styles/portico/comparison_table.css | 2 +- web/styles/portico/navbar.css | 6 +++--- web/styles/portico/portico.css | 2 +- web/styles/portico/portico_signin.css | 2 +- web/styles/portico/pricing_plans.css | 2 +- web/styles/progress_bar.css | 2 +- web/styles/reactions.css | 2 +- web/styles/rendered_markdown.css | 6 ++++-- web/styles/subscriptions.css | 2 +- 15 files changed, 36 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 8357f621b7..771066b128 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "stackframe": "^1.3.4", "stacktrace-gps": "^3.0.4", "style-loader": "^4.0.0", + "stylelint-high-performance-animation": "^1.10.0", "text-field-edit": "^4.0.0", "textarea-caret": "^3.1.0", "tippy.js": "^6.3.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45ebb456bc..76288eb64a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,6 +253,9 @@ importers: style-loader: specifier: ^4.0.0 version: 4.0.0(webpack@5.95.0) + stylelint-high-performance-animation: + specifier: ^1.10.0 + version: 1.10.0(stylelint@16.10.0(typescript@5.6.3)) text-field-edit: specifier: ^4.0.0 version: 4.1.1 @@ -8051,6 +8054,11 @@ packages: peerDependencies: stylelint: ^16.1.0 + stylelint-high-performance-animation@1.10.0: + resolution: {integrity: sha512-YzNI+E6taN8pwgaM0INazRg4tw23VA17KNMKUVdOeohpnpSyJLBnLVT9NkRcaCFLodK/67smS5VZK+Qe4Ohrvw==} + peerDependencies: + stylelint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + stylelint@16.10.0: resolution: {integrity: sha512-z/8X2rZ52dt2c0stVwI9QL2AFJhLhbPkyfpDFcizs200V/g7v+UYY6SNcB9hKOLcDDX/yGLDsY/pX08sLkz9xQ==} engines: {node: '>=18.12.0'} @@ -18380,6 +18388,11 @@ snapshots: stylelint: 16.10.0(typescript@5.6.3) stylelint-config-recommended: 14.0.1(stylelint@16.10.0(typescript@5.6.3)) + stylelint-high-performance-animation@1.10.0(stylelint@16.10.0(typescript@5.6.3)): + dependencies: + postcss-value-parser: 4.2.0 + stylelint: 16.10.0(typescript@5.6.3) + stylelint@16.10.0(typescript@5.6.3): dependencies: '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) diff --git a/stylelint.config.js b/stylelint.config.js index d922b9cd22..2a677539f0 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -2,6 +2,7 @@ module.exports = { extends: ["stylelint-config-standard"], + plugins: ["stylelint-high-performance-animation"], rules: { // Add some exceptions for recommended rules "at-rule-no-unknown": [true, {ignoreAtRules: ["extend"]}], @@ -37,6 +38,7 @@ module.exports = { // We use hsl instead of rgb "rgb", ], + "plugin/no-low-performance-animation-properties": [true, {ignore: "paint-properties"}], // Zulip CSS should have no dependencies on external resources "function-url-no-scheme-relative": true, diff --git a/version.py b/version.py index d0ee0f4da6..09d0bccc65 100644 --- a/version.py +++ b/version.py @@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = (295, 0) # bumped 2024-10-20 to upgrade Python requirements +PROVISION_VERSION = (295, 1) # bumped 2024-10-23 for stylelint-high-performance-animation diff --git a/web/styles/components.css b/web/styles/components.css index 5bb03435ea..5973919471 100644 --- a/web/styles/components.css +++ b/web/styles/components.css @@ -79,20 +79,20 @@ a.no-underline:hover { } &.simplebar-vertical { - transition: width 0.2s ease 1s; + transition: width 0.2s ease 1s; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ &.simplebar-hover { width: 15px; - transition: width 0.2s ease; + transition: width 0.2s ease; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ } } &.simplebar-horizontal { - transition: height 0.2s ease 1s; + transition: height 0.2s ease 1s; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ &.simplebar-hover { height: 15px; - transition: height 0.2s ease; + transition: height 0.2s ease; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ } } } diff --git a/web/styles/compose.css b/web/styles/compose.css index a0896f4dba..0747b0a388 100644 --- a/web/styles/compose.css +++ b/web/styles/compose.css @@ -840,7 +840,7 @@ width: 0; /* The progress updates seem to come every second or so, so this is the smoothest it can probably get. */ - transition: width 1s ease-in-out; + transition: width 1s ease-in-out; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ background: hsl(204deg 63% 85%); top: 0; bottom: 0; diff --git a/web/styles/portico/comparison_table.css b/web/styles/portico/comparison_table.css index e6330ef150..8e56c71f33 100644 --- a/web/styles/portico/comparison_table.css +++ b/web/styles/portico/comparison_table.css @@ -95,7 +95,7 @@ color: inherit; text-decoration: none; border-bottom: 1px solid hsl(0deg 0% 100% / 50%) !important; - transition: border 0.4s ease-out; + transition: border 0.4s ease-out; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ &:hover { border-bottom: 2px solid hsl(0deg 0% 100%) !important; diff --git a/web/styles/portico/navbar.css b/web/styles/portico/navbar.css index 91e2cfaef6..27830de18c 100644 --- a/web/styles/portico/navbar.css +++ b/web/styles/portico/navbar.css @@ -195,7 +195,7 @@ details summary::-webkit-details-marker { height: 0; width: 100%; transition: none 0.05s; - transition-property: height, opacity; + transition-property: height, opacity; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ } .top-menu-tab-input-unselect:not(:checked) + .top-menu-submenu-backdrop { @@ -385,7 +385,7 @@ details summary::-webkit-details-marker { color: hsl(0deg 0% 100% / 80%); transition: none 0.2; - transition-property: backdrop-filter, background, bottom, height; + transition-property: backdrop-filter, background, bottom, height; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ overscroll-behavior: contain; } @@ -578,7 +578,7 @@ details summary::-webkit-details-marker { background-position: right; transition: background, - letter-spacing 0.2s; + letter-spacing 0.2s; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ } .top-menu-mobile-summary:active::after { diff --git a/web/styles/portico/portico.css b/web/styles/portico/portico.css index 19398902d4..fa981e8406 100644 --- a/web/styles/portico/portico.css +++ b/web/styles/portico/portico.css @@ -1105,7 +1105,7 @@ input.new-organization-button { fill: hsl(0deg 0% 100%); z-index: 2; transition: none 0.3s ease; - transition-property: right, top; + transition-property: right, top; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ cursor: pointer; } diff --git a/web/styles/portico/portico_signin.css b/web/styles/portico/portico_signin.css index 6543ee5df1..b2478dc3fd 100644 --- a/web/styles/portico/portico_signin.css +++ b/web/styles/portico/portico_signin.css @@ -573,7 +573,7 @@ html { margin-top: 1px; transition: none 0.3s ease; - transition-property: color, font-size, font-weight, transform; + transition-property: color, font-size, font-weight, transform; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ } &.moving-label { diff --git a/web/styles/portico/pricing_plans.css b/web/styles/portico/pricing_plans.css index 516a8a6d74..145f397a5e 100644 --- a/web/styles/portico/pricing_plans.css +++ b/web/styles/portico/pricing_plans.css @@ -84,7 +84,7 @@ color: inherit; text-decoration: none; border-bottom: 1px solid hsl(0deg 0% 100% / 50%); - transition: border 0.4s ease-out; + transition: border 0.4s ease-out; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ &:hover { text-decoration: none; diff --git a/web/styles/progress_bar.css b/web/styles/progress_bar.css index 5073daf031..fa174b0047 100644 --- a/web/styles/progress_bar.css +++ b/web/styles/progress_bar.css @@ -31,7 +31,7 @@ background-repeat: repeat-x; box-shadow: inset 0 -1px 2px hsl(0deg 0% 0% / 15%); box-sizing: border-box; - transition: width 0.6s ease; + transition: width 0.6s ease; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ } .progress.active .bar { diff --git a/web/styles/reactions.css b/web/styles/reactions.css index 55b02cb072..9cdf169cf3 100644 --- a/web/styles/reactions.css +++ b/web/styles/reactions.css @@ -22,7 +22,7 @@ box-shadow: inset 0 0 5px 0 var(--color-message-reaction-shadow-inner); transition: transform 100ms linear, - font-weight 100ms linear; + font-weight 100ms linear; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ &.reacted { color: var(--color-message-reaction-text-reacted); diff --git a/web/styles/rendered_markdown.css b/web/styles/rendered_markdown.css index 446df3579b..7c6bd5f5dc 100644 --- a/web/styles/rendered_markdown.css +++ b/web/styles/rendered_markdown.css @@ -316,18 +316,20 @@ overflow: hidden; border-top: hsl(0deg 0% 50%) 0 solid; transition: + /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ height 0.4s ease-in-out, border-top 0.4s step-end, - padding 0.4s step-end; + padding 0.4s step-end; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ padding: 0; height: 0; &.spoiler-content-open { border-top: hsl(0deg 0% 50%) 1px solid; transition: + /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ height 0.4s ease-in-out, border-top 0.4s step-start, - padding 0.4s step-start; + padding 0.4s step-start; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ padding: 5px; height: auto; } diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 15cb0f7edb..1642862f30 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -1229,7 +1229,7 @@ div.settings-radio-input-parent { background-color: var(--color-background-modal); border-top: none; - transition: left 0.3s ease; + transition: left 0.3s ease; /* stylelint-disable-line plugin/no-low-performance-animation-properties */ z-index: 10; &.show { From c1fc8e633127d87e564ced243332e016f6b7a18c Mon Sep 17 00:00:00 2001 From: Aditya Chaudhary <113302312+userAdityaa@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:32:47 +0530 Subject: [PATCH 032/276] channel_feed: Configure click event for channel name. This commit configures the click event on the channel name so that when the user is narrowed to a topic within a channel, clicking the same channel will navigate the user to the general channel feed. This improves the user experience by allowing easy access to the full channel feed when a user is focused on a specific topic. Fixes #32032. --- web/src/stream_list.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/src/stream_list.ts b/web/src/stream_list.ts index a4edfa063c..3e653cc038 100644 --- a/web/src/stream_list.ts +++ b/web/src/stream_list.ts @@ -846,6 +846,14 @@ export function set_event_handlers({ e.stopPropagation(); const stream_id = stream_id_for_elt($(e.target).parents("li")); + const current_narrow_stream_id = narrow_state.stream_id(); + const current_topic = narrow_state.topic(); + + if (current_narrow_stream_id === stream_id && current_topic) { + const channel_feed_url = hash_util.by_stream_url(stream_id); + browser_history.go_to_location(channel_feed_url); + return; + } if ( user_settings.web_channel_default_view === From ac652ffab636d0743f0c889734852a9f23807a2a Mon Sep 17 00:00:00 2001 From: Aditya Chaudhary <113302312+userAdityaa@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:36:21 +0530 Subject: [PATCH 033/276] stream_settings: Place the cursor automatically in channel name. When the user opens channel create settings from left sidebar, now the focus is set on the channel name input. The logic was already there, but not properly placed inside the overlay code path. Fixes #32034. --- web/src/stream_settings_ui.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/web/src/stream_settings_ui.js b/web/src/stream_settings_ui.js index 7ee0f08f44..39b8b3d739 100644 --- a/web/src/stream_settings_ui.js +++ b/web/src/stream_settings_ui.js @@ -818,14 +818,16 @@ export function launch(section, left_side_tab, right_side_tab) { }, }); change_state(section, left_side_tab, right_side_tab); + setTimeout(() => { + if (!stream_settings_components.get_active_data().id) { + if (section === "new") { + $("#create_stream_name").trigger("focus"); + } else { + $("#search_stream_name").trigger("focus"); + } + } + }, 0); }); - if (!stream_settings_components.get_active_data().id) { - if (section === "new") { - $("#create_stream_name").trigger("focus"); - } else { - $("#search_stream_name").trigger("focus"); - } - } } export function switch_rows(event) { From a601715e37f776c53983b5c8540b315c8376dbb4 Mon Sep 17 00:00:00 2001 From: klarabratteby <93648999+klarabratteby@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:45:37 +0200 Subject: [PATCH 034/276] user_topic_popover: Remove all occurrences of instance.context. --- web/src/user_topic_popover.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/web/src/user_topic_popover.js b/web/src/user_topic_popover.js index bd608834f1..2317157691 100644 --- a/web/src/user_topic_popover.js +++ b/web/src/user_topic_popover.js @@ -32,18 +32,22 @@ export function initialize() { const topic_name = $elt.attr("data-topic-name"); $elt.addClass("visibility-policy-popover-visible"); - instance.context = - popover_menus_data.get_change_visibility_policy_popover_content_context( - Number.parseInt(stream_id, 10), - topic_name, - ); instance.setContent( - parse_html(render_change_visibility_policy_popover(instance.context)), + parse_html( + render_change_visibility_policy_popover( + popover_menus_data.get_change_visibility_policy_popover_content_context( + Number.parseInt(stream_id, 10), + topic_name, + ), + ), + ), ); }, onMount(instance) { const $popper = $(instance.popper); - const {stream_id, topic_name} = instance.context; + const $elt = $(instance.reference).closest(".change_visibility_policy").expectOne(); + const stream_id = Number.parseInt($elt.attr("data-stream-id"), 10); + const topic_name = $elt.attr("data-topic-name"); if (!stream_id) { popover_menus.hide_current_popover_if_visible(instance); From ab03c74314aecc26888a5a348ad338f387a251f8 Mon Sep 17 00:00:00 2001 From: klarabratteby Date: Tue, 22 Oct 2024 13:10:32 +0000 Subject: [PATCH 035/276] user_topic_popover: Convert module to TypeScript. --- tools/test-js-with-node | 2 +- ...topic_popover.js => user_topic_popover.ts} | 30 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) rename web/src/{user_topic_popover.js => user_topic_popover.ts} (83%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 61fb25a7e3..62e0bea6a4 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -292,7 +292,7 @@ EXEMPT_FILES = make_set( "web/src/user_sort.ts", "web/src/user_status.ts", "web/src/user_status_ui.ts", - "web/src/user_topic_popover.js", + "web/src/user_topic_popover.ts", "web/src/user_topics.ts", "web/src/user_topics_ui.ts", "web/src/views_util.ts", diff --git a/web/src/user_topic_popover.js b/web/src/user_topic_popover.ts similarity index 83% rename from web/src/user_topic_popover.js rename to web/src/user_topic_popover.ts index 2317157691..80a1155fe3 100644 --- a/web/src/user_topic_popover.js +++ b/web/src/user_topic_popover.ts @@ -1,4 +1,5 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; import render_change_visibility_policy_popover from "../templates/popovers/change_visibility_policy_popover.hbs"; @@ -8,7 +9,7 @@ import {parse_html} from "./ui_util"; import * as user_topics from "./user_topics"; import * as util from "./util"; -export function initialize() { +export function initialize(): void { popover_menus.register_popover_menu(".change_visibility_policy", { theme: "popover-menu", placement: "bottom", @@ -28,15 +29,18 @@ export function initialize() { popover_menus.popover_instances.change_visibility_policy = instance; popover_menus.on_show_prep(instance); const $elt = $(instance.reference).closest(".change_visibility_policy").expectOne(); - const stream_id = $elt.attr("data-stream-id"); - const topic_name = $elt.attr("data-topic-name"); + const stream_id_str = $elt.attr("data-stream-id"); $elt.addClass("visibility-policy-popover-visible"); + assert(stream_id_str !== undefined); + + const stream_id = Number.parseInt(stream_id_str, 10); + const topic_name = $elt.attr("data-topic-name")!; instance.setContent( parse_html( render_change_visibility_policy_popover( popover_menus_data.get_change_visibility_policy_popover_content_context( - Number.parseInt(stream_id, 10), + stream_id, topic_name, ), ), @@ -46,8 +50,11 @@ export function initialize() { onMount(instance) { const $popper = $(instance.popper); const $elt = $(instance.reference).closest(".change_visibility_policy").expectOne(); - const stream_id = Number.parseInt($elt.attr("data-stream-id"), 10); - const topic_name = $elt.attr("data-topic-name"); + const stream_id_str = $elt.attr("data-stream-id"); + assert(stream_id_str !== undefined); + + const stream_id = Number.parseInt(stream_id_str, 10); + const topic_name = $elt.attr("data-topic-name")!; if (!stream_id) { popover_menus.hide_current_popover_if_visible(instance); @@ -58,11 +65,11 @@ export function initialize() { const start_time = Date.now(); const visibility_policy = Number.parseInt( - $(e.currentTarget).attr("data-visibility-policy"), + $(e.currentTarget).attr("data-visibility-policy")!, 10, ); - const success_cb = () => { + const success_cb = (): void => { setTimeout( () => { popover_menus.hide_current_popover_if_visible(instance); @@ -71,7 +78,8 @@ export function initialize() { ); }; - const error_cb = () => { + const error_cb = (): void => { + assert(stream_id !== undefined); const prev_visibility_policy = user_topics.get_topic_visibility_policy( stream_id, topic_name, @@ -86,7 +94,7 @@ export function initialize() { util.get_remaining_time(start_time, 500), ); }; - + assert(stream_id !== undefined); user_topics.set_user_topic_visibility_policy( stream_id, topic_name, @@ -105,7 +113,7 @@ export function initialize() { .expectOne() .removeClass("visibility-policy-popover-visible"); instance.destroy(); - popover_menus.popover_instances.change_visibility_policy = undefined; + popover_menus.popover_instances.change_visibility_policy = null; // If the reference is in recent view / inbox, we would ideally restore focus // to the reference icon here but we don't do that because there are a lot of From 3e13914f4af42dfc213f479cca7311855f425891 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Thu, 24 Oct 2024 16:19:34 +0530 Subject: [PATCH 036/276] attachments_ui: Remove unused 'id' from 'Delete file' dialog widget. The 'id' field in a dialog widget is used to add custom id to the container element to modify styles. We were not using this id anywhere, so this commit removes it. --- web/src/attachments_ui.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/attachments_ui.ts b/web/src/attachments_ui.ts index a56044c32c..7d0ef31877 100644 --- a/web/src/attachments_ui.ts +++ b/web/src/attachments_ui.ts @@ -92,7 +92,6 @@ function delete_attachments(attachment: string, file_name: string): void { html_heading: $t_html({defaultMessage: "Delete file?"}), html_body, html_submit_button: $t_html({defaultMessage: "Delete"}), - id: "confirm_delete_file_modal", focus_submit_on_open: true, on_click() { dialog_widget.submit_api_request(channel.del, "/json/attachments/" + attachment, {}); From fd66c08221b8d1a8854709b2ca3d6fa5d229d332 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Thu, 24 Oct 2024 16:46:00 +0530 Subject: [PATCH 037/276] onboarding_steps: Fix blueslip error message. This commit fixes the blueslip error message displayed when marking an onboarding step as read fails. --- web/src/onboarding_steps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/onboarding_steps.ts b/web/src/onboarding_steps.ts index 715e3301ec..6548f03a0f 100644 --- a/web/src/onboarding_steps.ts +++ b/web/src/onboarding_steps.ts @@ -15,7 +15,7 @@ export function post_onboarding_step_as_read(onboarding_step_name: string): void data: {onboarding_step: onboarding_step_name}, error(err) { if (err.readyState !== 0) { - blueslip.error("Failed to fetch onboarding steps", { + blueslip.error(`Failed to mark ${onboarding_step_name} as read.`, { readyState: err.readyState, status: err.status, body: err.responseText, From 30ada20ececb0e13b5c417b7c5bbed8914751668 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Thu, 24 Oct 2024 17:03:00 +0530 Subject: [PATCH 038/276] dialog_widget: Fix stale comment. The 'settings_user_groups' file which the comment refers to no longer exists. This commit updates the comment. --- web/src/dialog_widget.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/dialog_widget.ts b/web/src/dialog_widget.ts index 2b2afb46da..db81a4e1ec 100644 --- a/web/src/dialog_widget.ts +++ b/web/src/dialog_widget.ts @@ -25,9 +25,9 @@ function current_dialog_widget_selector(): string { } /* - * Look for confirm_dialog in settings_user_groups - * to see an example of how to use this widget. It's - * pretty simple to use! + * Look for dialog_widget or confirm_dialog in various + * 'web/src/' files to see examples of how to use this widget. + * It's pretty simple to use! * * Some things to note: * 1) We create DOM on the fly, and we remove From af9b44ed024b71db68db5065f3ba8b3484480aed Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Wed, 23 Oct 2024 14:02:26 +0200 Subject: [PATCH 039/276] auth: Fix invalid credentials message in login form. Email is not case-sensitive. And password is obviously case-sensitive, so no point mentioning that. --- zerver/forms.py | 5 ++--- zerver/tests/test_signup.py | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/zerver/forms.py b/zerver/forms.py index f25338d9df..f1f37b5043 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -52,6 +52,7 @@ MIT_VALIDATION_ERROR = Markup( ' contact us.' ) +INVALID_ACCOUNT_CREDENTIALS_ERROR = gettext_lazy("Please enter a correct email and password.") DEACTIVATED_ACCOUNT_ERROR = gettext_lazy( "Your account {username} has been deactivated." " Please contact your organization administrator to reactivate it." @@ -514,9 +515,7 @@ class OurAuthenticationForm(AuthenticationForm): if self.user_cache is None: raise forms.ValidationError( - self.error_messages["invalid_login"], - code="invalid_login", - params={"username": self.username_field.verbose_name}, + INVALID_ACCOUNT_CREDENTIALS_ERROR, ) self.confirm_login_allowed(self.user_cache) diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 5fd42628ae..80569e5d6d 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -993,7 +993,7 @@ class LoginTest(ZulipTestCase): def test_login_nonexistent_user(self) -> None: result = self.login_with_return("xxx@zulip.com", "xxx") self.assertEqual(result.status_code, 200) - self.assert_in_response("Please enter a correct email and password", result) + self.assert_in_response("Please enter a correct email and password.", result) self.assert_logged_in_user_id(None) def test_login_wrong_subdomain(self) -> None: @@ -1009,9 +1009,7 @@ class LoginTest(ZulipTestCase): ], ) self.assertEqual(result.status_code, 200) - expected_error = ( - "Please enter a correct email and password. Note that both fields may be case-sensitive" - ) + expected_error = "Please enter a correct email and password." self.assert_in_response(expected_error, result) self.assert_logged_in_user_id(None) From f6db00bfb590c1836f99eb91906ac60d11e27732 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 23 Oct 2024 08:47:33 +0530 Subject: [PATCH 040/276] user_group_popover: Show members count in popover. This count includes direct members as well as members of all the recursive subgroups. --- web/src/user_group_popover.ts | 1 + web/templates/popovers/user_group_info_popover.hbs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/web/src/user_group_popover.ts b/web/src/user_group_popover.ts index bfa0113ff0..adb0ea722e 100644 --- a/web/src/user_group_popover.ts +++ b/web/src/user_group_popover.ts @@ -102,6 +102,7 @@ export function toggle_user_group_info_popover( is_guest: current_user.is_guest, is_system_group: group.is_system_group, deactivated: group.deactivated, + members_count: user_groups.get_recursive_group_members(group).size, }; instance.setContent(ui_util.parse_html(render_user_group_info_popover(args))); }, diff --git a/web/templates/popovers/user_group_info_popover.hbs b/web/templates/popovers/user_group_info_popover.hbs index 105731eb46..ec74e115aa 100644 --- a/web/templates/popovers/user_group_info_popover.hbs +++ b/web/templates/popovers/user_group_info_popover.hbs @@ -9,6 +9,13 @@
    {{group_description}}
    + {{#if members_count}} +
  1. + {{#tr}} + {members_count, plural, =1 {1 member.} other {# members.}} + {{/tr}} +
  2. + {{/if}} {{#if deactivated}}
  3. {{t "This group has been deactivated." }} From 7fe927c61b4a2ce0792dd8233bfd82defc6208e5 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 23 Oct 2024 12:30:59 +0530 Subject: [PATCH 041/276] user_group: Show subgroups in popover. Previously, all members of the group, including members of recursive groups, were shown in the the popover. Now only direct members are shown along with the direct subgroups of the group. Fixes #32088. --- web/src/user_group_popover.ts | 8 +++++--- web/src/user_groups.ts | 11 +++++++++++ web/styles/popovers.css | 6 ++++++ web/templates/popovers/user_group_info_popover.hbs | 8 +++++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/web/src/user_group_popover.ts b/web/src/user_group_popover.ts index adb0ea722e..9580d0ec83 100644 --- a/web/src/user_group_popover.ts +++ b/web/src/user_group_popover.ts @@ -14,6 +14,7 @@ import * as popover_menus from "./popover_menus"; import * as rows from "./rows"; import {current_user} from "./state_data"; import * as ui_util from "./ui_util"; +import * as user_group_components from "./user_group_components"; import * as user_groups from "./user_groups"; import * as util from "./util"; @@ -95,9 +96,10 @@ export function toggle_user_group_info_popover( const args = { group_name: user_groups.get_display_group_name(group.name), group_description: group.description, - members: sort_group_members( - fetch_group_members([...user_groups.get_recursive_group_members(group)]), - ), + members: sort_group_members(fetch_group_members([...group.members])), + subgroups: user_groups + .get_direct_subgroups_of_group(group) + .sort(user_group_components.sort_group_member_name), group_edit_url: hash_util.group_edit_url(group, "general"), is_guest: current_user.is_guest, is_system_group: group.is_system_group, diff --git a/web/src/user_groups.ts b/web/src/user_groups.ts index d31e700e93..5e58b3b10f 100644 --- a/web/src/user_groups.ts +++ b/web/src/user_groups.ts @@ -335,6 +335,17 @@ export function get_potential_subgroups(target_user_group_id: number): UserGroup }); } +export function get_direct_subgroups_of_group(target_user_group: UserGroup): UserGroup[] { + const direct_subgroups = []; + const subgroup_ids = target_user_group.direct_subgroup_ids; + for (const subgroup_id of subgroup_ids) { + const subgroup = user_group_by_id_dict.get(subgroup_id); + assert(subgroup !== undefined); + direct_subgroups.push(subgroup); + } + return direct_subgroups; +} + export function is_user_in_group( user_group_id: number, user_id: number, diff --git a/web/styles/popovers.css b/web/styles/popovers.css index 329b6e3b92..91268c35fe 100644 --- a/web/styles/popovers.css +++ b/web/styles/popovers.css @@ -346,6 +346,12 @@ ul.popover-group-menu-member-list { display: flex; align-items: center; padding: 0 10px; + + .zulip-icon-triple-users { + color: var(--color-icon-purple); + padding-left: 3px; + padding-right: 3px; + } } .popover-group-menu-member-name { diff --git a/web/templates/popovers/user_group_info_popover.hbs b/web/templates/popovers/user_group_info_popover.hbs index ec74e115aa..35d7ea161d 100644 --- a/web/templates/popovers/user_group_info_popover.hbs +++ b/web/templates/popovers/user_group_info_popover.hbs @@ -23,8 +23,14 @@ {{/if}}
  4. - {{#if members.length}} + {{#if (or members.length subgroups.length)}}
      + {{#each subgroups}} +
    • + + {{name}} +
    • + {{/each}} {{#each members}}
    • {{#if is_bot}} From b593f6a881e56f463e7da52353ecd8ee85479f83 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 23 Oct 2024 12:40:04 +0530 Subject: [PATCH 042/276] user_group_popover: Fix icon alignment. This commit fixes alignment of icons for the group members and subgroups list shown in the popover. --- web/styles/popovers.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/styles/popovers.css b/web/styles/popovers.css index 91268c35fe..63bb2670cf 100644 --- a/web/styles/popovers.css +++ b/web/styles/popovers.css @@ -349,6 +349,9 @@ ul.popover-group-menu-member-list { .zulip-icon-triple-users { color: var(--color-icon-purple); + } + + .zulip-icon { padding-left: 3px; padding-right: 3px; } From a6b03852292d449d79b5682bf4e12abd830dcfc9 Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Thu, 24 Oct 2024 00:35:02 +0200 Subject: [PATCH 043/276] tests: Extract upload_image helpers from test_markdown_thumbnail. These are pretty general and can be useful utils for other tests. --- zerver/lib/test_classes.py | 15 +++++++++++++++ zerver/tests/test_markdown_thumbnail.py | 19 +++++-------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/zerver/lib/test_classes.py b/zerver/lib/test_classes.py index b5eb1deec4..fe935b2082 100644 --- a/zerver/lib/test_classes.py +++ b/zerver/lib/test_classes.py @@ -72,6 +72,7 @@ from zerver.lib.test_console_output import ( from zerver.lib.test_helpers import ( cache_tries_captured, find_key_by_email, + get_test_image_file, instrument_url, queries_captured, ) @@ -2199,6 +2200,20 @@ class ZulipTestCase(ZulipTestCaseMixin, TestCase): ) return message_id + def upload_image(self, image_name: str) -> str: + with get_test_image_file(image_name) as image_file: + response = self.assert_json_success( + self.client_post("/json/user_uploads", {"file": image_file}) + ) + return re.sub(r"/user_uploads/", "", response["url"]) + + def upload_and_thumbnail_image(self, image_name: str) -> str: + with self.captureOnCommitCallbacks(execute=True): + # Running captureOnCommitCallbacks includes inserting into + # the Rabbitmq queue, which in testing means we + # immediately run the worker for it, producing the thumbnails. + return self.upload_image(image_name) + def get_row_ids_in_all_tables() -> Iterator[tuple[str, set[int]]]: all_models = apps.get_models(include_auto_created=True) diff --git a/zerver/tests/test_markdown_thumbnail.py b/zerver/tests/test_markdown_thumbnail.py index 07ac40d598..c2290a3028 100644 --- a/zerver/tests/test_markdown_thumbnail.py +++ b/zerver/tests/test_markdown_thumbnail.py @@ -2,6 +2,7 @@ import re from unittest.mock import patch import pyvips +from typing_extensions import override from zerver.actions.message_delete import do_delete_messages from zerver.actions.message_send import check_message, do_send_messages @@ -9,7 +10,7 @@ from zerver.lib.addressee import Addressee from zerver.lib.camo import get_camo_url from zerver.lib.markdown import render_message_markdown from zerver.lib.test_classes import ZulipTestCase -from zerver.lib.test_helpers import get_test_image_file, read_test_image_file +from zerver.lib.test_helpers import read_test_image_file from zerver.lib.thumbnail import ThumbnailFormat from zerver.lib.upload import upload_message_attachment from zerver.models import ( @@ -25,20 +26,10 @@ from zerver.worker.thumbnail import ensure_thumbnails class MarkdownThumbnailTest(ZulipTestCase): - def upload_image(self, image_name: str) -> str: + @override + def setUp(self) -> None: self.login("othello") - with get_test_image_file(image_name) as image_file: - response = self.assert_json_success( - self.client_post("/json/user_uploads", {"file": image_file}) - ) - return re.sub(r"/user_uploads/", "", response["url"]) - - def upload_and_thumbnail_image(self, image_name: str) -> str: - with self.captureOnCommitCallbacks(execute=True): - # Running captureOnCommitCallbacks includes inserting into - # the Rabbitmq queue, which in testing means we - # immediately run the worker for it, producing the thumbnails. - return self.upload_image(image_name) + super().setUp() def assert_message_content_is( self, message_id: int, rendered_content: str, user_name: str = "othello" From da4443f392cc8aa9e6879d905cb1ccd50b66127b Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Thu, 17 Oct 2024 21:20:49 +0200 Subject: [PATCH 044/276] thumbnail: Make thumbnailing work with data import. We didn't have thumbnailing for images coming from data import and this commit adds the functionality. There are a few fundamental issues that the implementation needs to solve. 1. The images come from an untrusted source and therefore we don't want to just pass them through to thumbnailing without checking. For that reason, we cannot just import ImageAttachment rows from the export data, even for zulip=>zulip imports. The right way to process images is to pass them to maybe_thumbail(), which runs libvips_check_image() on them to verify we're okay with thumbnailing, creates ImageAttachment rows for them and sends them to the thumbnailing queue worker. This approach lets us handle both zulip=>zulip and 3rd party=>zulip imports in the same way, 2. There is a somewhat circular dependency between the Message, Attachment and ImageAttachment import process: - ImageAttachments would ideally be created after importing Attachments, but they need to already exist at the time of Message import. Otherwise, the markdown processor doesn't know it has to add HTML for image previews to messages that reference images. This would mean that messages imported from 3rd party tools don't get image previews. - Attachments only get created after Message import however, due to the many-to-many relationship between Message and Attachment. This is solved by fixing up some data of Attachments pre-emptively, such as the path_ids. This gives us the necessary information for creating ImageAttachments before importing Messages. While we generate ImageAttachment rows synchronously, the actual thumbnailing job is sent to the queue worker. Theoretically, the worker could be very backlogged and not process the thumbnails anytime soon. This is fine - if the app is loaded and tries to display a message with such a not-yet-generated thumbnail, the code in `serve_file` will generate the thumbnails synchronously on the fly and the user will see the image preview displayed normally. See: https://github.com/zulip/zulip/blob/1b47134d0d564f8ba4961d25743f3ad0f09e6dfb/zerver/views/upload.py#L333-L342 --- zerver/lib/export.py | 3 ++ zerver/lib/import_realm.py | 77 ++++++++++++++++++++++++---- zerver/lib/thumbnail.py | 10 ++-- zerver/lib/upload/__init__.py | 2 +- zerver/tests/test_import_export.py | 81 +++++++++++++++++++++++++++++- 5 files changed, 155 insertions(+), 18 deletions(-) diff --git a/zerver/lib/export.py b/zerver/lib/export.py index e8be4e7771..568a856ab2 100644 --- a/zerver/lib/export.py +++ b/zerver/lib/export.py @@ -260,6 +260,9 @@ NON_EXPORTED_TABLES = { "zerver_submessage", # Drafts don't need to be exported as they are supposed to be more ephemeral. "zerver_draft", + # The importer cannot trust ImageAttachment objects anyway and needs to check + # and process images for thumbnailing on its own. + "zerver_imageattachment", # For any tables listed below here, it's a bug that they are not present in the export. } diff --git a/zerver/lib/import_realm.py b/zerver/lib/import_realm.py index d58cbda996..c3ef0a94ec 100644 --- a/zerver/lib/import_realm.py +++ b/zerver/lib/import_realm.py @@ -8,6 +8,7 @@ from typing import Any import bmemcached import orjson +import pyvips from bs4 import BeautifulSoup from django.conf import settings from django.core.cache import cache @@ -34,7 +35,7 @@ from zerver.lib.push_notifications import sends_notifications_directly from zerver.lib.remote_server import maybe_enqueue_audit_log_upload from zerver.lib.server_initialization import create_internal_realm, server_initialized from zerver.lib.streams import render_stream_description -from zerver.lib.thumbnail import BadImageError +from zerver.lib.thumbnail import THUMBNAIL_ACCEPT_IMAGE_TYPES, BadImageError, maybe_thumbnail from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.upload import ensure_avatar_image, sanitize_name, upload_backend, upload_emoji_image from zerver.lib.upload.s3 import get_bucket @@ -161,7 +162,16 @@ id_map_to_list: dict[str, dict[int, list[int]]] = { } path_maps: dict[str, dict[str, str]] = { - "attachment_path": {}, + # Maps original attachment path pre-import to the final, post-import + # attachment path. + "old_attachment_path_to_new_path": {}, + # Inverse of old_attachment_path_to_new_path. + "new_attachment_path_to_old_path": {}, + # Maps the new (post-import) attachment path to the absolute path to the file + # in the on-disk export data that we're importing. + # Allows code running after this is filled to access file contents + # without needing to go through S3 to get it. + "new_attachment_path_to_local_data_path": {}, } message_id_to_attachments: dict[str, dict[int, list[str]]] = { @@ -212,13 +222,12 @@ def fix_upload_links(data: TableData, message_table: TableName) -> None: for message in data[message_table]: if message["has_attachment"] is True: for attachment_path in message_id_to_attachments[message_table][message["id"]]: - message["content"] = message["content"].replace( - attachment_path, path_maps["attachment_path"][attachment_path] - ) + old_path = path_maps["new_attachment_path_to_old_path"][attachment_path] + message["content"] = message["content"].replace(old_path, attachment_path) if message["rendered_content"]: message["rendered_content"] = message["rendered_content"].replace( - attachment_path, path_maps["attachment_path"][attachment_path] + old_path, attachment_path ) @@ -976,7 +985,11 @@ def import_uploads( relative_path = upload_backend.generate_message_upload_path( str(record["realm_id"]), sanitize_name(os.path.basename(record["path"])) ) - path_maps["attachment_path"][record["s3_path"]] = relative_path + path_maps["old_attachment_path_to_new_path"][record["s3_path"]] = relative_path + path_maps["new_attachment_path_to_old_path"][relative_path] = record["s3_path"] + path_maps["new_attachment_path_to_local_data_path"][relative_path] = os.path.join( + import_dir, record["path"] + ) if s3_uploads: key = bucket.Object(relative_path) @@ -1612,6 +1625,22 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea with open(attachments_file, "rb") as f: attachment_data = orjson.loads(f.read()) + # We need to import ImageAttachments before messages, as the message rendering logic + # checks for existence of ImageAttachment records to determine if HTML content for image + # preview needs to be added to a message. + # In order for ImageAttachments to be correctly created, we need to know the new path_ids + # and content_types of the attachments. + # + # Begin by fixing up the Attachment data. + fix_attachments_data(attachment_data) + # Now we're ready create ImageAttachment rows and enqueue thumbnailing + # for the images. + # This order ensures that during message import, rendered_content will be generated + # correctly with image previews. + # The important detail here is that we **only** care about having ImageAttachment + # rows ready at the time of message import. Thumbnailing happens in a queue worker + # in a different process, and we don't care about when it'll complete. + create_image_attachments_and_maybe_enqueue_thumbnailing(realm, attachment_data) map_messages_to_attachments(attachment_data) # Import zerver_message and zerver_usermessage @@ -1930,10 +1959,6 @@ def import_attachments(data: TableData) -> None: "scheduledmessage_id", ) - # Update 'path_id' for the attachments - for attachment in data[parent_db_table_name]: - attachment["path_id"] = path_maps["attachment_path"][attachment["path_id"]] - # Next, load the parent rows. bulk_import_model(data, parent_model) @@ -1960,6 +1985,36 @@ def import_attachments(data: TableData) -> None: logging.info("Successfully imported M2M table %s", m2m_table_name) +def fix_attachments_data(attachment_data: TableData) -> None: + for attachment in attachment_data["zerver_attachment"]: + attachment["path_id"] = path_maps["old_attachment_path_to_new_path"][attachment["path_id"]] + + # In the case of images, content_type needs to be set for thumbnailing. + # Zulip exports set this, but third-party exports may not. + if attachment.get("content_type") is None: + guessed_content_type = guess_type(attachment["path_id"])[0] + if guessed_content_type in THUMBNAIL_ACCEPT_IMAGE_TYPES: + attachment["content_type"] = guessed_content_type + + +def create_image_attachments_and_maybe_enqueue_thumbnailing( + realm: Realm, attachment_data: TableData +) -> None: + for attachment in attachment_data["zerver_attachment"]: + if attachment["content_type"] not in THUMBNAIL_ACCEPT_IMAGE_TYPES: + continue + + path_id = attachment["path_id"] + content_type = attachment["content_type"] + + # We don't have to go to S3 to obtain the file. We still have the export + # data on disk and stored the absolute path to it. + local_filename = path_maps["new_attachment_path_to_local_data_path"][path_id] + pyvips_source = pyvips.Source.new_from_file(local_filename) + maybe_thumbnail(pyvips_source, content_type, path_id, realm.id) + continue + + def import_analytics_data(realm: Realm, import_dir: Path, crossrealm_user_ids: set[int]) -> None: analytics_filename = os.path.join(import_dir, "analytics.json") if not os.path.exists(analytics_filename): diff --git a/zerver/lib/thumbnail.py b/zerver/lib/thumbnail.py index 4bba1ee9dd..ce565d5817 100644 --- a/zerver/lib/thumbnail.py +++ b/zerver/lib/thumbnail.py @@ -14,7 +14,7 @@ from typing_extensions import override from zerver.lib.exceptions import ErrorCode, JsonableError from zerver.lib.queue import queue_event_on_commit -from zerver.models import AbstractAttachment, ImageAttachment +from zerver.models import ImageAttachment DEFAULT_AVATAR_SIZE = 100 MEDIUM_AVATAR_SIZE = 500 @@ -279,9 +279,9 @@ def missing_thumbnails(image_attachment: ImageAttachment) -> list[ThumbnailForma def maybe_thumbnail( - attachment: AbstractAttachment, content: bytes | pyvips.Source + content: bytes | pyvips.Source, content_type: str | None, path_id: str, realm_id: int ) -> ImageAttachment | None: - if attachment.content_type not in THUMBNAIL_ACCEPT_IMAGE_TYPES: + if content_type not in THUMBNAIL_ACCEPT_IMAGE_TYPES: # If it doesn't self-report as an image file that we might want # to thumbnail, don't parse the bytes at all. return None @@ -301,8 +301,8 @@ def maybe_thumbnail( (width, height) = (image.width, image.height) image_row = ImageAttachment.objects.create( - realm_id=attachment.realm_id, - path_id=attachment.path_id, + realm_id=realm_id, + path_id=path_id, original_width_px=width, original_height_px=height, frames=image.get_n_pages(), diff --git a/zerver/lib/upload/__init__.py b/zerver/lib/upload/__init__.py index e1e5d83fa5..3557c3f601 100644 --- a/zerver/lib/upload/__init__.py +++ b/zerver/lib/upload/__init__.py @@ -70,7 +70,7 @@ def create_attachment( size=file_size, content_type=content_type, ) - maybe_thumbnail(attachment, file_real_data) + maybe_thumbnail(file_real_data, content_type, path_id, realm.id) from zerver.actions.uploads import notify_attachment_update notify_attachment_update(user_profile, "add", attachment.to_dict()) diff --git a/zerver/tests/test_import_export.py b/zerver/tests/test_import_export.py index 5b38e877e4..cfda844d94 100644 --- a/zerver/tests/test_import_export.py +++ b/zerver/tests/test_import_export.py @@ -93,6 +93,7 @@ from zerver.models import ( ) from zerver.models.clients import get_client from zerver.models.groups import SystemGroups +from zerver.models.messages import ImageAttachment from zerver.models.presence import PresenceSequence from zerver.models.realm_audit_logs import AuditLogEventType from zerver.models.realms import get_realm @@ -335,6 +336,9 @@ class ExportFile(ZulipTestCase): class RealmImportExportTest(ExportFile): + def create_user_and_login(self, email: str, realm: Realm) -> None: + self.register(email, "test", subdomain=realm.subdomain) + def export_realm( self, realm: Realm, @@ -811,6 +815,32 @@ class RealmImportExportTest(ExportFile): ) self.assertEqual(realm_emoji.name, "hawaii") + # We want to set up some image data to verify image attachment thumbnailing works correctly + # in the import. + # We'll create a new user to use as the sender of the messages with such images, + # so that we can easily find them after importing - by fetching messages sent + # by the thumbnailing_test_user_email account. + thumbnailing_test_user_email = "thumbnailing_test@zulip.com" + self.create_user_and_login(thumbnailing_test_user_email, original_realm) + thumbnailing_test_user = get_user_by_delivery_email( + thumbnailing_test_user_email, original_realm + ) + + # Send a message with the image. After the import, we'll verify that this message + # and the associated ImageAttachment have been created correctly. + image_path_id = self.upload_and_thumbnail_image("img.png") + self.send_stream_message( + sender=thumbnailing_test_user, + stream_name="Verona", + content=f"An [image](/user_uploads/{image_path_id})", + ) + image_attachment = ImageAttachment.objects.get(path_id=image_path_id) + # Malform some ImageAttachment info. These shouldn't get exported (and certainly not imported!) + # anyway, so we can test that this misinformation doesn't make its way into the imported realm. + image_attachment.original_width_px = 9999 + image_attachment.original_height_px = 9999 + image_attachment.save() + # Deactivate a user to ensure such a case is covered. do_deactivate_user(self.example_user("aaron"), acting_user=None) @@ -1003,7 +1033,14 @@ class RealmImportExportTest(ExportFile): self.export_realm(original_realm, export_type=RealmExport.EXPORT_FULL_WITHOUT_CONSENT) - with self.settings(BILLING_ENABLED=False), self.assertLogs(level="INFO"): + with ( + self.settings(BILLING_ENABLED=False), + self.assertLogs(level="INFO"), + # With captureOnCommitCallbacks we ensure that tasks delegated to the queue workers + # are executed immediately. We use this to make thumbnailing runs in the import + # process in this test. + self.captureOnCommitCallbacks(execute=True), + ): do_import_realm(get_output_dir(), "test-zulip") # Make sure our export/import didn't somehow leak info into the @@ -1164,6 +1201,48 @@ class RealmImportExportTest(ExportFile): Message.objects.filter(realm=imported_realm).count(), ) + # Verify thumbnailing. + imported_thumbnailing_test_user = get_user_by_delivery_email( + thumbnailing_test_user_email, imported_realm + ) + imported_messages_with_thumbnail = Message.objects.filter( + sender=imported_thumbnailing_test_user, realm=imported_realm + ) + imported_message_with_thumbnail = imported_messages_with_thumbnail.latest("id") + attachment_with_thumbnail = Attachment.objects.get( + owner=imported_thumbnailing_test_user, messages=imported_message_with_thumbnail + ) + + path_id = attachment_with_thumbnail.path_id + # An ImageAttachment has been created in the import process. + imported_image_attachment = ImageAttachment.objects.get( + path_id=path_id, realm=imported_realm + ) + + # It figured out the dimensions correctly and didn't inherit the bad data in the + # original ImageAttachment. + self.assertEqual(imported_image_attachment.original_width_px, 128) + self.assertEqual(imported_image_attachment.original_height_px, 128) + # ImageAttachment.thumbnail_metadata contains information about thumbnails that actually + # got generated. By asserting it's not empty, we make sure thumbnailing ran for the image + # and that we didn't merely create the ImageAttachment row in the database. + self.assertNotEqual(len(imported_image_attachment.thumbnail_metadata), 0) + self.assertTrue(imported_image_attachment.thumbnail_metadata[0]) + + # Content and rendered_content got updated correctly, to point to the correct, new path_id + # and include the HTML for image preview using the thumbnail. + self.assertEqual( + imported_message_with_thumbnail.content, f"An [image](/user_uploads/{path_id})" + ) + expected_rendered_preview = ( + f'

      An image

      \n' + f'' + ) + self.assertEqual( + imported_message_with_thumbnail.rendered_content, expected_rendered_preview + ) + def test_import_message_edit_history(self) -> None: realm = get_realm("zulip") iago = self.example_user("iago") From 327647f4f8e7449f00ffbebb4102b31b7b660919 Mon Sep 17 00:00:00 2001 From: Kislay Udbhav Verma Date: Wed, 9 Oct 2024 00:47:19 +0530 Subject: [PATCH 045/276] copy_and_paste: Paste fallback md link if syntax link will be broken. If we paste a stream-topic URL that can be formatted as per #29302, we now generate a normal markdown link if the stream topic syntax could result in a broken link. Fixes #31904 --- web/src/copy_and_paste.ts | 7 +++++-- web/tests/copy_and_paste.test.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/web/src/copy_and_paste.ts b/web/src/copy_and_paste.ts index 051403bd5a..892444a053 100644 --- a/web/src/copy_and_paste.ts +++ b/web/src/copy_and_paste.ts @@ -666,18 +666,21 @@ export function try_stream_topic_syntax_text(text: string): string | null { return null; } + // Now we're sure that the URL is a valid stream topic URL. + // But the produced #**stream>topic** syntax could be broken. + const stream = stream_data.get_sub_by_id(stream_topic.stream_id); assert(stream !== undefined); const stream_name = stream.name; if (topic_link_util.will_produce_broken_stream_topic_link(stream_name)) { - return null; + return topic_link_util.get_fallback_markdown_link(stream_name, stream_topic.topic_name); } if ( stream_topic.topic_name !== undefined && topic_link_util.will_produce_broken_stream_topic_link(stream_topic.topic_name) ) { - return null; + return topic_link_util.get_fallback_markdown_link(stream_name, stream_topic.topic_name); } let syntax_text = "#**" + stream_name; diff --git a/web/tests/copy_and_paste.test.js b/web/tests/copy_and_paste.test.js index 449151bec7..98674b0aaf 100644 --- a/web/tests/copy_and_paste.test.js +++ b/web/tests/copy_and_paste.test.js @@ -12,6 +12,10 @@ stream_data.add_sub({ stream_id: 4, name: "Rome", }); +stream_data.add_sub({ + stream_id: 5, + name: "Romeo`s lair", +}); run_test("try_stream_topic_syntax_text", () => { const test_cases = [ @@ -41,11 +45,28 @@ run_test("try_stream_topic_syntax_text", () => { ["http://zulip.zulipdev.com/#narrow/topic/cheese"], ["http://zulip.zulipdev.com/#narrow/topic/pizza/stream/Rome"], - // characters which are known to produce broken #**stream>topic** urls. - ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/100.25.20profits.60"], - ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/100.25.20*profits"], - ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/.24.24 100.25.20profits"], - ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/>100.25.20profits"], + // When a url containing characters which are known to produce broken + // #**stream>topic** urls is pasted, a normal markdown link syntax is produced. + [ + "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20profits.60", + "[#Rome>100% profits`](#narrow/channel/4-Rome/topic/100.25.20profits.60)", + ], + [ + "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20*profits", + "[#Rome>100% *profits](#narrow/channel/4-Rome/topic/100.25.20*profits)", + ], + [ + "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/.24.24 100.25.20profits", + "[#Rome>$$ 100% profits](#narrow/channel/4-Rome/topic/.24.24.20100.25.20profits)", + ], + [ + "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/>100.25.20profits", + "[#Rome>>100% profits](#narrow/channel/4-Rome/topic/.3E100.25.20profits)", + ], + [ + "http://zulip.zulipdev.com/#narrow/stream/5-Romeo.60s-lair/topic/normal", + "[#Romeo`s lair>normal](#narrow/channel/5-Romeo.60s-lair/topic/normal)", + ], ]; for (const test_case of test_cases) { From 9b5accdb4300710d3d437cac824ce138b2512209 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Sun, 20 Oct 2024 10:43:15 +0530 Subject: [PATCH 046/276] css: Refactor theme color for kbd. --- web/styles/app_variables.css | 2 ++ web/styles/dark_theme.css | 7 ------- web/styles/settings.css | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index 18499cc048..f12345f245 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -761,6 +761,7 @@ --color-kbd-background: hsl(0deg 0% 98%); --color-kbd-border: hsl(0deg 0% 80%); --color-kbd-text: hsl(0deg 0% 20%); + --color-kbd-enter-sends: hsl(0deg 0% 40%); /* Markdown colors */ --color-background-rendered-markdown-thead: hsl(0deg 0% 93%); @@ -1276,6 +1277,7 @@ --color-kbd-background: hsl(211deg 29% 14%); --color-kbd-border: hsl(211deg 29% 14%); --color-kbd-text: hsl(236deg 33% 90%); + --color-kbd-enter-sends: hsl(236deg 33% 90%); /* Markdown colors */ --color-background-rendered-markdown-thead: hsl(0deg 0% 0% / 50%); diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index 0a60bbf7fc..c41aa2198a 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -48,13 +48,6 @@ box-shadow: inset 0 1px 0 hsl(0deg 0% 0% / 20%); } - #user_enter_sends_label, - #realm_enter_sends_label { - & kbd { - color: var(--color-kbd-text); - } - } - #message-formatting, #keyboard-shortcuts { & kbd { diff --git a/web/styles/settings.css b/web/styles/settings.css index df3d8a01f9..943bc438f8 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -510,7 +510,7 @@ input[type="checkbox"] { font-size: 0.9333em; font-style: normal; font-weight: 500; - color: hsl(0deg 0% 40%); + color: var(--color-kbd-enter-sends); position: relative; bottom: 1px; margin: 0 2px; From aa48a0e3ee0e33cc4ad115f90ec0ced0836fc432 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Sun, 15 Sep 2024 18:53:36 +0530 Subject: [PATCH 047/276] css: Refactor theme colors for `input_pill`. This change moves the light and dark theme colors for `input_pill` to CSS variables. --- web/styles/app_variables.css | 6 ++++++ web/styles/dark_theme.css | 4 ---- web/styles/input_pill.css | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index f12345f245..25191a4c81 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -901,6 +901,9 @@ 4deg 75% 53% / 15% ); --color-background-user-pill: hsla(0deg 0% 100% / 85%); + --color-background-compose-direct-recipient-pill-container: hsl( + 0deg 0% 100% + ); /* Inbox view constants - Values from Figma design */ --height-inbox-search: 26px; @@ -1423,6 +1426,9 @@ --color-close-deactivated-user-pill: hsl(7deg 100% 74%); --color-background-exit-hover-deactivated-user-pill: hsl(0deg 0% 100% / 7%); --color-background-user-pill: hsl(0deg 0% 0% / 40%); + --color-background-compose-direct-recipient-pill-container: hsl( + 0deg 0% 0% / 20% + ); /* Inbox view */ --color-background-inbox: var(--color-background); diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index c41aa2198a..114b57c7fe 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -386,10 +386,6 @@ border-color: hsl(3deg 73% 74%); } - #compose-direct-recipient .pill-container { - background-color: hsl(0deg 0% 0% / 20%); - } - #searchbox { /* Light theme shows hover mostly through box-shadow, and dark theme shows it mostly through changing color diff --git a/web/styles/input_pill.css b/web/styles/input_pill.css index ecfb66ba11..3900e52fee 100644 --- a/web/styles/input_pill.css +++ b/web/styles/input_pill.css @@ -181,7 +181,9 @@ #compose-direct-recipient .pill-container { border: 1px solid hsl(0deg 0% 0% / 20%); - background-color: hsl(0deg 0% 100%); + background-color: var( + --color-background-compose-direct-recipient-pill-container + ); .input:first-child:empty::before { content: attr(data-no-recipients-text); From 5b96769739d871a62aa34034c99bf7e05898fdbc Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Thu, 24 Oct 2024 13:29:32 -0500 Subject: [PATCH 048/276] right_sidebar: Correct line-height by decoupling .filters class. --- web/styles/right_sidebar.css | 2 +- web/templates/right_sidebar.hbs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/styles/right_sidebar.css b/web/styles/right_sidebar.css index 0468a153ac..a442a035a3 100644 --- a/web/styles/right_sidebar.css +++ b/web/styles/right_sidebar.css @@ -116,7 +116,7 @@ $user_status_emoji_width: 24px; } .buddy-list-section { - margin-bottom: 0; + margin: 0; overflow-x: hidden; list-style-position: inside; /* Draw the bullets inside our box */ line-height: var(--line-height-sidebar-row); diff --git a/web/templates/right_sidebar.hbs b/web/templates/right_sidebar.hbs index c717a5cff5..460d7fa9db 100644 --- a/web/templates/right_sidebar.hbs +++ b/web/templates/right_sidebar.hbs @@ -21,15 +21,15 @@
      -
        +
          -
            +
              -
                +
                • {{#tr}} - {members_count, plural, =1 {1 member.} other {# members.}} + {members_count, plural, =1 {1 member} other {# members}} {{/tr}}
                • {{/if}} From c3017e55d2ae261ebd770162ba15f43f66fa30c1 Mon Sep 17 00:00:00 2001 From: PieterCK Date: Thu, 24 Oct 2024 14:23:34 +0700 Subject: [PATCH 062/276] storage: Rework static avatar files hashing logic. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the hashing logic for static avatar files hashed the default and medium files separately, which didn’t match how user-uploaded avatars work—where you just add the "-medium.png" suffix to get the medium version. Since we don’t have clear documentation for avatars yet, this caused some issues for the mobile apps. This commit makes sure the default and its medium variation share the same hash. --- zerver/lib/storage.py | 79 ++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/zerver/lib/storage.py b/zerver/lib/storage.py index 0465ae69a8..8ca7f66e1f 100644 --- a/zerver/lib/storage.py +++ b/zerver/lib/storage.py @@ -24,26 +24,62 @@ else: return os.path.join(settings.STATIC_ROOT, path) -def reformat_medium_filename(hashed_name: str) -> str: - """ - Because the protocol for getting medium-size avatar URLs - was never fully documented, the mobile apps use a - substitution of the form s/.png/-medium.png/ to get the - medium-size avatar URLs. Thus, we must ensure the hashed - filenames for system bot avatars follow this naming convention. - """ - - name_parts = hashed_name.rsplit(".", 1) - base_name = name_parts[0] - - if len(name_parts) != 2 or "-medium" not in base_name: - return hashed_name - extension = name_parts[1].replace("png", "medium.png") - base_name = base_name.replace("-medium", "") - return f"{base_name}-{extension}" - - class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): + hashed_static_avatar_file_map: dict[str, str] = {} + + def process_static_avatars_name( + self, + name: str, + content: Optional["File[bytes]"] = None, + filename: str | None = None, + ) -> str: + """ + Because the protocol for getting medium-size avatar URLs + was never fully documented, the mobile apps use a + substitution of the form s/.png/-medium.png/ to get the + medium-size avatar URLs. + + This function hashes system bots' avatar files in a way + that follows the pattern used for user-uploaded avatars. + + It ensures the following: + + * Hashed filenames for system bot avatars follow this + naming convention: + - avatar.png -> avatar-medium.png + + * The system bots' default avatar file and its medium + version share the same hash: + - bot.36f721bad3d0.png -> bot.36f721bad3d0-medium.png + """ + + def reformat_medium_filename(hashed_name: str) -> str: + name_parts = hashed_name.rsplit(".", 1) + base_name = name_parts[0] + + if len(name_parts) != 2 or "-medium" not in base_name: + return hashed_name + extension = name_parts[1].replace("png", "medium.png") + base_name = base_name.replace("-medium", "") + return f"{base_name}-{extension}" + + if name.endswith("-medium.png"): + # This logic relies on the fact that the medium files will + # be hashed first due to the "-medium" string, which places + # them earlier in the naming order. So, adhering to the + # medium file naming convention is crucial for this logic. + hashed_medium_file: str = reformat_medium_filename( + super().hashed_name(name, content, filename) + ) + + default_file = name.replace("-medium.png", ".png") + hashed_default_file = hashed_medium_file.replace("-medium.png", ".png") + + self.hashed_static_avatar_file_map[default_file] = hashed_default_file + return hashed_medium_file + assert name in self.hashed_static_avatar_file_map + return self.hashed_static_avatar_file_map[name] + @override def hashed_name( self, name: str, content: Optional["File[bytes]"] = None, filename: str | None = None @@ -64,11 +100,8 @@ class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): # so they can hit our Nginx caching block for static files. # We don't need to worry about stale caches since these are # only used by the system bots. - if not name.endswith("-medium.png"): - return super().hashed_name(name, content, filename) + return self.process_static_avatars_name(name, content, filename) - hashed_medium_file = super().hashed_name(name, content, filename) - return reformat_medium_filename(hashed_medium_file) if name == "generated/emoji/emoji_api.json": # Unlike most .json files, we do want to hash this file; # its hashed URL is returned as part of the API. See From 71d81484ad5f51b6bea1f5c89cb1a51f6ceb49c2 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Fri, 25 Oct 2024 09:54:54 -0700 Subject: [PATCH 063/276] storage: Simplify system bot avatar logic. --- zerver/lib/storage.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/zerver/lib/storage.py b/zerver/lib/storage.py index 8ca7f66e1f..6519eb2b24 100644 --- a/zerver/lib/storage.py +++ b/zerver/lib/storage.py @@ -25,8 +25,6 @@ else: class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): - hashed_static_avatar_file_map: dict[str, str] = {} - def process_static_avatars_name( self, name: str, @@ -64,21 +62,20 @@ class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): return f"{base_name}-{extension}" if name.endswith("-medium.png"): - # This logic relies on the fact that the medium files will - # be hashed first due to the "-medium" string, which places - # them earlier in the naming order. So, adhering to the - # medium file naming convention is crucial for this logic. - hashed_medium_file: str = reformat_medium_filename( + hashed_medium_file = reformat_medium_filename( super().hashed_name(name, content, filename) ) - - default_file = name.replace("-medium.png", ".png") - hashed_default_file = hashed_medium_file.replace("-medium.png", ".png") - - self.hashed_static_avatar_file_map[default_file] = hashed_default_file return hashed_medium_file - assert name in self.hashed_static_avatar_file_map - return self.hashed_static_avatar_file_map[name] + else: + medium_name = name.replace(".png", "-medium.png") + from django.core.files import File + + with File(open(self.path(medium_name), "rb")) as medium_content: + hashed_medium_file = reformat_medium_filename( + super().hashed_name(medium_name, medium_content, filename) + ) + hashed_default_file = hashed_medium_file.replace("-medium.png", ".png") + return hashed_default_file @override def hashed_name( From d8cf3ff2e9833491736d5d3cdccda7a80114b42e Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Fri, 25 Oct 2024 10:34:09 -0700 Subject: [PATCH 064/276] ci: Run production suite when changing storage.py. --- .github/workflows/production-suite.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/production-suite.yml b/.github/workflows/production-suite.yml index 750ddae43e..799dff134b 100644 --- a/.github/workflows/production-suite.yml +++ b/.github/workflows/production-suite.yml @@ -20,6 +20,7 @@ on: - web/webpack.config.ts - zerver/worker/queue_processors.py - zerver/lib/push_notifications.py + - zerver/lib/storage.py - zerver/decorator.py - zproject/** workflow_dispatch: From 34ff1de3384d398d0670eb86f35028f4ffecc5e1 Mon Sep 17 00:00:00 2001 From: Pratik Chanda Date: Wed, 23 Oct 2024 00:30:15 +0530 Subject: [PATCH 065/276] left_sidebar: Hide new topic button on restricted compose permission. Earlier, in left stream sidebar, new topic button was shown for all stream rows irrespective of compose permission of the user for individual streams. This commit changes the behaviour by hiding the new topic button if user doesn't have appropriate compose permission for individual streams. Fixes: zulip#31800. --- web/src/stream_list.ts | 2 ++ web/templates/stream_sidebar_row.hbs | 2 ++ web/tests/stream_list.test.js | 16 ++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/web/src/stream_list.ts b/web/src/stream_list.ts index 3e653cc038..2356fb794b 100644 --- a/web/src/stream_list.ts +++ b/web/src/stream_list.ts @@ -445,6 +445,7 @@ export function set_in_home_view(stream_id: number, in_home: boolean): void { function build_stream_sidebar_li(sub: StreamSubscription): JQuery { const name = sub.name; const is_muted = stream_data.is_muted(sub.stream_id); + const can_post_messages = stream_data.can_post_messages_in_stream(sub); const args = { name, id: sub.stream_id, @@ -455,6 +456,7 @@ function build_stream_sidebar_li(sub: StreamSubscription): JQuery { color: sub.color, pin_to_top: sub.pin_to_top, hide_unread_count: settings_data.should_mask_unread_count(is_muted), + can_post_messages, }; const $list_item = $(render_stream_sidebar_row(args)); return $list_item; diff --git a/web/templates/stream_sidebar_row.hbs b/web/templates/stream_sidebar_row.hbs index bbbffe8a33..0808881a2e 100644 --- a/web/templates/stream_sidebar_row.hbs +++ b/web/templates/stream_sidebar_row.hbs @@ -11,9 +11,11 @@ {{name}}
                  diff --git a/web/tests/stream_list.test.js b/web/tests/stream_list.test.js index 0d77fbc049..5d23a52155 100644 --- a/web/tests/stream_list.test.js +++ b/web/tests/stream_list.test.js @@ -7,9 +7,14 @@ const {run_test, noop} = require("./lib/test"); const $ = require("./lib/zjquery"); const {page_params} = require("./lib/zpage_params"); +const people = zrequire("people"); +const {set_current_user} = zrequire("state_data"); + set_global("document", "document-stub"); page_params.realm_users = []; +const current_user = {}; +set_current_user(current_user); // We use this with override. let unread_unmuted_count; @@ -39,6 +44,16 @@ const {initialize_user_settings} = zrequire("user_settings"); const user_settings = {}; initialize_user_settings({user_settings}); +const me = { + email: "me@example.com", + user_id: 30, + full_name: "Me Myself", + date_joined: new Date(), +}; + +people.add_active_user(me); +people.initialize_current_user(me.user_id); + const devel = { name: "devel", stream_id: 100, @@ -673,6 +688,7 @@ test_ui("rename_stream", ({mock_template, override}) => { color: payload.color, pin_to_top: true, hide_unread_count: true, + can_post_messages: true, }); return {to_$: () => $li_stub}; }); From 9e8d908a37978c02150669116f9040b716948f60 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Thu, 24 Oct 2024 15:26:15 -0500 Subject: [PATCH 066/276] left_sidebar: Show Direct Messages controls on DM area hover. --- web/src/pm_list.ts | 8 ++++++++ web/styles/left_sidebar.css | 1 + 2 files changed, 9 insertions(+) diff --git a/web/src/pm_list.ts b/web/src/pm_list.ts index 3dc1508917..71d4dc0e27 100644 --- a/web/src/pm_list.ts +++ b/web/src/pm_list.ts @@ -258,4 +258,12 @@ export function initialize(): void { clear_search(); }); + + $(".direct-messages-container").on("mouseenter", () => { + $("#direct-messages-section-header").addClass("hover-over-dm-section"); + }); + + $(".direct-messages-container").on("mouseleave", () => { + $("#direct-messages-section-header").removeClass("hover-over-dm-section"); + }); } diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index 54ec179b83..a991ba1a07 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -259,6 +259,7 @@ } } + &.hover-over-dm-section, &.zoom-in, &:hover { #compose-new-direct-message, From cd1f58080b1660d1de175dabf77dcbb80424cb48 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Thu, 24 Oct 2024 15:36:16 -0500 Subject: [PATCH 067/276] left_sidebar: Put the New DM button to the right of All DMs. --- web/templates/left_sidebar.hbs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/templates/left_sidebar.hbs b/web/templates/left_sidebar.hbs index 65fc27ec2f..c3e3eeff5a 100644 --- a/web/templates/left_sidebar.hbs +++ b/web/templates/left_sidebar.hbs @@ -150,12 +150,12 @@
                  From 8c90c9d68d40f546771abc3f8909d2df65f20bd0 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Mon, 7 Oct 2024 16:24:04 -0400 Subject: [PATCH 068/276] modals: Set new background, border colors. --- web/styles/app_components.css | 7 ++++--- web/styles/app_variables.css | 10 ++++++++-- web/styles/dark_theme.css | 35 ++--------------------------------- web/styles/settings.css | 19 +++++++++++-------- web/styles/subscriptions.css | 18 +++++++++--------- web/styles/zulip.css | 2 +- 6 files changed, 35 insertions(+), 56 deletions(-) diff --git a/web/styles/app_components.css b/web/styles/app_components.css index cb32085483..574f0a2f7b 100644 --- a/web/styles/app_components.css +++ b/web/styles/app_components.css @@ -443,8 +443,8 @@ input.settings_text_input { .grey-box { margin: 0; padding: 5px 10px; - background-color: hsl(0deg 0% 98%); - border: 1px solid hsl(0deg 0% 87%); + background-color: var(--color-background-modal-bar); + border: 1px solid var(--color-border-modal-bar); border-radius: 4px; list-style-type: none; @@ -1004,7 +1004,8 @@ input.settings_text_input { padding-top: 4px; padding-bottom: 8px; text-align: center; - border-bottom: 1px solid hsl(0deg 0% 87%); + background-color: var(--color-background-modal-bar); + border-bottom: 1px solid var(--color-border-modal-bar); & h1 { margin: 0; diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index 25191a4c81..aa21a0fe15 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -585,7 +585,10 @@ --color-unread-marker: hsl(217deg 64% 59%); --color-masked-unread-marker: hsl(0deg 0% 80%); --color-failed-message-send-icon: hsl(3.88deg 98.84% 66.27%); - --color-background-modal: hsl(0deg 0% 98%); + --color-background-modal: #ededed; + --color-background-modal-bar: #f5f5f5; + --color-border-modal: #8c8c8c; + --color-border-modal-bar: #c2c2c2; --color-background-invitee-emails-pill-container: hsl(0deg 0% 100%); --color-unmuted-or-followed-topic-list-item: hsl(0deg 0% 20%); --color-topic-indent-border: hsl(0deg 0% 0% / 19%); @@ -1098,7 +1101,10 @@ --color-navbar-bottom-border: hsl(0deg 0% 0% / 60%); --color-unread-marker: hsl(217deg 64% 59%); --color-masked-unread-marker: hsl(0deg 0% 30%); - --color-background-modal: hsl(212deg 28% 18%); + --color-background-modal: #242424; + --color-background-modal-bar: #333; + --color-border-modal: color-mix(in srgb, #fff 15%, transparent); + --color-border-modal-bar: color-mix(in srgb, #fff 12%, transparent); --color-background-invitee-emails-pill-container: hsl(0deg 0% 0% / 20%); --color-unmuted-or-followed-topic-list-item: hsl(236deg 33% 90%); --color-recipient-bar-controls-spinner: hsl(0deg 0% 100%); diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index 114b57c7fe..ccf6f7909e 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -43,11 +43,6 @@ } } - #subscription_overlay #stream-creation .settings-sticky-footer, - #groups_overlay #user-group-creation .settings-sticky-footer { - box-shadow: inset 0 1px 0 hsl(0deg 0% 0% / 20%); - } - #message-formatting, #keyboard-shortcuts { & kbd { @@ -286,11 +281,6 @@ } } - .stream_name_search_section, - .group_name_search_section { - border-color: hsl(0deg 0% 0% / 20%); - } - #stream-actions-menu-popover .sp-container { background-color: transparent; @@ -466,7 +456,6 @@ #message-edit-history-overlay-container { .flex.overlay-content > .overlay-container { box-shadow: 0 0 30px hsl(213deg 31% 0%); - background-color: var(--color-background); } } @@ -561,21 +550,13 @@ background-color: hsl(228deg 11% 17%); } - & .drafts-container .drafts-header, - .subscriptions-container .subscriptions-header, - .user-groups-container .user-groups-header, - .overlay-messages-header, + & .overlay-messages-header, .grey-box, .white-box, .stream-email, #generate-integration-url-modal .integration-url, - #settings_page .settings-header, #settings_page .sidebar li.active, - #settings_page .sidebar-wrapper .tab-container, - .table-striped tbody tr:nth-child(odd) th, - #subscription_overlay #stream-creation .settings-sticky-footer, - #groups_overlay #user-group-creation .settings-sticky-footer { - border-color: hsl(0deg 0% 0% / 20%); + .table-striped tbody tr:nth-child(odd) th { background-color: hsl(0deg 0% 0% / 20%); } @@ -588,21 +569,11 @@ ); } - .user-groups-container .right .display-type, - .subscriptions-container .right .display-type, - .stream-row, - .group-row, - .subscriptions-container .left .list-toggler-container, - .user-groups-container .left .list-toggler-container, - .subscriptions-container .left, - .user-groups-container .left, .subscriber-list-box, .subscriber-list-box .subscriber_list_container .subscriber-list td, .member-list-box, .member-list-box .member_list_container .member-list td, #subscription_overlay .settings-radio-input-parent, - #settings_page .sidebar, - #settings_page .sidebar .sidebar-item, #recent_view_table table td { border-color: hsl(0deg 0% 0% / 20%); } @@ -755,9 +726,7 @@ } #feedback_container { - background-color: hsl(212deg 25% 15%); border-color: hsl(0deg 0% 0% / 50%); - color: inherit; & a:hover { color: hsl(0deg 0% 100%); diff --git a/web/styles/settings.css b/web/styles/settings.css index 943bc438f8..01d972cade 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -1290,7 +1290,8 @@ $option_title_width: 180px; box-sizing: border-box; height: $settings_header_height; padding: 6px; - border-bottom: 1px solid hsl(0deg 0% 87%); + background-color: var(--color-background-modal-bar); + border-bottom: 1px solid var(--color-border-modal-bar); @media (width >= $md_min) { .tab-switcher { @@ -1322,7 +1323,7 @@ $option_title_width: 180px; .sidebar { height: calc(100% - $settings_header_height); overflow-y: auto; - border-right: 1px solid hsl(0deg 0% 93%); + border-right: 1px solid var(--color-border-modal-bar); .header { height: auto; @@ -1332,8 +1333,8 @@ $option_title_width: 180px; text-align: center; text-transform: uppercase; - background-color: hsl(180deg 6% 93%); - border-bottom: 1px solid hsl(0deg 0% 87%); + background-color: var(--color-background-modal-bar); + border-bottom: 1px solid var(--color-border-modal-bar); } .sidebar-item { @@ -1351,15 +1352,16 @@ $option_title_width: 180px; transition: background-color 0.2s ease, border-bottom 0.2s ease; - border-bottom: 1px solid hsl(0deg 0% 93%); + border-bottom: 1px solid var(--color-border-modal-bar); &:last-of-type .text { border-bottom: none; } &.active { - background-color: hsl(0deg 0% 93%); - border-bottom: 1px solid transparent; + /* TODO: Check with Vlad about highlight + colors such as this. */ + background-color: hsl(0deg 0% 98%); } .sidebar-item-icon { @@ -1462,7 +1464,8 @@ $option_title_width: 180px; width: 100%; height: $settings_header_height; box-sizing: border-box; - border-bottom: 1px solid hsl(0deg 0% 87%); + border-bottom: 1px solid var(--color-border-modal-bar); + background-color: var(--color-background-modal-bar); & h1 .section { font-weight: 400; diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 1642862f30..b2a887c050 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -320,7 +320,8 @@ h4.user_group_setting_subsection_title { text-align: center; text-transform: uppercase; font-weight: 700; - border-bottom: 1px solid hsl(0deg 0% 87%); + background-color: var(--color-background-modal-bar); + border-bottom: 1px solid var(--color-border-modal-bar); .fa-chevron-left { display: none; @@ -378,12 +379,12 @@ h4.user_group_setting_subsection_title { } .left { - border-right: 1px solid hsl(0deg 0% 87%); + border-right: 1px solid var(--color-border-modal-bar); .list-toggler-container { align-items: center; padding: 6px 8px; - border-bottom: 1px solid hsl(0deg 0% 87%); + border-bottom: 1px solid var(--color-border-modal-bar); display: flex; justify-content: space-between; @@ -409,7 +410,7 @@ h4.user_group_setting_subsection_title { padding: 2px; text-align: center; font-weight: 600; - border-bottom: 1px solid hsl(0deg 0% 87%); + border-bottom: 1px solid var(--color-border-modal-bar); & a { color: inherit; @@ -496,7 +497,7 @@ h4.user_group_setting_subsection_title { justify-content: center; margin-bottom: 0; height: auto; - border-bottom: 1px solid hsl(0deg 0% 87%); + border-bottom: 1px solid var(--color-border-modal-bar); } .user-groups-list, @@ -537,7 +538,7 @@ h4.user_group_setting_subsection_title { .stream-row, .group-row { padding: 15px 10px 11px; - border-bottom: 1px solid hsl(0deg 0% 93%); + border-bottom: 1px solid var(--color-border-modal-bar); cursor: pointer; display: flex; @@ -763,9 +764,8 @@ h4.user_group_setting_subsection_title { width: calc(100% - 27px); padding: 9px 15px 15px; text-align: right; - background-color: hsl(0deg 0% 96%); - border-top: 1px solid hsl(0deg 0% 87%); - box-shadow: inset 0 1px 0 hsl(0deg 0% 100%); + background-color: var(--color-background-modal-bar); + border-top: 1px solid var(--color-border-modal-bar); } @media (width > $md_min) { diff --git a/web/styles/zulip.css b/web/styles/zulip.css index a58889bf10..eeb1c9755e 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -229,7 +229,7 @@ p.n-margin { top: 0; left: calc(50vw - 220px); padding: 15px; - background-color: hsl(0deg 0% 98%); + background-color: var(--color-background-modal); border-radius: 5px; box-shadow: 0 0 30px hsl(0deg 0% 0% / 25%); z-index: 110; From 2c78efc3c5f6059d26254d9947689071952f54a4 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Wed, 9 Oct 2024 13:08:31 -0500 Subject: [PATCH 069/276] settings: Improve layout and appearance of Organization logo area. --- web/styles/image_upload_widget.css | 28 ++++++++++++++++------------ web/styles/settings.css | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/web/styles/image_upload_widget.css b/web/styles/image_upload_widget.css index e09931caf7..7fb9bb3091 100644 --- a/web/styles/image_upload_widget.css +++ b/web/styles/image_upload_widget.css @@ -139,11 +139,14 @@ } .user-avatar-section, -.realm-logo-section, .realm-icon-section { margin: 20px 0; } +.realm-logo-section { + margin: 0 0 20px; +} + /* CSS related to settings page user avatar upload widget only */ #user-avatar-upload-widget { .image-block { @@ -184,25 +187,26 @@ } #realm-day-logo-upload-widget { - background-color: hsl(0deg 100% 100%); + /* Match to light-theme --color-background-navbar. */ + background-color: hsl(0deg 0% 97%); + padding: 5px; } #realm-night-logo-upload-widget { - background-color: hsl(212deg 28% 18%); -} - -.realm-logo-block { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 0 20px; + /* Match to dark-theme --color-background-navbar. */ + background-color: hsl(0deg 0% 13%); + padding: 5px; } .realm-logo-group { display: flex; - justify-content: space-around; flex-wrap: wrap; + gap: 20px; + + .image_upload_button { + top: 0; + left: 0; + } } /* CSS related to upload widget's preview image */ diff --git a/web/styles/settings.css b/web/styles/settings.css index 01d972cade..bb7f03c8f0 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -1274,7 +1274,7 @@ $option_title_width: 180px; } & h5 { - font-size: 1.2em; + font-size: 1em; font-weight: normal; line-height: 1.2; margin: 10px 0; From 40da0e44fb4d89626218b323a65292f94f432bd5 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Tue, 15 Oct 2024 16:33:26 -0500 Subject: [PATCH 070/276] squash: Experiment with less intense modal border colors. --- web/styles/app_variables.css | 8 ++++---- web/styles/settings.css | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index aa21a0fe15..59547706a4 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -587,8 +587,8 @@ --color-failed-message-send-icon: hsl(3.88deg 98.84% 66.27%); --color-background-modal: #ededed; --color-background-modal-bar: #f5f5f5; - --color-border-modal: #8c8c8c; - --color-border-modal-bar: #c2c2c2; + --color-border-modal: color-mix(in srgb, #8c8c8c 25%, transparent); + --color-border-modal-bar: color-mix(in srgb, #c2c2c2 80%, transparent); --color-background-invitee-emails-pill-container: hsl(0deg 0% 100%); --color-unmuted-or-followed-topic-list-item: hsl(0deg 0% 20%); --color-topic-indent-border: hsl(0deg 0% 0% / 19%); @@ -1103,8 +1103,8 @@ --color-masked-unread-marker: hsl(0deg 0% 30%); --color-background-modal: #242424; --color-background-modal-bar: #333; - --color-border-modal: color-mix(in srgb, #fff 15%, transparent); - --color-border-modal-bar: color-mix(in srgb, #fff 12%, transparent); + --color-border-modal: color-mix(in srgb, #fff 8%, transparent); + --color-border-modal-bar: color-mix(in srgb, #fff 5%, transparent); --color-background-invitee-emails-pill-container: hsl(0deg 0% 0% / 20%); --color-unmuted-or-followed-topic-list-item: hsl(236deg 33% 90%); --color-recipient-bar-controls-spinner: hsl(0deg 0% 100%); diff --git a/web/styles/settings.css b/web/styles/settings.css index bb7f03c8f0..393921416b 100644 --- a/web/styles/settings.css +++ b/web/styles/settings.css @@ -1323,7 +1323,7 @@ $option_title_width: 180px; .sidebar { height: calc(100% - $settings_header_height); overflow-y: auto; - border-right: 1px solid var(--color-border-modal-bar); + border-right: 1px solid var(--color-border-modal); .header { height: auto; @@ -1352,7 +1352,7 @@ $option_title_width: 180px; transition: background-color 0.2s ease, border-bottom 0.2s ease; - border-bottom: 1px solid var(--color-border-modal-bar); + border-bottom: 1px solid var(--color-border-modal); &:last-of-type .text { border-bottom: none; From e25bbe100502a1e807a8e1d2f5ea9a1ba8e8c5d2 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 18 Jan 2024 19:21:35 +0530 Subject: [PATCH 071/276] migration: Rename previously archived streams to their original names. As several archived streams may have the same new name, it is essential to verify whether any stream, regardless of its current status (active or archived), already has that name before executing any renaming operation. --- ...617_remove_prefix_from_archived_streams.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 zerver/migrations/0617_remove_prefix_from_archived_streams.py diff --git a/zerver/migrations/0617_remove_prefix_from_archived_streams.py b/zerver/migrations/0617_remove_prefix_from_archived_streams.py new file mode 100644 index 0000000000..9b2aeee0d6 --- /dev/null +++ b/zerver/migrations/0617_remove_prefix_from_archived_streams.py @@ -0,0 +1,56 @@ +import hashlib + +from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps + + +def remove_prefix_from_archived_streams( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + Stream = apps.get_model("zerver", "Stream") + archived_streams = Stream.objects.filter(deactivated=True) + + for archived_stream in archived_streams: + old_prefix = "!DEACTIVATED:" + streamID = str(archived_stream.id) + stream_id_hash_object = hashlib.sha512(streamID.encode()) + hashed_stream_id = stream_id_hash_object.hexdigest()[0:7] + prefix = hashed_stream_id + old_prefix + prefix_length = len(prefix) + old_name = archived_stream.name + new_name = old_name + if old_name.startswith(prefix): + new_name = old_name[prefix_length:] + + # Check for archived streams before commit 1b6f68b. + elif old_prefix in old_name: + prefix_end_index = old_name.find(old_prefix) + len(old_prefix) + new_name = old_name[prefix_end_index:] + + else: + continue + + # Check if there's an active stream or another archived stream with the new name + if not Stream.objects.filter(realm=archived_stream.realm, name=new_name).exists(): + archived_stream.name = new_name + archived_stream.save(update_fields=["name"]) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ( + "zerver", + "0616_userprofile_can_change_user_emails", + ), + ] + + operations = [ + migrations.RunPython( + remove_prefix_from_archived_streams, + reverse_code=migrations.RunPython.noop, + elidable=True, + ), + ] From c6fc25e5df5304dd283ea3e2e42937f9ba9d5790 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Sun, 29 Sep 2024 20:47:01 +0530 Subject: [PATCH 072/276] do_deactivate_stream: Do not rename streams during archiving. Functions related to archived streams are also updated. --- zerver/actions/streams.py | 22 +++++----------------- zerver/tests/test_realm.py | 3 +-- zerver/tests/test_subs.py | 30 ++++++++++-------------------- 3 files changed, 16 insertions(+), 39 deletions(-) diff --git a/zerver/actions/streams.py b/zerver/actions/streams.py index 2a666c9283..28abb82712 100644 --- a/zerver/actions/streams.py +++ b/zerver/actions/streams.py @@ -1,4 +1,3 @@ -import hashlib from collections import defaultdict from collections.abc import Collection, Iterable, Mapping from typing import Any, TypeAlias @@ -146,20 +145,8 @@ def do_deactivate_stream(stream: Stream, *, acting_user: UserProfile | None) -> # the code to unset is_web_public field on attachments below. stream.deactivated = True stream.invite_only = True - # Preserve as much as possible the original stream name while giving it a - # special prefix that both indicates that the stream is deactivated and - # frees up the original name for reuse. - old_name = stream.name - # Prepend a substring of the hashed stream ID to the new stream name - streamID = str(stream.id) - stream_id_hash_object = hashlib.sha512(streamID.encode()) - hashed_stream_id = stream_id_hash_object.hexdigest()[0:7] - - new_name = (hashed_stream_id + "!DEACTIVATED:" + old_name)[: Stream.MAX_NAME_LENGTH] - - stream.name = new_name[: Stream.MAX_NAME_LENGTH] - stream.save(update_fields=["name", "deactivated", "invite_only"]) + stream.save(update_fields=["deactivated", "invite_only"]) assert stream.recipient_id is not None if was_web_public: @@ -191,7 +178,7 @@ def do_deactivate_stream(stream: Stream, *, acting_user: UserProfile | None) -> do_remove_streams_from_default_stream_group(stream.realm, group, [stream]) stream_dict = stream_to_dict(stream) - stream_dict.update(dict(name=old_name, invite_only=was_invite_only)) + stream_dict.update(dict(invite_only=was_invite_only)) event = dict(type="stream", op="delete", streams=[stream_dict]) send_event_on_commit(stream.realm, event, affected_user_ids) @@ -222,7 +209,8 @@ def deactivated_streams_by_old_name(realm: Realm, stream_name: str) -> QuerySet[ # characters, followed by `!DEACTIVATED:`, followed by at # most MAX_NAME_LENGTH-(length of the prefix) of the name # they provided: - Q(name__regex=rf"^{fixed_length_prefix}{truncated_name}") + Q(name=stream_name) + | Q(name__regex=rf"^{fixed_length_prefix}{truncated_name}") # Finally, we go looking for the pre-1b6f68bb59dc version, # which is any number of `!` followed by `DEACTIVATED:` # and a prefix of the old stream name @@ -237,7 +225,7 @@ def do_unarchive_stream(stream: Stream, new_name: str, *, acting_user: UserProfi realm = stream.realm if not stream.deactivated: raise JsonableError(_("Channel is not currently deactivated")) - if Stream.objects.filter(realm=realm, name=new_name).exists(): + if stream.name != new_name and Stream.objects.filter(realm=realm, name=new_name).exists(): raise JsonableError( _("Channel named {channel_name} already exists").format(channel_name=new_name) ) diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index ae53ca74c6..6c09c8504f 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -656,8 +656,7 @@ class RealmTest(ZulipTestCase): stats = merge_streams(realm, denmark, atlantis) self.assertEqual(stats, (1, 1, 1)) - with self.assertRaises(Stream.DoesNotExist): - get_stream("Atlantis", realm) + self.assertEqual(get_stream("Atlantis", realm).deactivated, True) stats = merge_streams(realm, denmark, new_stream_announcements_stream) self.assertEqual(stats, (2, new_stream_announcements_stream_messages_count, 10)) diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 2d27da6f49..f3a6fd3a7a 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -1487,12 +1487,6 @@ class StreamAdminTest(ZulipTestCase): do_deactivate_stream(stream, acting_user=None) self.assertEqual(set(deactivated_streams_by_old_name(realm, "new_stream")), {stream}) - second_stream = self.make_stream("new_stream") - do_deactivate_stream(second_stream, acting_user=None) - self.assertEqual( - set(deactivated_streams_by_old_name(realm, "new_stream")), {stream, second_stream} - ) - self.make_stream("!DEACTIVATED:old_style") # This is left active old_style = self.make_stream("old_style") do_deactivate_stream(old_style, acting_user=None) @@ -2479,11 +2473,6 @@ class StreamAdminTest(ZulipTestCase): realm = stream.realm stream_id = stream.id - # Simulate that a stream by the same name has already been - # deactivated, just to exercise our renaming logic: - # Since we do not know the id of these simulated stream we prepend the name with a random hashed_stream_id - ensure_stream(realm, "DB32B77!DEACTIVATED:" + active_name, acting_user=None) - with self.capture_send_event_calls(expected_num_events=1) as events: result = self.client_delete("/json/streams/" + str(stream_id)) self.assert_json_success(result) @@ -2498,16 +2487,18 @@ class StreamAdminTest(ZulipTestCase): self.assertEqual(event["op"], "delete") self.assertEqual(event["streams"][0]["stream_id"], stream.id) - with self.assertRaises(Stream.DoesNotExist): - Stream.objects.get(realm=get_realm("zulip"), name=active_name) - - # A deleted stream's name is changed, is deactivated, is invite-only, - # and has no subscribers. hashed_stream_id = hashlib.sha512(str(stream_id).encode()).hexdigest()[0:7] - deactivated_stream_name = hashed_stream_id + "!DEACTIVATED:" + active_name + old_deactivated_stream_name = hashed_stream_id + "!DEACTIVATED:" + active_name + + with self.assertRaises(Stream.DoesNotExist): + Stream.objects.get(realm=get_realm("zulip"), name=old_deactivated_stream_name) + + # An archived stream is deactivated, is invite-only, + # and has no subscribers. + deactivated_stream_name = active_name deactivated_stream = get_stream(deactivated_stream_name, realm) self.assertTrue(deactivated_stream.deactivated) - self.assertTrue(deactivated_stream.invite_only) + self.assertEqual(deactivated_stream.invite_only, True) self.assertEqual(deactivated_stream.name, deactivated_stream_name) subscribers = self.users_subscribed_to_stream(deactivated_stream_name, realm) self.assertEqual(subscribers, []) @@ -2515,10 +2506,9 @@ class StreamAdminTest(ZulipTestCase): # It doesn't show up in the list of public streams anymore. result = self.client_get("/json/streams", {"include_subscribed": "false"}) public_streams = [s["name"] for s in self.assert_json_success(result)["streams"]] - self.assertNotIn(active_name, public_streams) self.assertNotIn(deactivated_stream_name, public_streams) - # Even if you could guess the new name, you can't subscribe to it. + # You can't subscribe to archived stream. result = self.common_subscribe_to_streams( self.example_user("hamlet"), [deactivated_stream_name], allow_fail=True ) From af7ebde9e433d02dc51a39d7cc2d2252bef903d9 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 18 Jan 2024 21:08:48 +0530 Subject: [PATCH 073/276] subscription: Include archived channels in streams list. `is_archived` field is added to the stream and types. Include a new `archived_channeels` client capability, to allow clients to access data on archived channels, without breaking backwards-compatibility for existing clients that don't know how to handle these. Also, included `exclude_archived` parameter to `/get-streams`, which defaults to `true` as basic clients may not be interested in archived streams. --- api_docs/changelog.md | 18 +++++++ version.py | 2 +- zerver/actions/streams.py | 1 + zerver/lib/event_schema.py | 1 + zerver/lib/events.py | 43 +++++++++++---- zerver/lib/home.py | 1 + zerver/lib/streams.py | 11 +++- zerver/lib/subscription_info.py | 18 +++++-- zerver/lib/types.py | 4 ++ zerver/models/streams.py | 12 +++++ zerver/openapi/zulip.yaml | 55 ++++++++++++++++++++ zerver/tests/test_events.py | 25 ++++++++- zerver/tests/test_outgoing_webhook_system.py | 2 +- zerver/tests/test_subs.py | 35 ++++++++++--- zerver/tornado/django_api.py | 2 + zerver/tornado/event_queue.py | 4 ++ zerver/tornado/views.py | 5 ++ zerver/views/streams.py | 2 + 18 files changed, 215 insertions(+), 26 deletions(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index c85add7db9..83e550074c 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,24 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 315** + +* [POST /register](/api/register-queue), [`GET + /streams/{stream_id}`](/api/get-stream-by-id), [`GET + /events`](/api/get-events), [GET + /users/me/subscriptions](/api/get-subscriptions): The `is_archived` + property has been added to channel and subscription objects. + +* [`GET /streams`](/api/get-streams): The new parameter + `exclude_archived` controls whether archived channels should be + returned. + +* [`POST /register`](/api/register-queue): The new `archived_channels` + [client + capability](/api/register-queue#parameter-client_capabilities) + allows the client to specify whether it supports archived channels + being present in the response. + **Feature level 314** * `PATCH /realm`, [`POST /register`](/api/register-queue), diff --git a/version.py b/version.py index ffc95a4a31..c4dd04a563 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 314 # Last bumped for create_multiuse_invite_group api changes. +API_FEATURE_LEVEL = 315 # Last bumped for `is_archived` # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/actions/streams.py b/zerver/actions/streams.py index 28abb82712..43a23a5754 100644 --- a/zerver/actions/streams.py +++ b/zerver/actions/streams.py @@ -435,6 +435,7 @@ def send_subscription_add_events( stream_weekly_traffic=stream_dict["stream_weekly_traffic"], subscribers=stream_subscribers, # Fields from Stream.API_FIELDS + is_archived=stream_dict["is_archived"], can_remove_subscribers_group=stream_dict["can_remove_subscribers_group"], creator_id=stream_dict["creator_id"], date_created=stream_dict["date_created"], diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 18d413c9e8..694355386c 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -50,6 +50,7 @@ from zerver.models import Realm, RealmUserDefault, Stream, UserProfile # These fields are used for "stream" events, and are included in the # larger "subscription" events that also contain personal settings. default_stream_fields = [ + ("is_archived", bool), ("can_remove_subscribers_group", int), ("creator_id", OptionalType(int)), ("date_created", int), diff --git a/zerver/lib/events.py b/zerver/lib/events.py index cfe243821a..8d7b23d4e7 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -144,6 +144,7 @@ def fetch_initial_state_data( linkifier_url_template: bool = False, user_list_incomplete: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> dict[str, Any]: """When `event_types` is None, fetches the core data powering the web app's `page_params` and `/api/v1/register` (for mobile/terminal @@ -654,6 +655,7 @@ def fetch_initial_state_data( sub_info = gather_subscriptions_helper( user_profile, include_subscribers=include_subscribers, + include_archived_channels=archived_channels, ) else: sub_info = get_web_public_subs(realm) @@ -787,6 +789,7 @@ def apply_events( linkifier_url_template: bool, user_list_incomplete: bool, include_deactivated_groups: bool, + archived_channels: bool = False, ) -> None: for event in events: if fetch_event_types is not None and event["type"] not in fetch_event_types: @@ -809,6 +812,7 @@ def apply_events( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) @@ -823,6 +827,7 @@ def apply_event( linkifier_url_template: bool, user_list_incomplete: bool, include_deactivated_groups: bool, + archived_channels: bool = False, ) -> None: if event["type"] == "message": state["max_message_id"] = max(state["max_message_id"], event["message"]["id"]) @@ -1210,17 +1215,30 @@ def apply_event( s for s in state["streams"] if s["stream_id"] not in deleted_stream_ids ] - state["subscriptions"] = [ - stream - for stream in state["subscriptions"] - if stream["stream_id"] not in deleted_stream_ids - ] + if archived_channels: + for stream in state["subscriptions"]: + if stream["stream_id"] in deleted_stream_ids: + stream["is_archived"] = True - state["unsubscribed"] = [ - stream - for stream in state["unsubscribed"] - if stream["stream_id"] not in deleted_stream_ids - ] + for stream in state["unsubscribed"]: + if stream["stream_id"] in deleted_stream_ids: + stream["is_archived"] = True + stream["first_message_id"] = Stream.objects.get( + id=stream["stream_id"] + ).first_message_id + + else: + state["subscriptions"] = [ + stream + for stream in state["subscriptions"] + if stream["stream_id"] not in deleted_stream_ids + ] + + state["unsubscribed"] = [ + stream + for stream in state["unsubscribed"] + if stream["stream_id"] not in deleted_stream_ids + ] state["never_subscribed"] = [ stream @@ -1720,6 +1738,7 @@ class ClientCapabilities(TypedDict): linkifier_url_template: NotRequired[bool] user_list_incomplete: NotRequired[bool] include_deactivated_groups: NotRequired[bool] + archived_channels: NotRequired[bool] def do_events_register( @@ -1757,6 +1776,7 @@ def do_events_register( linkifier_url_template = client_capabilities.get("linkifier_url_template", False) user_list_incomplete = client_capabilities.get("user_list_incomplete", False) include_deactivated_groups = client_capabilities.get("include_deactivated_groups", False) + archived_channels = client_capabilities.get("archived_channels", False) if fetch_event_types is not None: event_types_set: set[str] | None = set(fetch_event_types) @@ -1789,6 +1809,7 @@ def do_events_register( user_avatar_url_field_optional=user_avatar_url_field_optional, user_settings_object=user_settings_object, user_list_incomplete=user_list_incomplete, + archived_channels=archived_channels, # These presence params are a noop, because presence is not included. slim_presence=True, presence_last_update_id_fetched_by_client=None, @@ -1828,6 +1849,7 @@ def do_events_register( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) if queue_id is None: @@ -1850,6 +1872,7 @@ def do_events_register( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) # Apply events that came in while we were fetching initial data diff --git a/zerver/lib/home.py b/zerver/lib/home.py index 30aef3b275..8a1c650408 100644 --- a/zerver/lib/home.py +++ b/zerver/lib/home.py @@ -156,6 +156,7 @@ def build_page_params_for_home_page_load( linkifier_url_template=True, user_list_incomplete=True, include_deactivated_groups=True, + archived_channels=False, ) if user_profile is not None: diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index 275f596552..78c903fefd 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -872,6 +872,7 @@ def stream_to_dict(stream: Stream, recent_traffic: dict[int, int] | None = None) stream_weekly_traffic = None return APIStreamDict( + is_archived=stream.deactivated, can_remove_subscribers_group=stream.can_remove_subscribers_group_id, creator_id=stream.creator_id, date_created=datetime_to_timestamp(stream.date_created), @@ -902,6 +903,7 @@ def get_streams_for_user( include_public: bool = True, include_web_public: bool = False, include_subscribed: bool = True, + exclude_archived: bool = True, include_all_active: bool = False, include_owner_subscribed: bool = False, ) -> list[Stream]: @@ -910,8 +912,11 @@ def get_streams_for_user( include_public = include_public and user_profile.can_access_public_streams() - # Start out with all active streams in the realm. - query = Stream.objects.filter(realm=user_profile.realm, deactivated=False) + # Start out with all streams in the realm. + query = Stream.objects.filter(realm=user_profile.realm) + + if exclude_archived: + query = query.filter(deactivated=False) if include_all_active: streams = query.only(*Stream.API_FIELDS) @@ -965,6 +970,7 @@ def do_get_streams( include_public: bool = True, include_web_public: bool = False, include_subscribed: bool = True, + exclude_archived: bool = True, include_all_active: bool = False, include_default: bool = False, include_owner_subscribed: bool = False, @@ -976,6 +982,7 @@ def do_get_streams( include_public, include_web_public, include_subscribed, + exclude_archived, include_all_active, include_owner_subscribed, ) diff --git a/zerver/lib/subscription_info.py b/zerver/lib/subscription_info.py index a6b13de712..c21290b868 100644 --- a/zerver/lib/subscription_info.py +++ b/zerver/lib/subscription_info.py @@ -27,7 +27,7 @@ from zerver.lib.types import ( SubscriptionStreamDict, ) from zerver.models import Realm, Stream, Subscription, UserProfile -from zerver.models.streams import get_active_streams +from zerver.models.streams import get_all_streams def get_web_public_subs(realm: Realm) -> SubscriptionInfo: @@ -42,6 +42,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo: subscribed = [] for stream in get_web_public_streams_queryset(realm): # Add Stream fields. + is_archived = stream.deactivated can_remove_subscribers_group_id = stream.can_remove_subscribers_group_id creator_id = stream.creator_id date_created = datetime_to_timestamp(stream.date_created) @@ -73,6 +74,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo: wildcard_mentions_notify = True sub = SubscriptionStreamDict( + is_archived=is_archived, audible_notifications=audible_notifications, can_remove_subscribers_group=can_remove_subscribers_group_id, color=color, @@ -115,6 +117,7 @@ def build_unsubscribed_sub_from_stream_dict( can_remove_subscribers_group_id=stream_dict["can_remove_subscribers_group"], creator_id=stream_dict["creator_id"], date_created=timestamp_to_datetime(stream_dict["date_created"]), + deactivated=stream_dict["is_archived"], description=stream_dict["description"], first_message_id=stream_dict["first_message_id"], history_public_to_subscribers=stream_dict["history_public_to_subscribers"], @@ -144,6 +147,7 @@ def build_stream_dict_for_sub( recent_traffic: dict[int, int] | None, ) -> SubscriptionStreamDict: # Handle Stream.API_FIELDS + is_archived = raw_stream_dict["deactivated"] can_remove_subscribers_group_id = raw_stream_dict["can_remove_subscribers_group_id"] creator_id = raw_stream_dict["creator_id"] date_created = datetime_to_timestamp(raw_stream_dict["date_created"]) @@ -187,6 +191,7 @@ def build_stream_dict_for_sub( # Our caller may add a subscribers field. return SubscriptionStreamDict( + is_archived=is_archived, audible_notifications=audible_notifications, can_remove_subscribers_group=can_remove_subscribers_group_id, color=color, @@ -218,6 +223,7 @@ def build_stream_dict_for_never_sub( raw_stream_dict: RawStreamDict, recent_traffic: dict[int, int] | None, ) -> NeverSubscribedStreamDict: + is_archived = raw_stream_dict["deactivated"] can_remove_subscribers_group_id = raw_stream_dict["can_remove_subscribers_group_id"] creator_id = raw_stream_dict["creator_id"] date_created = datetime_to_timestamp(raw_stream_dict["date_created"]) @@ -244,6 +250,7 @@ def build_stream_dict_for_never_sub( # Our caller may add a subscribers field. return NeverSubscribedStreamDict( + is_archived=is_archived, can_remove_subscribers_group=can_remove_subscribers_group_id, creator_id=creator_id, date_created=date_created, @@ -447,9 +454,12 @@ def has_metadata_access_to_previously_subscribed_stream( def gather_subscriptions_helper( user_profile: UserProfile, include_subscribers: bool = True, + include_archived_channels: bool = False, ) -> SubscriptionInfo: realm = user_profile.realm - all_streams = get_active_streams(realm).values( + all_streams = get_all_streams( + realm, include_archived_channels=include_archived_channels + ).values( *Stream.API_FIELDS, # The realm_id and recipient_id are generally not needed in the API. "realm_id", @@ -517,7 +527,9 @@ def gather_subscriptions_helper( unsubscribed.append(stream_dict) if user_profile.can_access_public_streams(): - never_subscribed_stream_ids = set(all_streams_map) - sub_unsub_stream_ids + never_subscribed_stream_ids = { + stream["id"] for stream in all_streams if not stream["deactivated"] + } - sub_unsub_stream_ids else: web_public_stream_ids = {stream["id"] for stream in all_streams if stream["is_web_public"]} never_subscribed_stream_ids = web_public_stream_ids - sub_unsub_stream_ids diff --git a/zerver/lib/types.py b/zerver/lib/types.py index 6937efeee9..f6af9f30fc 100644 --- a/zerver/lib/types.py +++ b/zerver/lib/types.py @@ -144,6 +144,7 @@ class RawStreamDict(TypedDict): can_remove_subscribers_group_id: int creator_id: int | None date_created: datetime + deactivated: bool description: str first_message_id: int | None history_public_to_subscribers: bool @@ -193,6 +194,7 @@ class SubscriptionStreamDict(TypedDict): in_home_view: bool invite_only: bool is_announcement_only: bool + is_archived: bool is_muted: bool is_web_public: bool message_retention_days: int | None @@ -208,6 +210,7 @@ class SubscriptionStreamDict(TypedDict): class NeverSubscribedStreamDict(TypedDict): + is_archived: bool can_remove_subscribers_group: int creator_id: int | None date_created: int @@ -232,6 +235,7 @@ class DefaultStreamDict(TypedDict): with few exceptions and possible additional fields. """ + is_archived: bool can_remove_subscribers_group: int creator_id: int | None date_created: int diff --git a/zerver/models/streams.py b/zerver/models/streams.py index 06b9a71f7e..0036d4592e 100644 --- a/zerver/models/streams.py +++ b/zerver/models/streams.py @@ -182,6 +182,7 @@ class Stream(models.Model): API_FIELDS = [ "creator_id", "date_created", + "deactivated", "description", "first_message_id", "history_public_to_subscribers", @@ -197,6 +198,7 @@ class Stream(models.Model): def to_dict(self) -> DefaultStreamDict: return DefaultStreamDict( + is_archived=self.deactivated, can_remove_subscribers_group=self.can_remove_subscribers_group_id, creator_id=self.creator_id, date_created=datetime_to_timestamp(self.date_created), @@ -229,6 +231,16 @@ def get_active_streams(realm: Realm) -> QuerySet[Stream]: return Stream.objects.filter(realm=realm, deactivated=False) +def get_all_streams(realm: Realm, include_archived_channels: bool = True) -> QuerySet[Stream]: + """ + Return all streams for `include_archived_channels`= true (including invite-only and deactivated streams). + """ + if not include_archived_channels: + return get_active_streams(realm) + + return Stream.objects.filter(realm=realm) + + def get_linkable_streams(realm_id: int) -> QuerySet[Stream]: """ This returns the streams that we are allowed to linkify using diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index d3e18a888f..0f543903e8 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -694,6 +694,7 @@ paths: { "name": "test", "stream_id": 9, + "is_archived": false, "creator_id": null, "description": "", "rendered_description": "", @@ -1337,6 +1338,7 @@ paths: { "name": "private", "stream_id": 12, + "is_archived": false, "description": "", "rendered_description": "", "date_created": 1691057093, @@ -1395,6 +1397,7 @@ paths: { "name": "private", "stream_id": 12, + "is_archived": true, "description": "", "rendered_description": "", "date_created": 1691057093, @@ -1994,6 +1997,7 @@ paths: { "name": "Scotland", "stream_id": 3, + "is_archived": false, "description": "Located in the United Kingdom", "rendered_description": "

                  Located in the United Kingdom

                  ", "date_created": 1691057093, @@ -2010,6 +2014,7 @@ paths: { "name": "Denmark", "stream_id": 1, + "is_archived": false, "description": "A Scandinavian country", "rendered_description": "

                  A Scandinavian country

                  ", "date_created": 1691057093, @@ -2026,6 +2031,7 @@ paths: { "name": "Verona", "stream_id": 5, + "is_archived": false, "description": "A city in Italy", "rendered_description": "

                  A city in Italy

                  ", "date_created": 1691057093, @@ -2073,6 +2079,7 @@ paths: { "name": "Scotland", "stream_id": 3, + "is_archived": false, "description": "Located in the United Kingdom", "rendered_description": "

                  Located in the United Kingdom

                  ", "date_created": 1691057093, @@ -10178,6 +10185,7 @@ paths: "creator_id": null, "description": "A Scandinavian country", "desktop_notifications": true, + "is_archived": false, "is_muted": false, "invite_only": false, "name": "Denmark", @@ -10192,6 +10200,7 @@ paths: "creator_id": 8, "description": "Located in the United Kingdom", "desktop_notifications": true, + "is_archived": false, "is_muted": false, "invite_only": false, "name": "Scotland", @@ -14079,6 +14088,15 @@ paths: **Changes**: New in Zulip 10.0 (feature level 294). This capability is for backwards-compatibility. + - `archived_channels`: Boolean for whether the client supports processing + [archived channels](/help/archive-a-channel) in the `stream` and + `subscription` event types. If `false`, the server will not include data + related to archived channels in the `register` response or in events. +
                  + **Changes**: New in Zulip 10.0 (feature level 315). This allows clients to + access archived channels, without breaking backwards-compatibility for + existing clients. + [help-linkifiers]: /help/add-a-custom-linkifier [rfc6570]: https://www.rfc-editor.org/rfc/rfc6570.html [events-linkifiers]: /api/get-events#realm_linkifiers @@ -14784,6 +14802,7 @@ paths: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -19562,6 +19581,16 @@ paths: type: boolean default: true example: false + - name: exclude_archived + in: query + description: | + Whether to exclude archived streams from the results. + + **Changes**: New in Zulip 10.0 (feature level 315). + schema: + type: boolean + default: true + example: true - name: include_all_active in: query description: | @@ -19612,6 +19641,7 @@ paths: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -19655,6 +19685,7 @@ paths: required: - stream_id - name + - is_archived - description - date_created - creator_id @@ -19683,6 +19714,7 @@ paths: "history_public_to_subscribers": false, "invite_only": true, "is_announcement_only": false, + "is_archived": false, "is_default": false, "is_web_public": false, "message_retention_days": null, @@ -19701,6 +19733,7 @@ paths: "history_public_to_subscribers": true, "invite_only": false, "is_announcement_only": false, + "is_archived": false, "is_default": true, "is_web_public": false, "message_retention_days": null, @@ -19768,6 +19801,7 @@ paths: "creator_id": null, "invite_only": false, "is_announcement_only": false, + "is_archived": false, "is_web_public": false, "message_retention_days": null, "name": "Denmark", @@ -21555,6 +21589,7 @@ components: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -21588,6 +21623,7 @@ components: required: - stream_id - name + - is_archived - description - date_created - creator_id @@ -21608,6 +21644,7 @@ components: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -21626,6 +21663,7 @@ components: required: - stream_id - name + - is_archived - description - date_created - creator_id @@ -21651,6 +21689,13 @@ components: type: string description: | The name of the channel. + is_archived: + type: boolean + description: | + A boolean indicating whether the channel is [archived](/help/archive-a-channel). + + **Changes**: New in Zulip 10.0 (feature level 315). + Previously, this endpoint never returned archived channels. description: type: string description: | @@ -22753,6 +22798,16 @@ components: was named `can_remove_subscribers_group_id`. New in Zulip 6.0 (feature level 142). + is_archived: + type: boolean + description: | + A boolean indicating whether the channel is [archived](/help/archive-a-channel). + + **Changes**: New in Zulip 10.0 (feature level 315). + Previously, subscriptions only included active + channels. Note that some endpoints will never return archived + channels unless the client declares explicit support for + them via the `archived_channels` client capability. DefaultChannelGroup: type: object description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 905caed49f..ca0ac83765 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -305,6 +305,7 @@ class BaseAction(ZulipTestCase): user_list_incomplete: bool = False, client_is_old: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> Iterator[list[dict[str, Any]]]: """ Make sure we have a clean slate of client descriptors for these tests. @@ -354,6 +355,7 @@ class BaseAction(ZulipTestCase): linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) if client_is_old: @@ -395,6 +397,7 @@ class BaseAction(ZulipTestCase): linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) post_process_state(self.user_profile, hybrid_state, notification_settings_null) after = orjson.dumps(hybrid_state) @@ -425,6 +428,7 @@ class BaseAction(ZulipTestCase): linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) post_process_state(self.user_profile, normal_state, notification_settings_null) self.match_states(hybrid_state, normal_state, events) @@ -3355,6 +3359,23 @@ class NormalActionsTest(BaseAction): check_stream_delete("events[0]", events[0]) self.assertIsNone(events[0]["streams"][0]["stream_weekly_traffic"]) + def test_admin_deactivate_unsubscribed_stream(self) -> None: + self.set_up_db_for_testing_user_access() + stream = self.make_stream("test_stream") + iago = self.example_user("iago") + realm = iago.realm + self.user_profile = self.example_user("iago") + + self.subscribe(iago, stream.name) + self.assertCountEqual(self.users_subscribed_to_stream(stream.name, realm), [iago]) + + self.unsubscribe(iago, stream.name) + self.assertCountEqual(self.users_subscribed_to_stream(stream.name, realm), []) + + with self.verify_action(num_events=1, archived_channels=True) as events: + do_deactivate_stream(stream, acting_user=iago) + check_stream_delete("events[0]", events[0]) + def test_user_losing_access_on_deactivating_stream(self) -> None: self.set_up_db_for_testing_user_access() polonius = self.example_user("polonius") @@ -3367,7 +3388,7 @@ class NormalActionsTest(BaseAction): self.users_subscribed_to_stream(stream.name, realm), [hamlet, polonius] ) - with self.verify_action(num_events=2) as events: + with self.verify_action(num_events=2, archived_channels=True) as events: do_deactivate_stream(stream, acting_user=None) check_stream_delete("events[0]", events[0]) check_realm_user_remove("events[1]", events[1]) @@ -3383,7 +3404,7 @@ class NormalActionsTest(BaseAction): self.users_subscribed_to_stream(stream.name, realm), [iago, polonius, shiva] ) - with self.verify_action(num_events=2) as events: + with self.verify_action(num_events=2, archived_channels=True) as events: do_deactivate_stream(stream, acting_user=None) check_stream_delete("events[0]", events[0]) check_realm_user_remove("events[1]", events[1]) diff --git a/zerver/tests/test_outgoing_webhook_system.py b/zerver/tests/test_outgoing_webhook_system.py index 470c29c901..b0ca446964 100644 --- a/zerver/tests/test_outgoing_webhook_system.py +++ b/zerver/tests/test_outgoing_webhook_system.py @@ -648,7 +648,7 @@ class TestOutgoingWebhookMessaging(ZulipTestCase): prev_message = self.get_second_to_last_message() self.assertIn( - "tried to send a message to channel #**Denmark**, but that channel does not exist", + "Failure! Bot is unavailable", prev_message.content, ) diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index f3a6fd3a7a..16ebe32c91 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -238,8 +238,8 @@ class TestMiscStuff(ZulipTestCase): """Verify that all the fields from `Stream.API_FIELDS` and `Subscription.API_FIELDS` present in `APIStreamDict` and `APISubscriptionDict`, respectively. """ - expected_fields = set(Stream.API_FIELDS) | {"stream_id"} - expected_fields -= {"id", "can_remove_subscribers_group_id"} + expected_fields = set(Stream.API_FIELDS) | {"stream_id", "is_archived"} + expected_fields -= {"id", "can_remove_subscribers_group_id", "deactivated"} expected_fields |= {"can_remove_subscribers_group"} stream_dict_fields = set(APIStreamDict.__annotations__.keys()) @@ -2508,6 +2508,13 @@ class StreamAdminTest(ZulipTestCase): public_streams = [s["name"] for s in self.assert_json_success(result)["streams"]] self.assertNotIn(deactivated_stream_name, public_streams) + # It shows up with `exclude_archived` parameter set to false. + result = self.client_get( + "/json/streams", {"exclude_archived": "false", "include_all_active": "true"} + ) + streams = [s["name"] for s in self.assert_json_success(result)["streams"]] + self.assertIn(deactivated_stream_name, streams) + # You can't subscribe to archived stream. result = self.common_subscribe_to_streams( self.example_user("hamlet"), [deactivated_stream_name], allow_fail=True @@ -5464,10 +5471,10 @@ class SubscriptionAPITest(ZulipTestCase): self.assert_length(result, 1) self.assertEqual(result[0]["stream_id"], stream1.id) - def test_gather_subscriptions_excludes_deactivated_streams(self) -> None: + def test_gather_subscriptions_deactivated_streams(self) -> None: """ - Check that gather_subscriptions_helper does not include deactivated streams in its - results. + Check that gather_subscriptions_helper does/doesn't include deactivated streams in its + results with `exclude_archived` parameter. """ realm = get_realm("zulip") admin_user = self.example_user("iago") @@ -5497,6 +5504,10 @@ class SubscriptionAPITest(ZulipTestCase): admin_after_delete = gather_subscriptions_helper(admin_user) non_admin_after_delete = gather_subscriptions_helper(non_admin_user) + admin_after_delete_include_archived = gather_subscriptions_helper( + admin_user, include_archived_channels=True + ) + # Compare results - should be 1 stream less self.assertTrue( len(admin_before_delete.subscriptions) == len(admin_after_delete.subscriptions) + 1, @@ -5508,6 +5519,14 @@ class SubscriptionAPITest(ZulipTestCase): "Expected exactly 1 less stream from gather_subscriptions_helper", ) + # Compare results - should be the same number of streams + self.assertTrue( + len(admin_before_delete.subscriptions) + len(admin_before_delete.unsubscribed) + == len(admin_after_delete_include_archived.subscriptions) + + len(admin_after_delete_include_archived.unsubscribed), + "Expected exact number of streams from gather_subscriptions_helper", + ) + def test_validate_user_access_to_subscribers_helper(self) -> None: """ Ensure the validate_user_access_to_subscribers_helper is properly raising @@ -5944,6 +5963,7 @@ class GetSubscribersTest(ZulipTestCase): def verify_sub_fields(self, sub_data: SubscriptionInfo) -> None: other_fields = { + "is_archived", "is_announcement_only", "in_home_view", "stream_id", @@ -5952,7 +5972,7 @@ class GetSubscribersTest(ZulipTestCase): } expected_fields = set(Stream.API_FIELDS) | set(Subscription.API_FIELDS) | other_fields - expected_fields -= {"id", "can_remove_subscribers_group_id"} + expected_fields -= {"id", "can_remove_subscribers_group_id", "deactivated"} expected_fields |= {"can_remove_subscribers_group"} for lst in [sub_data.subscriptions, sub_data.unsubscribed]: @@ -5960,6 +5980,7 @@ class GetSubscribersTest(ZulipTestCase): self.assertEqual(set(sub), expected_fields) other_fields = { + "is_archived", "is_announcement_only", "stream_id", "stream_weekly_traffic", @@ -5967,7 +5988,7 @@ class GetSubscribersTest(ZulipTestCase): } expected_fields = set(Stream.API_FIELDS) | other_fields - expected_fields -= {"id", "can_remove_subscribers_group_id"} + expected_fields -= {"id", "can_remove_subscribers_group_id", "deactivated"} expected_fields |= {"can_remove_subscribers_group"} for never_sub in sub_data.never_subscribed: diff --git a/zerver/tornado/django_api.py b/zerver/tornado/django_api.py index b9bc56b0cc..d7043401d8 100644 --- a/zerver/tornado/django_api.py +++ b/zerver/tornado/django_api.py @@ -91,6 +91,7 @@ def request_event_queue( linkifier_url_template: bool = False, user_list_incomplete: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> str | None: if not settings.USING_TORNADO: return None @@ -115,6 +116,7 @@ def request_event_queue( "linkifier_url_template": orjson.dumps(linkifier_url_template), "user_list_incomplete": orjson.dumps(user_list_incomplete), "include_deactivated_groups": orjson.dumps(include_deactivated_groups), + "archived_channels": orjson.dumps(archived_channels), } if event_types is not None: diff --git a/zerver/tornado/event_queue.py b/zerver/tornado/event_queue.py index 6b0f246424..84964eb044 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -79,6 +79,7 @@ class ClientDescriptor: linkifier_url_template: bool = False, user_list_incomplete: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> None: # TODO: We eventually want to upstream this code to the caller, but # serialization concerns make it a bit difficult. @@ -110,6 +111,7 @@ class ClientDescriptor: self.linkifier_url_template = linkifier_url_template self.user_list_incomplete = user_list_incomplete self.include_deactivated_groups = include_deactivated_groups + self.archived_channels = archived_channels # Default for lifespan_secs is DEFAULT_EVENT_QUEUE_TIMEOUT_SECS; # but users can set it as high as MAX_QUEUE_TIMEOUT_SECS. @@ -141,6 +143,7 @@ class ClientDescriptor: linkifier_url_template=self.linkifier_url_template, user_list_incomplete=self.user_list_incomplete, include_deactivated_groups=self.include_deactivated_groups, + archived_channels=self.archived_channels, ) @override @@ -178,6 +181,7 @@ class ClientDescriptor: d.get("linkifier_url_template", False), d.get("user_list_incomplete", False), d.get("include_deactivated_groups", False), + d.get("archived_channels", False), ) ret.last_connection_time = d["last_connection_time"] return ret diff --git a/zerver/tornado/views.py b/zerver/tornado/views.py index f2a9060bdd..3835cd8af3 100644 --- a/zerver/tornado/views.py +++ b/zerver/tornado/views.py @@ -210,6 +210,10 @@ def get_events_backend( Json[bool], ApiParamConfig(documentation_status=DocumentationStatus.INTENTIONALLY_UNDOCUMENTED), ] = False, + archived_channels: Annotated[ + Json[bool], + ApiParamConfig(documentation_status=DocumentationStatus.INTENTIONALLY_UNDOCUMENTED), + ] = False, ) -> HttpResponse: if narrow is None: narrow = [] @@ -248,6 +252,7 @@ def get_events_backend( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) result = in_tornado_thread(fetch_events)( diff --git a/zerver/views/streams.py b/zerver/views/streams.py index add9fe0880..d618d473e5 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -847,6 +847,7 @@ def get_streams_backend( include_public: Json[bool] = True, include_web_public: Json[bool] = False, include_subscribed: Json[bool] = True, + exclude_archived: Json[bool] = True, include_all_active: Json[bool] = False, include_default: Json[bool] = False, include_owner_subscribed: Json[bool] = False, @@ -856,6 +857,7 @@ def get_streams_backend( include_public=include_public, include_web_public=include_web_public, include_subscribed=include_subscribed, + exclude_archived=exclude_archived, include_all_active=include_all_active, include_default=include_default, include_owner_subscribed=include_owner_subscribed, From ca9ac293f3dce5a359d0cd72594edeb40c06a05b Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 25 Apr 2024 20:43:43 +0530 Subject: [PATCH 074/276] sub_store: Add `is_archived` field to `Stream` type. --- web/src/stream_types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/stream_types.ts b/web/src/stream_types.ts index cb6b37455f..8340a5b3cb 100644 --- a/web/src/stream_types.ts +++ b/web/src/stream_types.ts @@ -16,6 +16,7 @@ export const stream_schema = z.object({ history_public_to_subscribers: z.boolean(), invite_only: z.boolean(), is_announcement_only: z.boolean(), + is_archived: z.boolean(), is_web_public: z.boolean(), message_retention_days: z.number().nullable(), name: z.string(), From a91721529212220d40a44372b7ec3db204098811 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Sun, 28 Jan 2024 00:50:37 +0530 Subject: [PATCH 075/276] get_subs_for_settings: Exclude archived streams from `All Streams` menu. Don't display stream settings for archived channels. --- web/src/hash_util.ts | 8 ++++++-- web/src/stream_settings_data.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/hash_util.ts b/web/src/hash_util.ts index d3c6a0ff1a..ccf554f4e1 100644 --- a/web/src/hash_util.ts +++ b/web/src/hash_util.ts @@ -223,14 +223,18 @@ export function validate_channels_settings_hash(hash: string): string { const stream_id = Number.parseInt(section, 10); const sub = sub_store.get(stream_id); // There are a few situations where we can't display stream settings: - // 1. This is a stream that's been archived. (sub=undefined) + // 1. This is a stream that's been archived. (sub.is_archived=true) // 2. The stream ID is invalid. (sub=undefined) // 3. The current user is a guest, and was unsubscribed from the stream // stream in the current session. (In future sessions, the stream will // not be in sub_store). // // In all these cases we redirect the user to 'subscribed' tab. - if (sub === undefined || (page_params.is_guest && !stream_data.is_subscribed(stream_id))) { + if ( + sub === undefined || + sub.is_archived || + (page_params.is_guest && !stream_data.is_subscribed(stream_id)) + ) { return channels_settings_section_url(); } diff --git a/web/src/stream_settings_data.ts b/web/src/stream_settings_data.ts index 660bf63a86..d7cb664a8a 100644 --- a/web/src/stream_settings_data.ts +++ b/web/src/stream_settings_data.ts @@ -63,7 +63,7 @@ function get_subs_for_settings(subs: StreamSubscription[]): SettingsSubscription // delegating, so that we can more efficiently compute subscriber counts // (in bulk). If that plan appears to have been aborted, feel free to // inline this. - return subs.map((sub) => get_sub_for_settings(sub)); + return subs.filter((sub) => !sub.is_archived).map((sub) => get_sub_for_settings(sub)); } export function get_updated_unsorted_subs(): SettingsSubscription[] { From 616e39c290f647a82a3981b86dd5ac417739a980 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 8 Feb 2024 18:34:27 +0530 Subject: [PATCH 076/276] stream_data: Make `Subscribe` button not visible for archived streams. --- web/src/stream_data.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index f6cc3255d0..4e7a26d958 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -481,11 +481,9 @@ export function can_toggle_subscription(sub: StreamSubscription): boolean { // Spectators cannot subscribe to any streams. // // Note that the correctness of this logic relies on the fact that - // one cannot be subscribed to a deactivated stream, and - // deactivated streams are automatically made private during the - // archive stream process. + // one cannot be subscribed to a deactivated stream. return ( - (sub.subscribed || (!current_user.is_guest && !sub.invite_only)) && + (sub.subscribed || (!current_user.is_guest && !(sub.invite_only || sub.is_archived))) && !page_params.is_spectator ); } From fa268877d33ec2dbba9728b758a199ca28e84173 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Sun, 28 Jan 2024 00:52:27 +0530 Subject: [PATCH 077/276] stream: Show `(archived)` at the end of deactivated stream names. When a stream is deactivated the title area and messages are re-rendered to update the stream name with `(archived)` suffix. --- tools/lib/capitalization.py | 2 ++ web/src/inbox_ui.ts | 2 ++ web/src/message_list.ts | 2 +- web/src/message_list_view.ts | 3 +++ web/src/server_events_dispatch.js | 1 + web/src/stream_data.ts | 5 +++++ web/styles/app_variables.css | 1 + web/styles/inbox.css | 4 ++++ web/styles/message_header.css | 9 +++++++++ web/styles/message_view_header.css | 6 ++++++ web/templates/inbox_view/inbox_stream_header_row.hbs | 5 +++++ web/templates/message_view_header.hbs | 6 ++++++ web/templates/navbar_icon_and_title.hbs | 7 +++++++ web/templates/recipient_row.hbs | 7 ++++++- 14 files changed, 58 insertions(+), 2 deletions(-) diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py index 17afcd37f8..efc49a0aca 100644 --- a/tools/lib/capitalization.py +++ b/tools/lib/capitalization.py @@ -164,6 +164,8 @@ IGNORED_PHRASES = [ r"does not apply to users who can delete any message", # Used as indicator with names for guest users. r"guest", + # Used as indicator with names for archived streams. + r"archived", # Used in pills for deactivated users. r"deactivated", # This is a reference to a setting/secret and should be lowercase. diff --git a/web/src/inbox_ui.ts b/web/src/inbox_ui.ts index e668caa5a3..78405978c3 100644 --- a/web/src/inbox_ui.ts +++ b/web/src/inbox_ui.ts @@ -71,6 +71,7 @@ const direct_message_context_properties: (keyof DirectMessageContext)[] = [ type StreamContext = { is_stream: boolean; + is_archived: boolean; invite_only: boolean; is_web_public: boolean; stream_name: string; @@ -372,6 +373,7 @@ function format_stream(stream_id: number): StreamContext { return { is_stream: true, + is_archived: stream_info.is_archived, invite_only: stream_info.invite_only, is_web_public: stream_info.is_web_public, stream_name: stream_info.name, diff --git a/web/src/message_list.ts b/web/src/message_list.ts index ab60d4d519..6b62a7a545 100644 --- a/web/src/message_list.ts +++ b/web/src/message_list.ts @@ -432,7 +432,7 @@ export class MessageList { const is_web_public = sub?.is_web_public; const can_toggle_subscription = sub !== undefined && stream_data.can_toggle_subscription(sub); - if (sub === undefined) { + if (sub === undefined || sub.is_archived) { deactivated = true; } else if (!subscribed && !this.last_message_historical) { just_unsubscribed = true; diff --git a/web/src/message_list_view.ts b/web/src/message_list_view.ts index 25b951ac48..c72e469e20 100644 --- a/web/src/message_list_view.ts +++ b/web/src/message_list_view.ts @@ -99,6 +99,7 @@ export type MessageGroup = { stream_name?: string; stream_privacy_icon_color: string; stream_url: string; + is_archived: boolean; subscribed?: boolean; topic: string; topic_is_resolved: boolean; @@ -468,6 +469,7 @@ function populate_group_from_message( const topic = message.topic; const match_topic = util.get_match_topic(message); const stream_url = hash_util.by_stream_url(message.stream_id); + const is_archived = stream_data.is_stream_archived(message.stream_id); const topic_url = hash_util.by_stream_topic_url(message.stream_id, message.topic); const sub = sub_store.get(message.stream_id); @@ -508,6 +510,7 @@ function populate_group_from_message( is_web_public, match_topic, stream_url, + is_archived, topic_url, stream_id, is_subscribed, diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index dcc5b28fa3..c1944fa358 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -28,6 +28,7 @@ import * as message_edit from "./message_edit"; import * as message_events from "./message_events"; import * as message_lists from "./message_lists"; import * as message_live_update from "./message_live_update"; +import * as message_view_header from "./message_view_header"; import * as muted_users_ui from "./muted_users_ui"; import * as narrow_state from "./narrow_state"; import * as narrow_title from "./narrow_title"; diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index 4e7a26d958..985d580b72 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -629,6 +629,11 @@ export function get_stream_privacy_policy(stream_id: number): string { return settings_config.stream_privacy_policy_values.private_with_public_history.code; } +export function is_stream_archived(stream_id: number): boolean { + const sub = sub_store.get(stream_id); + return sub ? sub.is_archived : false; +} + export function is_web_public(stream_id: number): boolean { const sub = sub_store.get(stream_id); return sub ? sub.is_web_public : false; diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index 59547706a4..ae27406cf5 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -716,6 +716,7 @@ /* Text colors */ --color-text-default: hsl(0deg 0% 20%); --color-text-message-default: hsl(0deg 0% 15%); + --color-text-message-header-archived: hsl(0deg 0% 50%); --color-text-message-view-header: hsl(0deg 0% 20% / 100%); --color-text-message-header: hsl(0deg 0% 15%); /* Light and dark mode both use the same hover color on diff --git a/web/styles/inbox.css b/web/styles/inbox.css index cd56924f12..49596c7ab7 100644 --- a/web/styles/inbox.css +++ b/web/styles/inbox.css @@ -479,6 +479,10 @@ } } } + + .inbox-header-stream-archived { + color: var(--color-text-message-header-archived); + } } #inbox-view { diff --git a/web/styles/message_header.css b/web/styles/message_header.css index 0c9686a108..84d832d957 100644 --- a/web/styles/message_header.css +++ b/web/styles/message_header.css @@ -210,6 +210,15 @@ } } } + + .message-header-stream-name { + overflow: hidden; + text-overflow: ellipsis; + } + + .message-header-stream-archived { + color: var(--color-text-message-header-archived); + } } .recipient_bar_controls { diff --git a/web/styles/message_view_header.css b/web/styles/message_view_header.css index d677bc1b99..8401e5b28c 100644 --- a/web/styles/message_view_header.css +++ b/web/styles/message_view_header.css @@ -66,6 +66,12 @@ text-overflow: ellipsis; } + .message-header-archived { + color: var(--color-text-message-header-archived); + cursor: default; + padding-left: 5px; + } + .narrow_description { /* Flexbox's baseline alignment is responsible for matching the description's baseline to the title. diff --git a/web/templates/inbox_view/inbox_stream_header_row.hbs b/web/templates/inbox_view/inbox_stream_header_row.hbs index 706034e9d7..779478298d 100644 --- a/web/templates/inbox_view/inbox_stream_header_row.hbs +++ b/web/templates/inbox_view/inbox_stream_header_row.hbs @@ -9,6 +9,11 @@ {{> ../stream_privacy }} {{stream_name}} + {{#if is_archived}} + + ({{t 'archived' }}) + + {{/if}}
                  {{> navbar_icon_and_title }} @@ -12,6 +13,11 @@ {{/unless}} +{{else}} + + {{> navbar_icon_and_title }} + +{{/unless}} {{#if rendered_narrow_description}} {{rendered_markdown rendered_narrow_description}} diff --git a/web/templates/navbar_icon_and_title.hbs b/web/templates/navbar_icon_and_title.hbs index 84491b41b7..3f1ba27085 100644 --- a/web/templates/navbar_icon_and_title.hbs +++ b/web/templates/navbar_icon_and_title.hbs @@ -4,3 +4,10 @@ {{/if}} {{title}} +{{#if stream}} + {{#if stream.is_archived}} + + ({{t 'archived' }}) + + {{/if}} +{{/if}} diff --git a/web/templates/recipient_row.hbs b/web/templates/recipient_row.hbs index f053a94e2c..eade9f3c76 100644 --- a/web/templates/recipient_row.hbs +++ b/web/templates/recipient_row.hbs @@ -10,7 +10,12 @@ {{~! Recipient (e.g. stream/topic or topic) ~}} - {{~display_recipient~}} + + {{~display_recipient~}} + + {{#if is_archived}} + ({{t 'archived' }}) + {{/if}} From a29b6485d65cce6254d2d683f93eaaee9d732c11 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Tue, 2 Jul 2024 00:49:22 +0530 Subject: [PATCH 078/276] delete_sub: Do not remove archived stream when deactivated. Stream is simply marked as `archived: true` instead of removing the stream from `sub_store` and `stream_info`. A check in `subscribe_myself` is added before subscribing to a stream. --- web/src/server_events_dispatch.js | 2 ++ web/src/stream_data.ts | 11 ++++++++--- web/tests/dispatch_subs.test.js | 12 ++++++++++++ web/tests/lib/events.js | 2 ++ web/tests/stream_data.test.js | 14 +++++++++++--- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index c1944fa358..bd6392d931 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -604,6 +604,7 @@ export function dispatch_normal_event(event) { ); stream_data.delete_sub(stream.stream_id); stream_settings_ui.remove_stream(stream.stream_id); + message_view_header.maybe_rerender_title_area_for_stream(stream); if (was_subscribed) { stream_list.remove_sidebar_row(stream.stream_id); if (stream.stream_id === compose_state.selected_recipient_id) { @@ -632,6 +633,7 @@ export function dispatch_normal_event(event) { message_lists.current.update_trailing_bookend(true); } } + message_live_update.rerender_messages_view(); stream_list.update_subscribe_to_more_streams_link(); break; default: diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index 985d580b72..9d220adb16 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -143,6 +143,10 @@ export function rename_sub(sub: StreamSubscription, new_name: string): void { } export function subscribe_myself(sub: StreamSubscription): void { + if (sub.is_archived) { + blueslip.warn("Can't subscribe to an archived stream."); + return; + } const user_id = people.my_current_user_id(); peer_data.add_subscriber(sub.stream_id, user_id); sub.subscribed = true; @@ -297,12 +301,13 @@ export function slug_to_stream_id(slug: string): number | undefined { } export function delete_sub(stream_id: number): void { - if (!stream_info.get(stream_id)) { + const sub = get_sub_by_id(stream_id); + if (sub === undefined || !stream_info.get(stream_id)) { blueslip.warn("Failed to archive stream " + stream_id.toString()); return; } - sub_store.delete_sub(stream_id); - stream_info.delete(stream_id); + sub.is_archived = true; + stream_info.set_false(stream_id, sub); } export function get_non_default_stream_names(): {name: string; unique_id: number}[] { diff --git a/web/tests/dispatch_subs.test.js b/web/tests/dispatch_subs.test.js index 4063a9ac59..52814359cb 100644 --- a/web/tests/dispatch_subs.test.js +++ b/web/tests/dispatch_subs.test.js @@ -13,6 +13,8 @@ const test_user = events.test_user; const compose_recipient = mock_esm("../src/compose_recipient"); const message_lists = mock_esm("../src/message_lists"); +const message_live_update = mock_esm("../src/message_live_update"); +const message_view_header = mock_esm("../src/message_view_header"); const narrow_state = mock_esm("../src/narrow_state"); const overlays = mock_esm("../src/overlays"); const settings_org = mock_esm("../src/settings_org"); @@ -223,6 +225,8 @@ test("stream delete (normal)", ({override}) => { removed_sidebar_rows += 1; }); override(stream_list, "update_subscribe_to_more_streams_link", noop); + override(message_live_update, "rerender_messages_view", noop); + override(message_view_header, "maybe_rerender_title_area_for_stream", noop); dispatch(event); @@ -239,9 +243,12 @@ test("stream delete (special streams)", ({override}) => { const event = event_fixtures.stream__delete; for (const stream of event.streams) { + stream.is_archived = false; stream_data.add_sub(stream); } + stream_data.subscribe_myself(event.streams[0]); + // sanity check data assert.equal(event.streams.length, 2); override(realm, "realm_new_stream_announcements_stream_id", event.streams[0].stream_id); @@ -254,6 +261,8 @@ test("stream delete (special streams)", ({override}) => { override(message_lists.current, "update_trailing_bookend", noop); override(stream_list, "remove_sidebar_row", noop); override(stream_list, "update_subscribe_to_more_streams_link", noop); + override(message_live_update, "rerender_messages_view", noop); + override(message_view_header, "maybe_rerender_title_area_for_stream", noop); dispatch(event); @@ -268,6 +277,7 @@ test("stream delete (stream is selected in compose)", ({override}) => { const event = event_fixtures.stream__delete; for (const stream of event.streams) { + stream.is_archived = false; stream_data.add_sub(stream); } @@ -294,6 +304,8 @@ test("stream delete (stream is selected in compose)", ({override}) => { removed_sidebar_rows += 1; }); override(stream_list, "update_subscribe_to_more_streams_link", noop); + override(message_live_update, "rerender_messages_view", noop); + override(message_view_header, "maybe_rerender_title_area_for_stream", noop); dispatch(event); diff --git a/web/tests/lib/events.js b/web/tests/lib/events.js index d0e2ef179d..21940c2662 100644 --- a/web/tests/lib/events.js +++ b/web/tests/lib/events.js @@ -40,6 +40,7 @@ const fake_now = 1596713966; exports.test_streams = { devel: { + is_archived: false, name: "devel", description: ":devel fun:", rendered_description: "devel fun", @@ -56,6 +57,7 @@ exports.test_streams = { can_remove_subscribers_group: 2, }, test: { + is_archived: false, name: "test", description: "test desc", rendered_description: "test desc", diff --git a/web/tests/stream_data.test.js b/web/tests/stream_data.test.js index b1cad1c581..ff5dc40647 100644 --- a/web/tests/stream_data.test.js +++ b/web/tests/stream_data.test.js @@ -579,24 +579,32 @@ test("default_stream_names", () => { test("delete_sub", () => { const canada = { + is_archived: false, stream_id: 101, name: "Canada", subscribed: true, }; stream_data.add_sub(canada); + const num_subscribed_subs = stream_data.num_subscribed_subs(); assert.ok(stream_data.is_subscribed(canada.stream_id)); assert.equal(stream_data.get_sub("Canada").stream_id, canada.stream_id); assert.equal(sub_store.get(canada.stream_id).name, "Canada"); + assert.equal(stream_data.is_stream_archived(canada.stream_id), false); stream_data.delete_sub(canada.stream_id); - assert.ok(!stream_data.is_subscribed(canada.stream_id)); - assert.ok(!stream_data.get_sub("Canada")); - assert.ok(!sub_store.get(canada.stream_id)); + assert.ok(stream_data.is_stream_archived(canada.stream_id)); + assert.ok(stream_data.is_subscribed(canada.stream_id)); + assert.ok(stream_data.get_sub("Canada")); + assert.ok(sub_store.get(canada.stream_id)); + assert.equal(stream_data.num_subscribed_subs(), num_subscribed_subs - 1); blueslip.expect("warn", "Failed to archive stream 99999"); stream_data.delete_sub(99999); + + blueslip.expect("warn", "Can't subscribe to an archived stream."); + stream_data.subscribe_myself(canada); }); test("notifications", ({override}) => { From 5ce2b307c4be4739b8d839570d211be2bd90346e Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Fri, 1 Mar 2024 11:30:56 +0530 Subject: [PATCH 079/276] stream_info: Remove now-unused function `delete`. Previously, when archiving a stream `delete` function was used to remove stream from `stream_info`. However, with the current approach, we don't remove stream instead we use the `set_false` function to mark streams as false, making the `delete` function unnecessary. --- web/src/stream_data.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index 9d220adb16..0d36f5f3f1 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -108,11 +108,6 @@ class BinaryDict { this.trues.delete(k); this.falses.set(k, v); } - - delete(k: number): void { - this.trues.delete(k); - this.falses.delete(k); - } } // The stream_info variable maps stream ids to stream properties objects From f257188ab514be0ec58384f59b53ec07307aea8a Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 29 Feb 2024 19:35:25 +0530 Subject: [PATCH 080/276] sub_store: Remove unused function `delete_sub`. Function `delete_sub` was exclusively called by `stream_data.delete_sub`. With the change in the approach where we no longer remove subscriptions from `stream_info` and `subs_by_stream_id`, the `delete_sub` function is no longer needed. --- web/src/sub_store.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/src/sub_store.ts b/web/src/sub_store.ts index 4a7d388ef4..9479f702a7 100644 --- a/web/src/sub_store.ts +++ b/web/src/sub_store.ts @@ -60,10 +60,6 @@ export function clear(): void { subs_by_stream_id.clear(); } -export function delete_sub(stream_id: number): void { - subs_by_stream_id.delete(stream_id); -} - export function add_hydrated_sub(stream_id: number, sub: StreamSubscription): void { // The only code that should call this directly is // in stream_data.js. Grep there to find callers. From 7e97358c353723644a5f3aeca2370034f48df85e Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 29 Feb 2024 12:42:17 +0530 Subject: [PATCH 081/276] home: Update `client_capabilities` for viewing of archived streams. --- zerver/lib/home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zerver/lib/home.py b/zerver/lib/home.py index 8a1c650408..57eec0bc00 100644 --- a/zerver/lib/home.py +++ b/zerver/lib/home.py @@ -156,7 +156,7 @@ def build_page_params_for_home_page_load( linkifier_url_template=True, user_list_incomplete=True, include_deactivated_groups=True, - archived_channels=False, + archived_channels=True, ) if user_profile is not None: From 4b90741f914c3bf266641429f7030a2c13aa6d54 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Fri, 1 Mar 2024 12:14:18 +0530 Subject: [PATCH 082/276] can_post_messages_in_stream: Add condition check for archived streams. Prevents users from sending any message in an archived stream. --- web/src/stream_data.ts | 4 ++++ web/tests/stream_data.test.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index 0d36f5f3f1..a2683705b9 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -568,6 +568,10 @@ export function can_unsubscribe_others(sub: StreamSubscription): boolean { } export function can_post_messages_in_stream(stream: StreamSubscription): boolean { + if (stream.is_archived) { + return false; + } + if (page_params.is_spectator) { return false; } diff --git a/web/tests/stream_data.test.js b/web/tests/stream_data.test.js index ff5dc40647..cdd9fdeed7 100644 --- a/web/tests/stream_data.test.js +++ b/web/tests/stream_data.test.js @@ -1116,6 +1116,9 @@ test("can_post_messages_in_stream", ({override}) => { page_params.is_spectator = true; assert.equal(stream_data.can_post_messages_in_stream(social), false); + + social.is_archived = true; + assert.equal(stream_data.can_post_messages_in_stream(social), false); }); test("can_unsubscribe_others", ({override}) => { From dbc90ba2e57bee5a81d2ce7ebd967e42d54e1dc9 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 26 Sep 2024 11:50:41 +0530 Subject: [PATCH 083/276] stream_list: Prevent archived channels to show up in left sidebar. --- web/src/stream_list.ts | 5 +++-- web/src/stream_list_sort.ts | 3 +++ web/tests/stream_list_sort.test.js | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/web/src/stream_list.ts b/web/src/stream_list.ts index 2356fb794b..e1fd2ac8a5 100644 --- a/web/src/stream_list.ts +++ b/web/src/stream_list.ts @@ -227,7 +227,8 @@ export function create_initial_sidebar_rows(): void { // This code is slightly opaque, but it ends up building // up list items and attaching them to the "sub" data // structures that are kept in stream_data.js. - const subs = stream_data.subscribed_subs(); + let subs = stream_data.subscribed_subs(); + subs = subs.filter((sub) => !sub.is_archived); for (const sub of subs) { create_sidebar_row(sub); @@ -689,7 +690,7 @@ export function get_sidebar_stream_topic_info(filter: Filter): { return result; } - if (!stream_data.is_subscribed(stream_id)) { + if (!stream_data.is_subscribed(stream_id) || stream_data.is_stream_archived(stream_id)) { return result; } diff --git a/web/src/stream_list_sort.ts b/web/src/stream_list_sort.ts index 50c2e7f1d1..2b3bad124a 100644 --- a/web/src/stream_list_sort.ts +++ b/web/src/stream_list_sort.ts @@ -110,6 +110,9 @@ export function sort_groups(stream_ids: number[], search_term: string): StreamLi const sub = sub_store.get(stream_id); assert(sub); const pinned = sub.pin_to_top; + if (sub.is_archived) { + continue; + } if (pinned) { if (!sub.is_muted) { pinned_streams.push(stream_id); diff --git a/web/tests/stream_list_sort.test.js b/web/tests/stream_list_sort.test.js index 5da734e76b..305647c8a4 100644 --- a/web/tests/stream_list_sort.test.js +++ b/web/tests/stream_list_sort.test.js @@ -77,6 +77,13 @@ const muted_pinned = { pin_to_top: true, is_muted: true, }; +const archived = { + subscribed: true, + name: "archived channel", + stream_id: 9, + pin_to_top: true, + is_archived: true, +}; function sort_groups(query) { const streams = stream_data.subscribed_stream_ids(); @@ -112,6 +119,7 @@ test("basics", ({override_rewire}) => { stream_data.add_sub(stream_hyphen_underscore_slash_colon); stream_data.add_sub(muted_active); stream_data.add_sub(muted_pinned); + stream_data.add_sub(archived); override_rewire(stream_list_sort, "has_recent_activity", (sub) => sub.name !== "pneumonia"); From e8bb9e2de1aab651cdb524ed68ef38a99f2cb903 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Thu, 26 Sep 2024 11:56:25 +0530 Subject: [PATCH 084/276] get_invite_stream_data: Do not display archived channels. User should not be able to sent invite for archived channels. --- web/src/stream_data.ts | 2 +- web/src/stream_pill.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index a2683705b9..682ae78e0e 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -378,7 +378,7 @@ export function get_invite_stream_data(): StreamSubscription[] { const streams = []; const all_subs = get_unsorted_subs(); for (const sub of all_subs) { - if (can_subscribe_others(sub)) { + if (!sub.is_archived && can_subscribe_others(sub)) { streams.push(sub); } } diff --git a/web/src/stream_pill.ts b/web/src/stream_pill.ts index 25f33d36d6..be14111380 100644 --- a/web/src/stream_pill.ts +++ b/web/src/stream_pill.ts @@ -134,7 +134,10 @@ export function typeahead_source( const potential_streams = invite_streams ? stream_data.get_invite_stream_data() : stream_data.get_unsorted_subs(); - return filter_taken_streams(potential_streams, pill_widget).map((stream) => ({ + + const active_streams = potential_streams.filter((sub) => !sub.is_archived); + + return filter_taken_streams(active_streams, pill_widget).map((stream) => ({ ...stream, type: "stream", })); From 5dfa8ac9fe6cf1303a40ff89e679ddcc8bc235fa Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Fri, 27 Sep 2024 18:03:13 +0530 Subject: [PATCH 085/276] popover_menus_data: Hide actions user can't take for archived channels. --- web/src/message_edit.ts | 12 ++++++++++++ web/src/popover_menus_data.ts | 5 ++++- web/styles/reactions.css | 10 ++++++++++ web/templates/message_controls.hbs | 18 ++++++++++-------- web/templates/message_group.hbs | 2 +- web/templates/message_reaction.hbs | 20 +++++++++++--------- web/templates/message_reactions.hbs | 14 ++++++++------ web/tests/popover_menus_data.test.js | 1 + 8 files changed, 57 insertions(+), 25 deletions(-) diff --git a/web/src/message_edit.ts b/web/src/message_edit.ts index 5f2637755a..5e4b77e304 100644 --- a/web/src/message_edit.ts +++ b/web/src/message_edit.ts @@ -102,6 +102,10 @@ export function is_topic_editable(message: Message, edit_limit_seconds_buffer = return false; } + if (message.type === "stream" && stream_data.is_stream_archived(message.stream_id)) { + return false; + } + if (!settings_data.user_can_move_messages_to_another_topic()) { return false; } @@ -206,6 +210,10 @@ export function is_message_sent_by_my_bot(message: Message): boolean { } export function get_deletability(message: Message): boolean { + if (message.type === "stream" && stream_data.is_stream_archived(message.stream_id)) { + return false; + } + if (settings_data.user_can_delete_any_message()) { return true; } @@ -243,6 +251,10 @@ export function is_stream_editable(message: Message, edit_limit_seconds_buffer = return false; } + if (message.type === "stream" && stream_data.is_stream_archived(message.stream_id)) { + return false; + } + if (!settings_data.user_can_move_messages_between_streams()) { return false; } diff --git a/web/src/popover_menus_data.ts b/web/src/popover_menus_data.ts index d33e22737a..5774796cab 100644 --- a/web/src/popover_menus_data.ts +++ b/web/src/popover_menus_data.ts @@ -206,7 +206,10 @@ export function get_actions_popover_content_context(message_id: number): ActionP // `media_breakpoints.sm_min`, we need to include the reaction button in the // popover if it is not displayed. const should_display_add_reaction_option = - !message.is_me_message && !is_add_reaction_icon_visible() && not_spectator; + !message.is_me_message && + !is_add_reaction_icon_visible() && + not_spectator && + !(stream_id && stream_data.is_stream_archived(stream_id)); return { message_id: message.id, diff --git a/web/styles/reactions.css b/web/styles/reactions.css index 9cdf169cf3..c0cc0a834d 100644 --- a/web/styles/reactions.css +++ b/web/styles/reactions.css @@ -6,6 +6,12 @@ margin-bottom: var(--message-box-markdown-aligned-vertical-space); } + .message_reaction_container { + &.disabled { + cursor: not-allowed; + } + } + .message_reaction { display: flex; /* Set a pixel and half padding to maintain @@ -39,6 +45,10 @@ box-shadow: none; } + &.disabled { + pointer-events: none; + } + &:hover { background-color: var(--color-message-reaction-background-hover); } diff --git a/web/templates/message_controls.hbs b/web/templates/message_controls.hbs index 1efbfbaf38..02ac760a68 100644 --- a/web/templates/message_controls.hbs +++ b/web/templates/message_controls.hbs @@ -1,13 +1,15 @@ -{{#if msg/sent_by_me}} -
                  -{{/if}} +{{#unless is_archived}} + {{#if msg/sent_by_me}} +
                  + {{/if}} -{{#unless msg/sent_by_me}} -
                  -
                  - + {{#unless msg/sent_by_me}} +
                  +
                  + +
                  -
                  + {{/unless}} {{/unless}}
                  diff --git a/web/templates/message_group.hbs b/web/templates/message_group.hbs index 99cc243af2..a29fd19ee8 100644 --- a/web/templates/message_group.hbs +++ b/web/templates/message_group.hbs @@ -10,7 +10,7 @@ {{> recipient_row use_match_properties=../use_match_properties}} {{#each message_containers}} {{#with this}} - {{> single_message use_match_properties=../../use_match_properties message_list_id=../../message_list_id}} + {{> single_message use_match_properties=../../use_match_properties message_list_id=../../message_list_id is_archived=../is_archived}} {{/with}} {{/each}}
                  diff --git a/web/templates/message_reaction.hbs b/web/templates/message_reaction.hbs index 91e9b20878..6df258a093 100644 --- a/web/templates/message_reaction.hbs +++ b/web/templates/message_reaction.hbs @@ -1,10 +1,12 @@ -
                  - {{#if this.emoji_alt_code}} -
                   :{{this.emoji_name}}:
                  - {{else if this.is_realm_emoji}} - - {{else}} -
                  - {{/if}} -
                  {{this.vote_text}}
                  +
                  +
                  + {{#if this.emoji_alt_code}} +
                   :{{this.emoji_name}}:
                  + {{else if this.is_realm_emoji}} + + {{else}} +
                  + {{/if}} +
                  {{this.vote_text}}
                  +
                  diff --git a/web/templates/message_reactions.hbs b/web/templates/message_reactions.hbs index 7828520d98..c1ab44c917 100644 --- a/web/templates/message_reactions.hbs +++ b/web/templates/message_reactions.hbs @@ -1,11 +1,13 @@
                  {{#each this/msg/message_reactions}} - {{> message_reaction}} + {{> message_reaction is_archived=../is_archived}} {{/each}} -
                  -
                  - -
                  +
                  + {{#unless is_archived}} +
                  +
                  + +
                  +
                  +
                  -
                  + {{/unless}}
                  diff --git a/web/tests/popover_menus_data.test.js b/web/tests/popover_menus_data.test.js index 346c3b61c7..61e25a3514 100644 --- a/web/tests/popover_menus_data.test.js +++ b/web/tests/popover_menus_data.test.js @@ -46,6 +46,7 @@ mock_esm("../src/hash_util", { }); mock_esm("../src/stream_data", { is_subscribed: () => true, + is_stream_archived: () => false, }); mock_esm("../src/group_permission_settings", { get_group_permission_setting_config() { From e60711f871f1b0fedbb4e80c60b422cb3d706e5d Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Tue, 14 May 2024 19:27:11 +0530 Subject: [PATCH 086/276] stream_create: Add functionality to rename archived streams. Users with appropriate permissions will now have the option to rename archived streams. --- web/src/stream_create.ts | 116 +++++++++++++++--- web/src/stream_settings_ui.js | 3 + web/src/ui_init.js | 2 + .../stream_settings/stream_creation_form.hbs | 1 + 4 files changed, 108 insertions(+), 14 deletions(-) diff --git a/web/src/stream_create.ts b/web/src/stream_create.ts index 2f58be4d7d..89a26bd3fb 100644 --- a/web/src/stream_create.ts +++ b/web/src/stream_create.ts @@ -3,9 +3,11 @@ import assert from "minimalistic-assert"; import {z} from "zod"; import render_subscription_invites_warning_modal from "../templates/confirm_dialog/confirm_subscription_invites_warning.hbs"; +import render_change_stream_info_modal from "../templates/stream_settings/change_stream_info_modal.hbs"; import * as channel from "./channel"; import * as confirm_dialog from "./confirm_dialog"; +import * as dialog_widget from "./dialog_widget"; import {$t, $t_html} from "./i18n"; import * as keydown_util from "./keydown_util"; import * as loading from "./loading"; @@ -16,6 +18,7 @@ import {current_user, realm} from "./state_data"; import * as stream_create_subscribers from "./stream_create_subscribers"; import * as stream_data from "./stream_data"; import * as stream_settings_components from "./stream_settings_components"; +import * as stream_settings_data from "./stream_settings_data"; import * as stream_ui_updates from "./stream_ui_updates"; import type {HTMLSelectOneElement} from "./types"; import * as ui_report from "./ui_report"; @@ -58,6 +61,12 @@ export function should_show_first_stream_created_modal(): boolean { return onboarding_steps.ONE_TIME_NOTICES_TO_DISPLAY.has("first_stream_created_banner"); } +export function maybe_update_error_message(): void { + if ($("#stream_name_error").is(":visible") && $("#archived_stream_rename").is(":visible")) { + $("#create_stream_name").trigger("input"); + } +} + class StreamSubscriptionError { report_no_subs_to_stream(): void { $("#stream_subscription_error").text( @@ -83,15 +92,16 @@ class StreamSubscriptionError { const stream_subscription_error = new StreamSubscriptionError(); class StreamNameError { - report_already_exists(): void { - $("#stream_name_error").text( - $t({defaultMessage: "A channel with this name already exists."}), - ); + report_already_exists(error?: string): void { + const error_message = + error ?? $t({defaultMessage: "A channel with this name already exists."}); + $("#stream_name_error").text(error_message); $("#stream_name_error").show(); } clear_errors(): void { $("#stream_name_error").hide(); + $("#archived_stream_rename").hide(); } report_empty_stream(): void { @@ -103,6 +113,12 @@ class StreamNameError { $("#create_stream_name").trigger("focus").trigger("select"); } + rename_archived_stream(stream_id: number): void { + $("#archived_stream_rename").text($t({defaultMessage: "Rename archived channel"})); + $("#archived_stream_rename").attr("data-stream-id", stream_id); + $("#archived_stream_rename").show(); + } + pre_validate(stream_name: string): void { // Don't worry about empty strings...we just want to call this // to warn users early before they start doing too much work @@ -111,8 +127,16 @@ class StreamNameError { // out it already exists, and I was just too lazy to look at // the public streams that I'm not subscribed to yet. Once I // realize the stream already exists, I may want to cancel.) - if (stream_name && stream_data.get_sub(stream_name)) { - this.report_already_exists(); + const stream = stream_data.get_sub(stream_name); + if (stream_name && stream) { + let error; + if (stream.is_archived) { + error = $t({defaultMessage: "An archived channel with this name already exists."}); + if (stream_settings_data.get_sub_for_settings(stream).can_change_name_description) { + this.rename_archived_stream(stream.stream_id); + } + } + this.report_already_exists(error); return; } @@ -126,8 +150,13 @@ class StreamNameError { return false; } - if (stream_data.get_sub(stream_name)) { - this.report_already_exists(); + const stream = stream_data.get_sub(stream_name); + if (stream) { + let error; + if (stream.is_archived) { + error = $t({defaultMessage: "An archived channel with this name already exists."}); + } + this.report_already_exists(error); this.select(); return false; } @@ -380,13 +409,19 @@ function create_stream(): void { // "Error creating channel"? stream_name_error.report_already_exists(); stream_name_error.select(); - } + const message = $t_html({ + defaultMessage: + "Error creating channel: A channel with this name already exists.", + }); - ui_report.error( - $t_html({defaultMessage: "Error creating channel"}), - xhr, - $(".stream_create_info"), - ); + ui_report.error(message, undefined, $(".stream_create_info")); + } else { + ui_report.error( + $t_html({defaultMessage: "Error creating channel"}), + xhr, + $(".stream_create_info"), + ); + } loading.destroy_indicator($("#stream_creating_indicator")); }, }); @@ -548,3 +583,56 @@ export function set_up_handlers(): void { assert(stream_settings_components.new_stream_can_remove_subscribers_group_widget !== null); stream_settings_components.new_stream_can_remove_subscribers_group_widget.setup(); } + +export function initialize(): void { + $("#channels_overlay_container").on("click", "#archived_stream_rename", (e) => { + e.preventDefault(); + e.stopPropagation(); + const stream_id = Number.parseInt($("#archived_stream_rename").attr("data-stream-id")!, 10); + const stream = stream_data.get_sub_by_id(stream_id); + + assert(stream !== undefined); + + const template_data = { + stream_name: stream.name, + stream_description: stream.description, + max_stream_name_length: realm.max_stream_name_length, + max_stream_description_length: realm.max_stream_description_length, + }; + const change_stream_info_modal = render_change_stream_info_modal(template_data); + dialog_widget.launch({ + html_heading: $t_html( + {defaultMessage: "Edit #{stream_name} (archived)"}, + {stream_name: stream.name}, + ), + html_body: change_stream_info_modal, + id: "change_stream_info_modal", + loading_spinner: true, + on_click: save_stream_info, + post_render() { + $("#change_stream_info_modal .dialog_submit_button") + .addClass("save-button") + .attr("data-stream-id", stream_id); + }, + update_submit_disabled_state_on_change: true, + }); + }); + + function save_stream_info(): void { + const stream_id = Number.parseInt($("#archived_stream_rename").attr("data-stream-id")!, 10); + const sub = stream_data.get_sub_by_id(stream_id); + const url = `/json/streams/${sub?.stream_id}`; + const data: {new_name?: string; description?: string} = {}; + const new_name = $("#change_stream_name").val()!.trim(); + const new_description = $("#change_stream_description").val()!.trim(); + + if (new_name !== sub?.name) { + data.new_name = new_name; + } + if (new_description !== sub?.description) { + data.description = new_description; + } + + dialog_widget.submit_api_request(channel.patch, url, data); + } +} diff --git a/web/src/stream_settings_ui.js b/web/src/stream_settings_ui.js index 39b8b3d739..1d4ff231ab 100644 --- a/web/src/stream_settings_ui.js +++ b/web/src/stream_settings_ui.js @@ -124,6 +124,9 @@ export function update_stream_name(sub, new_name) { // Update navbar if needed message_view_header.maybe_rerender_title_area_for_stream(sub); + + // Update the create stream error if needed + stream_create.maybe_update_error_message(); } export function update_stream_description(sub, description, rendered_description) { diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 3a001a8b26..d14fafa093 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -117,6 +117,7 @@ import * as starred_messages from "./starred_messages"; import * as starred_messages_ui from "./starred_messages_ui"; import {current_user, realm, set_current_user, set_realm, state_data_schema} from "./state_data"; import * as stream_card_popover from "./stream_card_popover"; +import * as stream_create from "./stream_create"; import * as stream_data from "./stream_data"; import * as stream_edit from "./stream_edit"; import * as stream_edit_subscribers from "./stream_edit_subscribers"; @@ -533,6 +534,7 @@ export function initialize_everything(state_data) { on_send_message_success: compose.send_message_success, send_message: transmit.send_message, }); + stream_create.initialize(); stream_edit.initialize(); user_group_edit.initialize(); stream_edit_subscribers.initialize(); diff --git a/web/templates/stream_settings/stream_creation_form.hbs b/web/templates/stream_settings/stream_creation_form.hbs index dba3614862..7b2bc37e3e 100644 --- a/web/templates/stream_settings/stream_creation_form.hbs +++ b/web/templates/stream_settings/stream_creation_form.hbs @@ -13,6 +13,7 @@
                  +
                  - {{> ../dropdown_widget_with_label - widget_name="realm_create_multiuse_invite_group" - label=(t 'Who can create reusable invitation links') - value_type="number"}} +
                  + +
                  +
                  + {{~! Squash whitespace so that placeholder is displayed when empty. ~}} +
                  +
                  +
                  diff --git a/web/tests/settings_org.test.js b/web/tests/settings_org.test.js index d38e2e6134..2d17074d19 100644 --- a/web/tests/settings_org.test.js +++ b/web/tests/settings_org.test.js @@ -529,6 +529,7 @@ test("set_up", ({override, override_rewire}) => { }; override_rewire(settings_org, "init_dropdown_widgets", noop); + override_rewire(settings_org, "initialize_group_setting_widgets", noop); $("#id_realm_message_content_edit_limit_minutes").set_parent( $.create(""), ); diff --git a/web/tests/user_events.test.js b/web/tests/user_events.test.js index 78c1af996e..c4cc9f0e7d 100644 --- a/web/tests/user_events.test.js +++ b/web/tests/user_events.test.js @@ -43,6 +43,7 @@ mock_esm("../src/settings_linkifiers", { }); mock_esm("../src/settings_org", { maybe_disable_widgets() {}, + enable_or_disable_group_permission_settings() {}, }); mock_esm("../src/settings_profile_fields", { maybe_disable_widgets() {}, From c8e906d49e83023a3d63a6e24e820840b7cb24b3 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Mon, 28 Oct 2024 16:39:28 +0530 Subject: [PATCH 112/276] settings: Use pills UI for channel creation settings. We now use new pills UI for public and private channel creation settings. The UI for web-public channel creation setting is still a dropdown as we allow only system groups for that. --- web/src/settings_components.ts | 13 +++--- web/src/settings_org.ts | 44 +++++++++++++++++-- web/src/state_data.ts | 4 +- .../organization_permissions_admin.hbs | 26 +++++++---- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index cc417a3ce8..97498c6d96 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -481,8 +481,6 @@ const dropdown_widget_map = new Map([ ["realm_can_access_all_users_group", null], ["realm_can_add_custom_emoji_group", null], ["realm_can_create_groups", null], - ["realm_can_create_public_channel_group", null], - ["realm_can_create_private_channel_group", null], ["realm_can_create_web_public_channel_group", null], ["realm_can_delete_any_message_group", null], ["realm_can_delete_own_message_group", null], @@ -803,8 +801,6 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_can_access_all_users_group": case "realm_can_add_custom_emoji_group": case "realm_can_create_groups": - case "realm_can_create_public_channel_group": - case "realm_can_create_private_channel_group": case "realm_can_create_web_public_channel_group": case "realm_can_delete_any_message_group": case "realm_can_delete_own_message_group": @@ -814,6 +810,8 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_direct_message_permission_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; + case "realm_can_create_public_channel_group": + case "realm_can_create_private_channel_group": case "realm_create_multiuse_invite_group": { const pill_widget = get_group_setting_widget(property_name); assert(pill_widget !== null); @@ -1430,6 +1428,8 @@ export const group_setting_widget_map = new Map @@ -1153,6 +1183,14 @@ function initialize_group_setting_widgets(): void { $pill_container: $("#id_realm_create_multiuse_invite_group"), setting_name: "create_multiuse_invite_group", }); + settings_components.create_realm_group_setting_widget({ + $pill_container: $("#id_realm_can_create_public_channel_group"), + setting_name: "can_create_public_channel_group", + }); + settings_components.create_realm_group_setting_widget({ + $pill_container: $("#id_realm_can_create_private_channel_group"), + setting_name: "can_create_private_channel_group", + }); enable_or_disable_group_permission_settings(); } diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 5caffb14fc..58771f2463 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -289,8 +289,8 @@ export const realm_schema = z.object({ realm_can_access_all_users_group: z.number(), realm_can_add_custom_emoji_group: z.number(), realm_can_create_groups: z.number(), - realm_can_create_public_channel_group: z.number(), - realm_can_create_private_channel_group: z.number(), + realm_can_create_public_channel_group: group_setting_value_schema, + realm_can_create_private_channel_group: group_setting_value_schema, realm_can_create_web_public_channel_group: z.number(), realm_can_delete_any_message_group: z.number(), realm_can_delete_own_message_group: z.number(), diff --git a/web/templates/settings/organization_permissions_admin.hbs b/web/templates/settings/organization_permissions_admin.hbs index ad9c63df0b..ed4facbff2 100644 --- a/web/templates/settings/organization_permissions_admin.hbs +++ b/web/templates/settings/organization_permissions_admin.hbs @@ -73,10 +73,15 @@ {{> settings_save_discard_widget section_name="stream-permissions" }}
                  - {{> ../dropdown_widget_with_label - widget_name="realm_can_create_public_channel_group" - label=(t 'Who can create public channels') - value_type="number"}} +
                  + +
                  +
                  + {{~! Squash whitespace so that placeholder is displayed when empty. ~}} +
                  +
                  +
                  {{> upgrade_tip_widget }} {{> settings_checkbox @@ -92,10 +97,15 @@ label=(t 'Who can create web-public channels') value_type="number"}} - {{> ../dropdown_widget_with_label - widget_name="realm_can_create_private_channel_group" - label=(t 'Who can create private channels') - value_type="number"}} +
                  + +
                  +
                  + {{~! Squash whitespace so that placeholder is displayed when empty. ~}} +
                  +
                  +
                  From 8fa225d885396878d7d3ad4904eb3f0a977ec8b4 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Mon, 28 Oct 2024 19:36:21 +0530 Subject: [PATCH 113/276] settings: Use new pills UI for group related realm settings. This commit updates the code to use new UI for can_create_groups and can_manage_all_groups settings. --- web/src/settings_components.ts | 10 ++++--- web/src/settings_org.ts | 23 ++++++++++++--- web/src/state_data.ts | 4 +-- web/src/user_events.js | 1 + .../organization_permissions_admin.hbs | 28 ++++++++++++------- 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index 97498c6d96..79a684611b 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -480,11 +480,9 @@ const dropdown_widget_map = new Map([ ["can_remove_subscribers_group", null], ["realm_can_access_all_users_group", null], ["realm_can_add_custom_emoji_group", null], - ["realm_can_create_groups", null], ["realm_can_create_web_public_channel_group", null], ["realm_can_delete_any_message_group", null], ["realm_can_delete_own_message_group", null], - ["realm_can_manage_all_groups", null], ["realm_can_move_messages_between_channels_group", null], ["realm_direct_message_initiator_group", null], ["realm_direct_message_permission_group", null], @@ -800,18 +798,18 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_default_code_block_language": case "realm_can_access_all_users_group": case "realm_can_add_custom_emoji_group": - case "realm_can_create_groups": case "realm_can_create_web_public_channel_group": case "realm_can_delete_any_message_group": case "realm_can_delete_own_message_group": - case "realm_can_manage_all_groups": case "realm_can_move_messages_between_channels_group": case "realm_direct_message_initiator_group": case "realm_direct_message_permission_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; + case "realm_can_create_groups": case "realm_can_create_public_channel_group": case "realm_can_create_private_channel_group": + case "realm_can_manage_all_groups": case "realm_create_multiuse_invite_group": { const pill_widget = get_group_setting_widget(property_name); assert(pill_widget !== null); @@ -1428,8 +1426,10 @@ export const group_setting_widget_map = new Map
                  - {{> ../dropdown_widget_with_label - widget_name="realm_can_create_groups" - label=(t 'Who can create user groups') - value_type="number" - is_setting_disabled=(not is_owner)}} +
                  + +
                  +
                  + {{~! Squash whitespace so that placeholder is displayed when empty. ~}} +
                  +
                  +
                  - {{> ../dropdown_widget_with_label - widget_name="realm_can_manage_all_groups" - label=(t 'Who can manage user groups') - value_type="number" - is_setting_disabled=(not is_owner)}} +
                  + +
                  +
                  + {{~! Squash whitespace so that placeholder is displayed when empty. ~}} +
                  +
                  +
                  {{> ../dropdown_widget_with_label widget_name="realm_can_add_custom_emoji_group" From f29083f2ef4dfe60eb1655317a7d9dd9f61639be Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Tue, 29 Oct 2024 15:31:43 +0530 Subject: [PATCH 114/276] settings: Disable save button if nobody group is not allowed. This commit adds code to disable the save button if there no pills selected for the setting and the setting cannot be set to "Nobody" group. --- web/src/settings_components.ts | 43 ++++++++++++++++++++++++++++++++++ web/tests/settings_org.test.js | 1 + 2 files changed, 44 insertions(+) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index 79a684611b..ac53b7c897 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -1353,6 +1353,40 @@ function should_disable_save_button_for_time_limit_settings( return disable_save_btn; } +function should_disable_save_button_for_group_settings(settings: string[]): boolean { + for (const setting_name of settings) { + let group_setting_config; + if (setting_name.startsWith("realm_")) { + const setting_name_without_prefix = /^realm_(.*)$/.exec(setting_name)![1]!; + group_setting_config = group_permission_settings.get_group_permission_setting_config( + setting_name_without_prefix, + "realm", + ); + } else { + // We do not have any stream settings using the new UI currently, + // so we know that this block will be called for group setting only. + group_setting_config = group_permission_settings.get_group_permission_setting_config( + setting_name, + "group", + ); + } + assert(group_setting_config !== undefined); + if (group_setting_config.allow_nobody_group) { + continue; + } + + const pill_widget = get_group_setting_widget(setting_name); + assert(pill_widget !== null); + + const setting_value = get_group_setting_widget_value(pill_widget); + const nobody_group = user_groups.get_user_group_from_name("role:nobody")!; + if (setting_value === nobody_group.id) { + return true; + } + } + return false; +} + function enable_or_disable_save_button($subsection_elem: JQuery): void { const time_limit_settings = [...$subsection_elem.find(".time-limit-setting")]; @@ -1382,6 +1416,15 @@ function enable_or_disable_save_button($subsection_elem: JQuery): void { } } + if (!disable_save_btn) { + const group_settings = [...$subsection_elem.find(".pill-container")].map((elem) => + extract_property_name($(elem)), + ); + if (group_settings.length) { + disable_save_btn = should_disable_save_button_for_group_settings(group_settings); + } + } + $subsection_elem.find(".subsection-changes-save button").prop("disabled", disable_save_btn); } diff --git a/web/tests/settings_org.test.js b/web/tests/settings_org.test.js index 2d17074d19..26c58b235d 100644 --- a/web/tests/settings_org.test.js +++ b/web/tests/settings_org.test.js @@ -86,6 +86,7 @@ function createSaveButtons(subsection) { $save_button_controls.closest = () => $stub_save_button_header; $stub_save_button_header.set_find_results(".time-limit-setting", []); + $stub_save_button_header.set_find_results(".pill-container", []); $stub_save_button_header.set_find_results(".subsection-changes-save button", $stub_save_button); return { From ec1b265ff80cffb30a8e8eb412a912e731dd1d23 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 28 Oct 2024 16:32:25 -0700 Subject: [PATCH 115/276] buddy_list: Refactor section toggle to not need custom classname. --- web/src/buddy_list.ts | 15 ++++++--------- web/templates/buddy_list/section_header.hbs | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/web/src/buddy_list.ts b/web/src/buddy_list.ts index 7cc2b9f240..ad50be7dac 100644 --- a/web/src/buddy_list.ts +++ b/web/src/buddy_list.ts @@ -452,7 +452,6 @@ export class BuddyList extends BuddyListConf { id: "buddy-list-participants-section-heading", header_text: $t({defaultMessage: "In this conversation"}), user_count: get_formatted_sub_count(this.participant_user_ids.length), - toggle_class: "toggle-participants", is_collapsed: this.participants_is_collapsed, }), ), @@ -466,7 +465,6 @@ export class BuddyList extends BuddyListConf { user_count: get_formatted_sub_count( total_human_subscribers_count - this.participant_user_ids.length, ), - toggle_class: "toggle-users-matching-view", is_collapsed: this.users_matching_view_is_collapsed, }), ), @@ -478,7 +476,6 @@ export class BuddyList extends BuddyListConf { id: "buddy-list-other-users-section-heading", header_text: $t({defaultMessage: "Others"}), user_count: get_formatted_sub_count(other_users_count), - toggle_class: "toggle-other-users", is_collapsed: this.other_users_is_collapsed, }), ), @@ -491,11 +488,11 @@ export class BuddyList extends BuddyListConf { "collapsed", this.participants_is_collapsed, ); - $("#buddy-list-participants-container .toggle-participants").toggleClass( + $("#buddy-list-participants-container .buddy-list-section-toggle").toggleClass( "rotate-icon-down", !this.participants_is_collapsed, ); - $("#buddy-list-participants-container .toggle-participants").toggleClass( + $("#buddy-list-participants-container .buddy-list-section-toggle").toggleClass( "rotate-icon-right", this.participants_is_collapsed, ); @@ -511,11 +508,11 @@ export class BuddyList extends BuddyListConf { "collapsed", this.users_matching_view_is_collapsed, ); - $("#buddy-list-users-matching-view-container .toggle-users-matching-view").toggleClass( + $("#buddy-list-users-matching-view-container .buddy-list-section-toggle").toggleClass( "rotate-icon-down", !this.users_matching_view_is_collapsed, ); - $("#buddy-list-users-matching-view-container .toggle-users-matching-view").toggleClass( + $("#buddy-list-users-matching-view-container .buddy-list-section-toggle").toggleClass( "rotate-icon-right", this.users_matching_view_is_collapsed, ); @@ -531,11 +528,11 @@ export class BuddyList extends BuddyListConf { "collapsed", this.other_users_is_collapsed, ); - $("#buddy-list-other-users-container .toggle-other-users").toggleClass( + $("#buddy-list-other-users-container .buddy-list-section-toggle").toggleClass( "rotate-icon-down", !this.other_users_is_collapsed, ); - $("#buddy-list-other-users-container .toggle-other-users").toggleClass( + $("#buddy-list-other-users-container .buddy-list-section-toggle").toggleClass( "rotate-icon-right", this.other_users_is_collapsed, ); diff --git a/web/templates/buddy_list/section_header.hbs b/web/templates/buddy_list/section_header.hbs index 6c3a0db8e3..8dabbd1ac9 100644 --- a/web/templates/buddy_list/section_header.hbs +++ b/web/templates/buddy_list/section_header.hbs @@ -1,4 +1,4 @@
                  {{header_text}} ({{user_count}})
                  - + From b335c19d1c98a4da0baea0b6c3e83a3ac529ad02 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 28 Oct 2024 16:43:53 -0700 Subject: [PATCH 116/276] buddy_list: Refactor section collapse logic to be shared. --- web/src/buddy_list.ts | 68 +++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/web/src/buddy_list.ts b/web/src/buddy_list.ts index ad50be7dac..47fdbc6950 100644 --- a/web/src/buddy_list.ts +++ b/web/src/buddy_list.ts @@ -294,17 +294,15 @@ export class BuddyList extends BuddyListConf { } this.render_section_headers(); - if (this.render_data.hide_headers) { - // Ensure the section isn't collapsed, because we're hiding its header - // so there's no way to collapse or uncollapse the list in this view. - $("#buddy-list-other-users-container").toggleClass("collapsed", false); - } else { - $("#buddy-list-other-users-container").toggleClass( - "collapsed", - this.other_users_is_collapsed, - ); - this.update_empty_list_placeholders(); - } + // Ensure the "other" section is visible when headers are collapsed, + // because we're hiding its header so there's no way to collapse or + // uncollapse the list in this view. Ensure we're showing/hiding as + // the user specified otherwise. + this.set_section_collapse( + "#buddy-list-other-users-container", + this.render_data.hide_headers ? false : this.other_users_is_collapsed, + ); + this.update_empty_list_placeholders(); } update_empty_list_placeholders(): void { @@ -482,18 +480,22 @@ export class BuddyList extends BuddyListConf { ); } + set_section_collapse(container_selector: string, is_collapsed: boolean): void { + $(container_selector).toggleClass("collapsed", is_collapsed); + $(`${container_selector} .buddy-list-section-toggle`).toggleClass( + "rotate-icon-down", + !is_collapsed, + ); + $(`${container_selector} .buddy-list-section-toggle`).toggleClass( + "rotate-icon-right", + is_collapsed, + ); + } + toggle_participants_section(): void { this.participants_is_collapsed = !this.participants_is_collapsed; - $("#buddy-list-participants-container").toggleClass( - "collapsed", - this.participants_is_collapsed, - ); - $("#buddy-list-participants-container .buddy-list-section-toggle").toggleClass( - "rotate-icon-down", - !this.participants_is_collapsed, - ); - $("#buddy-list-participants-container .buddy-list-section-toggle").toggleClass( - "rotate-icon-right", + this.set_section_collapse( + "#buddy-list-participants-container", this.participants_is_collapsed, ); @@ -504,16 +506,8 @@ export class BuddyList extends BuddyListConf { toggle_users_matching_view_section(): void { this.users_matching_view_is_collapsed = !this.users_matching_view_is_collapsed; - $("#buddy-list-users-matching-view-container").toggleClass( - "collapsed", - this.users_matching_view_is_collapsed, - ); - $("#buddy-list-users-matching-view-container .buddy-list-section-toggle").toggleClass( - "rotate-icon-down", - !this.users_matching_view_is_collapsed, - ); - $("#buddy-list-users-matching-view-container .buddy-list-section-toggle").toggleClass( - "rotate-icon-right", + this.set_section_collapse( + "#buddy-list-users-matching-view-container", this.users_matching_view_is_collapsed, ); @@ -524,16 +518,8 @@ export class BuddyList extends BuddyListConf { toggle_other_users_section(): void { this.other_users_is_collapsed = !this.other_users_is_collapsed; - $("#buddy-list-other-users-container").toggleClass( - "collapsed", - this.other_users_is_collapsed, - ); - $("#buddy-list-other-users-container .buddy-list-section-toggle").toggleClass( - "rotate-icon-down", - !this.other_users_is_collapsed, - ); - $("#buddy-list-other-users-container .buddy-list-section-toggle").toggleClass( - "rotate-icon-right", + this.set_section_collapse( + "#buddy-list-other-users-container", this.other_users_is_collapsed, ); From d0dc33d8da3f22521ad4c9aaa7e75621519409e9 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 28 Oct 2024 16:54:40 -0700 Subject: [PATCH 117/276] buddy_list: Move fill_screen call to the end of populate(). Sometimes we might want to re-fill the screen after collapsing or uncollapsing a section, so it's better to fill the screen just once after determining if we should collapse the "other users" section. Also, this commit removes a call to `render_section_headers()` because that's always called at the end of `fill_screen_with_content()`. --- web/src/buddy_list.ts | 5 ++--- web/tests/activity.test.js | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/src/buddy_list.ts b/web/src/buddy_list.ts index 47fdbc6950..a024ca6878 100644 --- a/web/src/buddy_list.ts +++ b/web/src/buddy_list.ts @@ -285,15 +285,12 @@ export class BuddyList extends BuddyListConf { // in already-sorted order. this.all_user_ids = opts.all_user_ids; - this.fill_screen_with_content(); - $("#buddy-list-users-matching-view-container .view-all-subscribers-link").remove(); $("#buddy-list-other-users-container .view-all-users-link").remove(); if (!buddy_data.get_is_searching_users()) { this.render_view_user_list_links(); } - this.render_section_headers(); // Ensure the "other" section is visible when headers are collapsed, // because we're hiding its header so there's no way to collapse or // uncollapse the list in this view. Ensure we're showing/hiding as @@ -302,7 +299,9 @@ export class BuddyList extends BuddyListConf { "#buddy-list-other-users-container", this.render_data.hide_headers ? false : this.other_users_is_collapsed, ); + this.update_empty_list_placeholders(); + this.fill_screen_with_content(); } update_empty_list_placeholders(): void { diff --git a/web/tests/activity.test.js b/web/tests/activity.test.js index 066614fc75..77966cbba7 100644 --- a/web/tests/activity.test.js +++ b/web/tests/activity.test.js @@ -137,7 +137,6 @@ function test(label, f) { }); stub_buddy_list_elements(); - helpers.override(buddy_list, "render_section_headers", noop); helpers.override(buddy_list, "render_view_user_list_links", noop); presence.presence_info.set(alice.user_id, {status: "active"}); From f157bfe181136c2b0e0ee83399854961648237ed Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 28 Oct 2024 17:02:14 -0700 Subject: [PATCH 118/276] buddy_list: Simplify logic to update empty list placeholders. Because we regularly change what we want the placeholder text to be, as the user enters and exits search, it's simpler to just not use `data-search-results-empty` and set up the placeholder from the buddy list code directly. Previously we were trying to sometimes use `data-search-results-empty` and sometimes set it directly, which is more confusing and complex. --- web/src/buddy_list.ts | 34 ++++++++++++++++----------------- web/templates/right_sidebar.hbs | 6 +++--- web/tests/activity.test.js | 1 - 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/web/src/buddy_list.ts b/web/src/buddy_list.ts index a024ca6878..c8631c0ee6 100644 --- a/web/src/buddy_list.ts +++ b/web/src/buddy_list.ts @@ -330,27 +330,27 @@ export class BuddyList extends BuddyListConf { } } - $("#buddy-list-users-matching-view").attr( - "data-search-results-empty", - matching_view_empty_list_message, - ); - if ($("#buddy-list-users-matching-view .empty-list-message").length) { - const empty_list_widget_html = render_empty_list_widget_for_list({ - empty_list_message: matching_view_empty_list_message, - }); - $("#buddy-list-users-matching-view").html(empty_list_widget_html); + function add_or_update_empty_list_placeholder(selector: string, message: string): void { + if ( + $(selector).children().length === 0 || + $(`${selector} .empty-list-message`).length + ) { + const empty_list_widget_html = render_empty_list_widget_for_list({ + empty_list_message: message, + }); + $(selector).html(empty_list_widget_html); + } } - $("#buddy-list-other-users").attr( - "data-search-results-empty", + add_or_update_empty_list_placeholder( + "#buddy-list-users-matching-view", + matching_view_empty_list_message, + ); + + add_or_update_empty_list_placeholder( + "#buddy-list-other-users", other_users_empty_list_message, ); - if ($("#buddy-list-other-users .empty-list-message").length) { - const empty_list_widget_html = render_empty_list_widget_for_list({ - empty_list_message: other_users_empty_list_message, - }); - $("#buddy-list-other-users").html(empty_list_widget_html); - } } update_section_header_counts(): void { diff --git a/web/templates/right_sidebar.hbs b/web/templates/right_sidebar.hbs index 460d7fa9db..7d6e80d695 100644 --- a/web/templates/right_sidebar.hbs +++ b/web/templates/right_sidebar.hbs @@ -21,15 +21,15 @@
                  -
                    +
                      -
                        +
                          -
                            +
                              -
                              - - -
                              + + {{> ../dropdown_widget_with_label + widget_name="realm_can_move_messages_between_topics_group" + label=(t 'Who can move messages to another topic') + value_type="number" }}
                              diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index 2656f02ea2..a518b15c62 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -588,10 +588,10 @@ run_test("realm settings", ({override}) => { override(realm, "realm_create_multiuse_invite_group", 1); override(realm, "realm_allow_message_editing", false); override(realm, "realm_message_content_edit_limit_seconds", 0); - override(realm, "realm_edit_topic_policy", 3); override(realm, "realm_authentication_methods", {Google: {enabled: false, available: true}}); override(realm, "realm_can_add_custom_emoji_group", 1); override(realm, "realm_can_create_public_channel_group", 1); + override(realm, "realm_can_move_messages_between_topics_group", 1); override(realm, "realm_direct_message_permission_group", 1); override(realm, "realm_plan_type", 2); override(realm, "realm_upload_quota_mib", 5000); @@ -601,12 +601,12 @@ run_test("realm settings", ({override}) => { assert_same(realm.realm_create_multiuse_invite_group, 3); assert_same(realm.realm_allow_message_editing, true); assert_same(realm.realm_message_content_edit_limit_seconds, 5); - assert_same(realm.realm_edit_topic_policy, 4); assert_same(realm.realm_authentication_methods, { Google: {enabled: true, available: true}, }); assert_same(realm.realm_can_add_custom_emoji_group, 3); assert_same(realm.realm_can_create_public_channel_group, 3); + assert_same(realm.realm_can_move_messages_between_topics_group, 3); assert_same(realm.realm_direct_message_permission_group, 3); assert_same(realm.realm_plan_type, 3); assert_same(realm.realm_upload_quota_mib, 50000); diff --git a/web/tests/lib/events.js b/web/tests/lib/events.js index 21940c2662..a395dc89ab 100644 --- a/web/tests/lib/events.js +++ b/web/tests/lib/events.js @@ -365,13 +365,13 @@ exports.fixtures = { data: { allow_message_editing: true, message_content_edit_limit_seconds: 5, - edit_topic_policy: 4, create_multiuse_invite_group: 3, authentication_methods: { Google: {enabled: true, available: true}, }, can_add_custom_emoji_group: 3, can_create_public_channel_group: 3, + can_move_messages_between_topics_group: 3, direct_message_permission_group: 3, plan_type: 3, upload_quota_mib: 50000, diff --git a/web/tests/popover_menus_data.test.js b/web/tests/popover_menus_data.test.js index 61e25a3514..a7be16f87d 100644 --- a/web/tests/popover_menus_data.test.js +++ b/web/tests/popover_menus_data.test.js @@ -142,7 +142,6 @@ function set_page_params_no_edit_restrictions({override}) { override(realm, "realm_allow_edit_history", true); override(realm, "realm_message_content_delete_limit_seconds", null); override(realm, "realm_enable_read_receipts", true); - override(realm, "realm_edit_topic_policy", 5); override(realm, "realm_move_messages_within_stream_limit_seconds", null); } @@ -168,6 +167,7 @@ test("my_message_all_actions", ({override}) => { set_page_params_no_edit_restrictions({override}); override(realm, "realm_can_delete_any_message_group", everyone.id); override(realm, "realm_can_delete_own_message_group", everyone.id); + override(realm, "realm_can_move_messages_between_topics_group", everyone.id); override(current_user, "user_id", me.user_id); // Get message with maximum permissions available // Initialize message list @@ -259,6 +259,8 @@ test("not_my_message_view_actions", ({override}) => { test("not_my_message_view_source_and_move", ({override}) => { set_page_params_no_edit_restrictions({override}); override(realm, "realm_can_delete_any_message_group", everyone.id); + override(realm, "realm_can_move_messages_between_topics_group", everyone.id); + override(current_user, "user_id", me.user_id); // Get message that is movable with viewable source const list = init_message_list(); diff --git a/web/tests/settings_data.test.js b/web/tests/settings_data.test.js index 668feaa0c7..1957b5e021 100644 --- a/web/tests/settings_data.test.js +++ b/web/tests/settings_data.test.js @@ -163,66 +163,6 @@ test_policy( settings_data.user_can_invite_users_by_email, ); -function test_message_policy(label, policy, validation_func) { - run_test(label, ({override}) => { - override(current_user, "is_admin", true); - override(realm, policy, settings_config.common_message_policy_values.by_admins_only.code); - assert.equal(validation_func(), true); - - override(current_user, "is_admin", false); - override(current_user, "is_moderator", true); - assert.equal(validation_func(), false); - - override( - realm, - policy, - settings_config.common_message_policy_values.by_moderators_only.code, - ); - assert.equal(validation_func(), true); - - override(current_user, "is_moderator", false); - assert.equal(validation_func(), false); - - override(current_user, "is_guest", true); - override(realm, policy, settings_config.common_message_policy_values.by_everyone.code); - assert.equal(validation_func(), true); - - override(realm, policy, settings_config.common_message_policy_values.by_members.code); - assert.equal(validation_func(), false); - - override(current_user, "is_guest", false); - assert.equal(validation_func(), true); - - override(realm, policy, settings_config.common_message_policy_values.by_full_members.code); - override(current_user, "user_id", 30); - isaac.date_joined = new Date(Date.now()); - override(realm, "realm_waiting_period_threshold", 10); - settings_data.initialize(isaac.date_joined); - assert.equal(validation_func(), false); - - isaac.date_joined = new Date(Date.now() - 20 * 86400000); - settings_data.initialize(isaac.date_joined); - assert.equal(validation_func(), true); - }); -} - -test_message_policy( - "user_can_move_messages_to_another_topic", - "realm_edit_topic_policy", - settings_data.user_can_move_messages_to_another_topic, -); - -run_test("user_can_move_messages_to_another_topic_nobody_case", ({override}) => { - override(current_user, "is_admin", true); - override(current_user, "is_guest", false); - override( - realm, - "realm_edit_topic_policy", - settings_config.edit_topic_policy_values.nobody.code, - ); - assert.equal(settings_data.user_can_move_messages_to_another_topic(), false); -}); - test_realm_group_settings( "realm_can_add_custom_emoji_group", settings_data.user_can_add_custom_emoji, @@ -243,6 +183,11 @@ test_realm_group_settings( settings_data.user_can_move_messages_between_streams, ); +test_realm_group_settings( + "realm_can_move_messages_between_topics_group", + settings_data.user_can_move_messages_to_another_topic, +); + run_test("using_dark_theme", ({override}) => { override(user_settings, "color_scheme", settings_config.color_scheme_values.dark.code); assert.equal(settings_data.using_dark_theme(), true); diff --git a/web/tests/settings_org.test.js b/web/tests/settings_org.test.js index 26c58b235d..66af886a2c 100644 --- a/web/tests/settings_org.test.js +++ b/web/tests/settings_org.test.js @@ -439,19 +439,11 @@ function test_discard_changes_button({override}, discard_changes) { }; override(realm, "realm_allow_edit_history", true); - override( - realm, - "realm_edit_topic_policy", - settings_config.common_message_policy_values.by_everyone.code, - ); override(realm, "realm_allow_message_editing", true); override(realm, "realm_message_content_edit_limit_seconds", 3600); override(realm, "realm_message_content_delete_limit_seconds", 120); const $allow_edit_history = $("#id_realm_allow_edit_history").prop("checked", false); - const $edit_topic_policy = $("#id_realm_edit_topic_policy").val( - settings_config.common_message_policy_values.by_admins_only.code, - ); const $msg_edit_limit_setting = $("#id_realm_message_content_edit_limit_seconds").val( "custom_period", ); @@ -468,7 +460,6 @@ function test_discard_changes_button({override}, discard_changes) { $allow_edit_history.attr("id", "id_realm_allow_edit_history"); $msg_edit_limit_setting.attr("id", "id_realm_message_content_edit_limit_seconds"); $msg_delete_limit_setting.attr("id", "id_realm_message_content_delete_limit_seconds"); - $edit_topic_policy.attr("id", "id_realm_edit_topic_policy"); $message_content_edit_limit_minutes.attr("id", "id_realm_message_content_edit_limit_minutes"); $message_content_delete_limit_minutes.attr( "id", @@ -480,7 +471,6 @@ function test_discard_changes_button({override}, discard_changes) { $allow_edit_history, $msg_edit_limit_setting, $msg_delete_limit_setting, - $edit_topic_policy, ]); const {$discard_button, $save_button_controls, props} = createSaveButtons("msg-editing"); @@ -494,10 +484,6 @@ function test_discard_changes_button({override}, discard_changes) { discard_changes.call({to_$: () => $(".save-discard-widget-button.discard-button")}, ev); assert.equal($allow_edit_history.prop("checked"), true); - assert.equal( - $edit_topic_policy.val(), - settings_config.common_message_policy_values.by_everyone.code, - ); assert.equal($msg_edit_limit_setting.val(), "3600"); assert.equal($message_content_edit_limit_minutes.val(), "60"); assert.equal($msg_delete_limit_setting.val(), "120"); diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 694355386c..046aea37f8 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1078,6 +1078,7 @@ group_setting_update_data_type = DictType( ("can_delete_own_message_group", group_setting_type), ("can_manage_all_groups", group_setting_type), ("can_move_messages_between_channels_group", group_setting_type), + ("can_move_messages_between_topics_group", group_setting_type), ("direct_message_initiator_group", group_setting_type), ("direct_message_permission_group", group_setting_type), ], diff --git a/zerver/migrations/0618_realm_can_move_messages_between_topics_group.py b/zerver/migrations/0618_realm_can_move_messages_between_topics_group.py new file mode 100644 index 0000000000..95a082159a --- /dev/null +++ b/zerver/migrations/0618_realm_can_move_messages_between_topics_group.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.9 on 2024-10-25 14:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0617_remove_prefix_from_archived_streams"), + ] + + operations = [ + migrations.AddField( + model_name="realm", + name="can_move_messages_between_topics_group", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/migrations/0619_set_default_value_for_can_move_messages_between_topics_group.py b/zerver/migrations/0619_set_default_value_for_can_move_messages_between_topics_group.py new file mode 100644 index 0000000000..f0c5ca9993 --- /dev/null +++ b/zerver/migrations/0619_set_default_value_for_can_move_messages_between_topics_group.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.1 on 2023-06-12 10:47 + +from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.db.models import OuterRef + + +def set_default_value_for_can_move_messages_between_topics_group( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + Realm = apps.get_model("zerver", "Realm") + NamedUserGroup = apps.get_model("zerver", "NamedUserGroup") + + edit_topic_policy_to_group_name = { + 1: "role:members", + 2: "role:administrators", + 3: "role:fullmembers", + 4: "role:moderators", + 5: "role:everyone", + 6: "role:nobody", + } + + for id, group_name in edit_topic_policy_to_group_name.items(): + Realm.objects.filter( + can_move_messages_between_topics_group=None, edit_topic_policy=id + ).update( + can_move_messages_between_topics_group=NamedUserGroup.objects.filter( + name=group_name, realm=OuterRef("id"), is_system_group=True + ).values("pk") + ) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("zerver", "0618_realm_can_move_messages_between_topics_group"), + ] + + operations = [ + migrations.RunPython( + set_default_value_for_can_move_messages_between_topics_group, + elidable=True, + reverse_code=migrations.RunPython.noop, + ) + ] diff --git a/zerver/migrations/0620_alter_realm_can_move_messages_between_topics_group.py b/zerver/migrations/0620_alter_realm_can_move_messages_between_topics_group.py new file mode 100644 index 0000000000..1870e8d00e --- /dev/null +++ b/zerver/migrations/0620_alter_realm_can_move_messages_between_topics_group.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.9 on 2024-10-25 14:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0619_set_default_value_for_can_move_messages_between_topics_group"), + ] + + operations = [ + migrations.AlterField( + model_name="realm", + name="can_move_messages_between_topics_group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/models/realms.py b/zerver/models/realms.py index 7c0e2d87b1..4d36a452fa 100644 --- a/zerver/models/realms.py +++ b/zerver/models/realms.py @@ -306,6 +306,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub "UserGroup", on_delete=models.RESTRICT, related_name="+" ) + # UserGroup which is allowed to move messages between topics. + can_move_messages_between_topics_group = models.ForeignKey( + "UserGroup", on_delete=models.RESTRICT, related_name="+" + ) + # Who in the organization is allowed to edit topics of any message. edit_topic_policy = models.PositiveSmallIntegerField(default=EditTopicPolicyEnum.EVERYONE) @@ -787,6 +792,15 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub default_group_name=SystemGroups.MEMBERS, id_field_name="can_move_messages_between_channels_group_id", ), + can_move_messages_between_topics_group=GroupPermissionSetting( + require_system_group=not settings.ALLOW_GROUP_VALUED_SETTINGS, + allow_internet_group=False, + allow_owners_group=False, + allow_nobody_group=True, + allow_everyone_group=True, + default_group_name=SystemGroups.EVERYONE, + id_field_name="can_move_messages_between_topics_group_id", + ), direct_message_initiator_group=GroupPermissionSetting( require_system_group=not settings.ALLOW_GROUP_VALUED_SETTINGS, allow_internet_group=False, @@ -1203,6 +1217,8 @@ def get_realm_with_settings(realm_id: int) -> Realm: "can_manage_all_groups__named_user_group", "can_move_messages_between_channels_group", "can_move_messages_between_channels_group__named_user_group", + "can_move_messages_between_topics_group", + "can_move_messages_between_topics_group__named_user_group", "direct_message_initiator_group", "direct_message_initiator_group__named_user_group", "direct_message_permission_group", diff --git a/zerver/models/users.py b/zerver/models/users.py index 406271b38f..a6806dee56 100644 --- a/zerver/models/users.py +++ b/zerver/models/users.py @@ -897,7 +897,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): return self.has_permission("can_manage_all_groups") def can_move_messages_to_another_topic(self) -> bool: - return self.has_permission("edit_topic_policy") + return self.has_permission("can_move_messages_between_topics_group") def can_add_custom_emoji(self) -> bool: return self.has_permission("can_add_custom_emoji_group") diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 0f543903e8..db526a7692 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4515,6 +4515,20 @@ paths: In Zulip 7.0 (feature level 159), `Nobody` was added as an option to `move_messages_between_streams_policy` enum. - $ref: "#/components/schemas/GroupSettingValue" + can_move_messages_between_topics_group: + allOf: + - description: | + A [group-setting value](/api/group-setting-values) defining the set of + users who have permission to move messages from one topic to another + within a channel in the organization. + + **Changes**: New in Zulip 10.0 (feature level 316). Previously, this + permission was controlled by the enum `edit_topic_policy`. Values were + 1=Members, 2=Admins, 3=Full members, 4=Moderators, 5=Everyone, 6=Nobody. + + In Zulip 7.0 (feature level 159), `Nobody` was added as an option to + `edit_topic_policy` enum. + - $ref: "#/components/schemas/GroupSettingValue" can_manage_all_groups: allOf: - $ref: "#/components/schemas/GroupSettingValue" @@ -8303,7 +8317,7 @@ paths: - `allow_message_editing` - `can_move_messages_between_channels_group` - - `edit_topic_policy` + - `can_move_messages_between_topics_group` - `message_content_edit_limit_seconds` - `move_messages_within_stream_limit_seconds` - `move_messages_between_streams_limit_seconds` @@ -8313,6 +8327,10 @@ paths: of the [`realm op: update_dict`](/api/get-events#realm-update_dict) event in [`GET /events`](/api/get-events). + **Changes**: In Zulip 10.0 (feature level 316), `edit_topic_policy` + was removed and replaced by `can_move_messages_between_topics_group` + realm setting. + **Changes**: In Zulip 10.0 (feature level 310), `move_messages_between_streams_policy` was removed and replaced by `can_move_messages_between_channels_group` realm setting. @@ -16351,6 +16369,22 @@ paths: In Zulip 7.0 (feature level 159), `Nobody` was added as an option to `move_messages_between_streams_policy` enum. - $ref: "#/components/schemas/GroupSettingValue" + realm_can_move_messages_between_topics_group: + allOf: + - description: | + Present if `realm` is present in `fetch_event_types`. + + A [group-setting value](/api/group-setting-values) defining the set of + users who have permission to move messages from one topic to another + within a channel in the organization. + + **Changes**: New in Zulip 10.0 (feature level 316). Previously, this + permission was controlled by the enum `edit_topic_policy`. Values were + 1=Members, 2=Admins, 3=Full members, 4=Moderators, 5=Everyone, 6=Nobody. + + In Zulip 7.0 (feature level 159), `Nobody` was added as an option to + `edit_topic_policy` enum. + - $ref: "#/components/schemas/GroupSettingValue" realm_bot_creation_policy: type: integer description: | diff --git a/zerver/tests/test_audit_log.py b/zerver/tests/test_audit_log.py index 9b2e1c40c8..ab846e52eb 100644 --- a/zerver/tests/test_audit_log.py +++ b/zerver/tests/test_audit_log.py @@ -31,6 +31,7 @@ from zerver.actions.realm_linkifiers import ( ) from zerver.actions.realm_playgrounds import check_add_realm_playground, do_remove_realm_playground from zerver.actions.realm_settings import ( + do_change_realm_permission_group_setting, do_deactivate_realm, do_reactivate_realm, do_set_realm_authentication_methods, @@ -89,7 +90,7 @@ from zerver.models.linkifiers import linkifiers_for_realm from zerver.models.realm_audit_logs import AuditLogEventType from zerver.models.realm_emoji import EmojiInfo, get_all_custom_emoji_for_realm from zerver.models.realm_playgrounds import get_realm_playgrounds -from zerver.models.realms import EditTopicPolicyEnum, RealmDomainDict, get_realm, get_realm_domains +from zerver.models.realms import RealmDomainDict, get_realm, get_realm_domains from zerver.models.streams import get_stream @@ -546,14 +547,24 @@ class TestRealmAuditLog(ZulipTestCase): 1, ) + administrators_system_group = NamedUserGroup.objects.get( + name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True + ) + everyone_system_group = NamedUserGroup.objects.get( + name=SystemGroups.EVERYONE, realm=realm, is_system_group=True + ) + value_expected = { - RealmAuditLog.OLD_VALUE: EditTopicPolicyEnum.EVERYONE, - RealmAuditLog.NEW_VALUE: EditTopicPolicyEnum.ADMINS_ONLY, - "property": "edit_topic_policy", + RealmAuditLog.OLD_VALUE: everyone_system_group.id, + RealmAuditLog.NEW_VALUE: administrators_system_group.id, + "property": "can_move_messages_between_topics_group", } - do_set_realm_property( - realm, "edit_topic_policy", EditTopicPolicyEnum.ADMINS_ONLY, acting_user=user + do_change_realm_permission_group_setting( + realm, + "can_move_messages_between_topics_group", + administrators_system_group, + acting_user=user, ) self.assertEqual( RealmAuditLog.objects.filter( diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 031a9b872b..15e91e134f 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -138,6 +138,7 @@ class HomeTest(ZulipTestCase): "realm_can_delete_own_message_group", "realm_can_manage_all_groups", "realm_can_move_messages_between_channels_group", + "realm_can_move_messages_between_topics_group", "realm_create_multiuse_invite_group", "realm_create_private_stream_policy", "realm_create_public_stream_policy", diff --git a/zerver/tests/test_message_edit.py b/zerver/tests/test_message_edit.py index 54ae8639a0..5a74bce349 100644 --- a/zerver/tests/test_message_edit.py +++ b/zerver/tests/test_message_edit.py @@ -6,7 +6,11 @@ import orjson from django.utils.timezone import now as timezone_now from zerver.actions.message_edit import get_mentions_for_message_updates -from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property +from zerver.actions.realm_settings import ( + do_change_realm_permission_group_setting, + do_change_realm_plan_type, + do_set_realm_property, +) from zerver.actions.streams import do_deactivate_stream from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group from zerver.actions.user_topics import do_set_user_topic_visibility_policy @@ -18,7 +22,7 @@ from zerver.lib.topic import TOPIC_NAME from zerver.lib.utils import assert_is_not_none from zerver.models import Attachment, Message, NamedUserGroup, Realm, UserProfile, UserTopic from zerver.models.groups import SystemGroups -from zerver.models.realms import EditTopicPolicyEnum, WildcardMentionPolicyEnum, get_realm +from zerver.models.realms import WildcardMentionPolicyEnum, get_realm from zerver.models.streams import get_stream @@ -892,7 +896,7 @@ class EditMessageTest(ZulipTestCase): def set_message_editing_params( allow_message_editing: bool, message_content_edit_limit_seconds: int | str, - edit_topic_policy: int, + can_move_messages_between_topics_group: NamedUserGroup, ) -> None: result = self.client_patch( "/json/realm", @@ -901,7 +905,11 @@ class EditMessageTest(ZulipTestCase): "message_content_edit_limit_seconds": orjson.dumps( message_content_edit_limit_seconds ).decode(), - "edit_topic_policy": orjson.dumps(edit_topic_policy).decode(), + "can_move_messages_between_topics_group": orjson.dumps( + { + "new": can_move_messages_between_topics_group.id, + } + ).decode(), }, ) self.assert_json_success(result) @@ -949,31 +957,35 @@ class EditMessageTest(ZulipTestCase): message.date_sent -= timedelta(seconds=180) message.save() + administrators_system_group = NamedUserGroup.objects.get( + name=SystemGroups.ADMINISTRATORS, realm=get_realm("zulip"), is_system_group=True + ) + # test the various possible message editing settings # high enough time limit, all edits allowed - set_message_editing_params(True, 240, EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(True, 240, administrators_system_group) do_edit_message_assert_success(id_, "A") # out of time, only topic editing allowed - set_message_editing_params(True, 120, EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(True, 120, administrators_system_group) do_edit_message_assert_success(id_, "B", True) do_edit_message_assert_error(id_, "C", "The time limit for editing this message has passed") # infinite time, all edits allowed - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(True, "unlimited", administrators_system_group) do_edit_message_assert_success(id_, "D") # without allow_message_editing, editing content is not allowed but # editing topic is allowed if topic-edit time limit has not passed # irrespective of content-edit time limit. - set_message_editing_params(False, 240, EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(False, 240, administrators_system_group) do_edit_message_assert_success(id_, "B", True) - set_message_editing_params(False, 240, EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(False, 240, administrators_system_group) do_edit_message_assert_success(id_, "E", True) - set_message_editing_params(False, 120, EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(False, 120, administrators_system_group) do_edit_message_assert_success(id_, "F", True) - set_message_editing_params(False, "unlimited", EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(False, "unlimited", administrators_system_group) do_edit_message_assert_success(id_, "G", True) def test_edit_message_in_archived_stream(self) -> None: @@ -1003,11 +1015,11 @@ class EditMessageTest(ZulipTestCase): ) self.assert_json_error(result, "Invalid message(s)") - def test_edit_topic_policy(self) -> None: + def test_can_move_messages_between_topics_group(self) -> None: def set_message_editing_params( allow_message_editing: bool, message_content_edit_limit_seconds: int | str, - edit_topic_policy: int, + can_move_messages_between_topics_group: NamedUserGroup, ) -> None: self.login("iago") result = self.client_patch( @@ -1017,7 +1029,11 @@ class EditMessageTest(ZulipTestCase): "message_content_edit_limit_seconds": orjson.dumps( message_content_edit_limit_seconds ).decode(), - "edit_topic_policy": orjson.dumps(edit_topic_policy).decode(), + "can_move_messages_between_topics_group": orjson.dumps( + { + "new": can_move_messages_between_topics_group.id, + } + ).decode(), }, ) self.assert_json_success(result) @@ -1056,30 +1072,51 @@ class EditMessageTest(ZulipTestCase): # Guest user must be subscribed to the stream to access the message. polonius = self.example_user("polonius") + realm = polonius.realm self.subscribe(polonius, "Denmark") + administrators_system_group = NamedUserGroup.objects.get( + name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True + ) + full_members_system_group = NamedUserGroup.objects.get( + name=SystemGroups.FULL_MEMBERS, realm=realm, is_system_group=True + ) + members_system_group = NamedUserGroup.objects.get( + name=SystemGroups.MEMBERS, realm=realm, is_system_group=True + ) + moderators_system_group = NamedUserGroup.objects.get( + name=SystemGroups.MODERATORS, realm=realm, is_system_group=True + ) + everyone_system_group = NamedUserGroup.objects.get( + name=SystemGroups.EVERYONE, realm=realm, is_system_group=True + ) + nobody_system_group = NamedUserGroup.objects.get( + name=SystemGroups.NOBODY, realm=realm, is_system_group=True + ) + # any user can edit the topic of a message - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.EVERYONE) + set_message_editing_params(True, "unlimited", everyone_system_group) do_edit_message_assert_success(id_, "A", "polonius") # only members can edit topic of a message - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.MEMBERS_ONLY) + set_message_editing_params(True, "unlimited", members_system_group) do_edit_message_assert_error( id_, "B", "You don't have permission to edit this message", "polonius" ) do_edit_message_assert_success(id_, "B", "cordelia") # only full members can edit topic of a message - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.FULL_MEMBERS_ONLY) + set_message_editing_params(True, "unlimited", full_members_system_group) cordelia = self.example_user("cordelia") hamlet = self.example_user("hamlet") - do_set_realm_property(cordelia.realm, "waiting_period_threshold", 10, acting_user=None) cordelia.date_joined = timezone_now() - timedelta(days=9) cordelia.save() hamlet.date_joined = timezone_now() - timedelta(days=9) hamlet.save() + + do_set_realm_property(cordelia.realm, "waiting_period_threshold", 10, acting_user=None) do_edit_message_assert_error( id_, "C", "You don't have permission to edit this message", "cordelia" ) @@ -1089,15 +1126,12 @@ class EditMessageTest(ZulipTestCase): id_, "C", "You don't have permission to edit this message", "hamlet" ) - cordelia.date_joined = timezone_now() - timedelta(days=11) - cordelia.save() - hamlet.date_joined = timezone_now() - timedelta(days=11) - hamlet.save() + do_set_realm_property(cordelia.realm, "waiting_period_threshold", 8, acting_user=None) do_edit_message_assert_success(id_, "C", "cordelia") do_edit_message_assert_success(id_, "CD", "hamlet") # only moderators can edit topic of a message - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.MODERATORS_ONLY) + set_message_editing_params(True, "unlimited", moderators_system_group) do_edit_message_assert_error( id_, "D", "You don't have permission to edit this message", "cordelia" ) @@ -1108,14 +1142,14 @@ class EditMessageTest(ZulipTestCase): do_edit_message_assert_success(id_, "D", "shiva") # only admins can edit the topics of messages - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.ADMINS_ONLY) + set_message_editing_params(True, "unlimited", administrators_system_group) do_edit_message_assert_error( id_, "E", "You don't have permission to edit this message", "shiva" ) do_edit_message_assert_success(id_, "E", "iago") # even owners and admins cannot edit the topics of messages - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.NOBODY) + set_message_editing_params(True, "unlimited", nobody_system_group) do_edit_message_assert_error( id_, "H", "You don't have permission to edit this message", "desdemona" ) @@ -1124,14 +1158,14 @@ class EditMessageTest(ZulipTestCase): ) # users can edit topics even if allow_message_editing is False - set_message_editing_params(False, "unlimited", EditTopicPolicyEnum.EVERYONE) + set_message_editing_params(False, "unlimited", everyone_system_group) do_edit_message_assert_success(id_, "D", "cordelia") # non-admin users cannot edit topics sent > 1 week ago including # sender of the message. message.date_sent -= timedelta(seconds=604900) message.save() - set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.EVERYONE) + set_message_editing_params(True, "unlimited", everyone_system_group) do_edit_message_assert_success(id_, "E", "iago") do_edit_message_assert_success(id_, "F", "shiva") do_edit_message_assert_error( @@ -1158,6 +1192,39 @@ class EditMessageTest(ZulipTestCase): do_edit_message_assert_success(id_, "G", "cordelia") do_edit_message_assert_success(id_, "H", "hamlet") + # Test for checking setting for non-system user group. + user_group = check_add_user_group( + realm, "new_group", [polonius, cordelia], acting_user=cordelia + ) + set_message_editing_params(True, "unlimited", user_group) + # Polonius and Cordelia are in the allowed user group, so can move messages. + do_edit_message_assert_success(id_, "I", "polonius") + do_edit_message_assert_success(id_, "J", "cordelia") + # Iago is not in the allowed user group, so cannot move messages. + do_edit_message_assert_error( + id_, "K", "You don't have permission to edit this message", "iago" + ) + + # Test for checking the setting for anonymous user group. + anonymous_user_group = self.create_or_update_anonymous_group_for_setting( + [cordelia], + [administrators_system_group], + ) + do_change_realm_permission_group_setting( + realm, + "can_move_messages_between_topics_group", + anonymous_user_group, + acting_user=None, + ) + # Cordelia is the direct member of the anonymous user group, so can move messages. + do_edit_message_assert_success(id_, "K", "cordelia") + # Iago is in the `administrators_system_group` subgroup, so can move messages. + do_edit_message_assert_success(id_, "L", "iago") + # Shiva is not in the anonymous user group, so cannot move messages. + do_edit_message_assert_error( + id_, "M", "You don't have permission to edit this message", "shiva" + ) + @mock.patch("zerver.actions.message_edit.send_event_on_commit") def test_topic_wildcard_mention_in_followed_topic( self, mock_send_event: mock.MagicMock diff --git a/zerver/tests/test_message_move_stream.py b/zerver/tests/test_message_move_stream.py index 3a2fcc425d..6b4fd86797 100644 --- a/zerver/tests/test_message_move_stream.py +++ b/zerver/tests/test_message_move_stream.py @@ -16,7 +16,7 @@ from zerver.lib.test_helpers import queries_captured from zerver.lib.url_encoding import near_stream_message_url from zerver.models import Message, NamedUserGroup, Stream, UserMessage, UserProfile from zerver.models.groups import SystemGroups -from zerver.models.realms import EditTopicPolicyEnum, get_realm +from zerver.models.realms import get_realm from zerver.models.streams import get_stream @@ -1130,10 +1130,19 @@ class MessageMoveStreamTest(ZulipTestCase): (user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics( "othello", "old_stream_1", "new_stream_1", "test" ) - realm = user_profile.realm - realm.edit_topic_policy = EditTopicPolicyEnum.ADMINS_ONLY - realm.save() + + administrators_system_group = NamedUserGroup.objects.get( + name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True + ) + + do_change_realm_permission_group_setting( + realm, + "can_move_messages_between_topics_group", + administrators_system_group, + acting_user=None, + ) + self.login("cordelia") members_system_group = NamedUserGroup.objects.get( @@ -1175,7 +1184,7 @@ class MessageMoveStreamTest(ZulipTestCase): "iago", "test move stream", "new stream", "test" ) - with self.assert_database_query_count(53), self.assert_memcached_count(14): + with self.assert_database_query_count(55), self.assert_memcached_count(14): result = self.client_patch( f"/json/messages/{msg_id}", { diff --git a/zerver/tests/test_message_move_topic.py b/zerver/tests/test_message_move_topic.py index 2e1d31e060..8a9f745a19 100644 --- a/zerver/tests/test_message_move_topic.py +++ b/zerver/tests/test_message_move_topic.py @@ -266,7 +266,7 @@ class MessageMoveTopicTest(ZulipTestCase): # state + 1/user with a UserTopic row for the events data) # beyond what is typical were there not UserTopic records to # update. Ideally, we'd eliminate the per-user component. - with self.assert_database_query_count(25): + with self.assert_database_query_count(27): check_update_message( user_profile=hamlet, message_id=message_id, @@ -426,7 +426,7 @@ class MessageMoveTopicTest(ZulipTestCase): set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED) set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED) - with self.assert_database_query_count(29): + with self.assert_database_query_count(31): check_update_message( user_profile=desdemona, message_id=message_id, @@ -449,7 +449,7 @@ class MessageMoveTopicTest(ZulipTestCase): second_message_id = self.send_stream_message( hamlet, stream_name, topic_name="changed topic name", content="Second message" ) - with self.assert_database_query_count(23): + with self.assert_database_query_count(24): check_update_message( user_profile=desdemona, message_id=second_message_id, @@ -528,7 +528,7 @@ class MessageMoveTopicTest(ZulipTestCase): users_to_be_notified_via_muted_topics_event.append(user_topic.user_profile_id) change_all_topic_name = "Topic 1 edited" - with self.assert_database_query_count(30): + with self.assert_database_query_count(32): check_update_message( user_profile=hamlet, message_id=message_id, diff --git a/zerver/views/realm.py b/zerver/views/realm.py index fc7597975b..397afde2ad 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -143,6 +143,7 @@ def update_realm( can_create_web_public_channel_group: Json[GroupSettingChangeRequest] | None = None, can_manage_all_groups: Json[GroupSettingChangeRequest] | None = None, can_move_messages_between_channels_group: Json[GroupSettingChangeRequest] | None = None, + can_move_messages_between_topics_group: Json[GroupSettingChangeRequest] | None = None, direct_message_initiator_group: Json[GroupSettingChangeRequest] | None = None, direct_message_permission_group: Json[GroupSettingChangeRequest] | None = None, invite_to_stream_policy: Json[CommonPolicyEnum] | None = None, From 1ba1408b01038da303b073ded3f9b09205492e0f Mon Sep 17 00:00:00 2001 From: Vector73 Date: Sun, 27 Oct 2024 22:04:34 +0530 Subject: [PATCH 124/276] settings: Remove `edit_topic_policy` setting. Removed `edit_topic_policy` property, as the permission to move messages between topcis is now controlled by `can_move_messages_between_topics_group` setting. --- api_docs/changelog.md | 4 ++ zerver/actions/realm_settings.py | 1 - zerver/lib/event_schema.py | 9 ---- zerver/lib/events.py | 4 -- zerver/migrations/0001_squashed_0569.py | 13 +++-- .../0621_remove_realm_edit_topic_policy.py | 16 ++++++ zerver/models/realms.py | 15 ------ zerver/models/users.py | 1 - zerver/openapi/zulip.yaml | 50 ++----------------- zerver/tests/test_events.py | 2 - zerver/tests/test_home.py | 1 - zerver/tests/test_realm.py | 2 - zerver/views/realm.py | 2 - 13 files changed, 35 insertions(+), 85 deletions(-) create mode 100644 zerver/migrations/0621_remove_realm_edit_topic_policy.py diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 3d852599e6..30a0dbeae5 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -28,6 +28,10 @@ format used by the Zulip server that they are interacting with. [group-setting value](/api/group-setting-values) describing the set of users with permission to move messages from one topic to another within a channel in the organization. +* `PATCH /realm`, [`GET /events`](/api/get-events): Removed + `edit_topic_policy` property, as the permission to move messages between + topics in the organization is now controlled by + `can_move_messages_between_topics_group` setting. **Feature level 315** diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index babcce7055..7d06ab8257 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -82,7 +82,6 @@ def do_set_realm_property( # These settings have a different event format due to their history. message_edit_settings = [ "allow_message_editing", - "edit_topic_policy", "message_content_edit_limit_seconds", ] if name in message_edit_settings: diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 046aea37f8..5d47f3505b 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1039,12 +1039,6 @@ message_content_edit_limit_seconds_data = DictType( ] ) -edit_topic_policy_data = DictType( - required_keys=[ - ("edit_topic_policy", int), - ] -) - night_logo_data = DictType( required_keys=[ ("night_logo_url", str), @@ -1096,7 +1090,6 @@ update_dict_data = UnionType( [ allow_message_editing_data, authentication_data, - edit_topic_policy_data, icon_data, logo_data, message_content_edit_limit_seconds_data, @@ -1131,8 +1124,6 @@ def check_realm_update_dict( sub_type = allow_message_editing_data elif "message_content_edit_limit_seconds" in event["data"]: sub_type = message_content_edit_limit_seconds_data - elif "edit_topic_policy" in event["data"]: - sub_type = edit_topic_policy_data elif "authentication_methods" in event["data"]: sub_type = authentication_data elif any( diff --git a/zerver/lib/events.py b/zerver/lib/events.py index 8d7b23d4e7..5d4e2b7be1 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -96,7 +96,6 @@ from zerver.models.linkifiers import linkifiers_for_realm from zerver.models.realm_emoji import get_all_custom_emoji_for_realm from zerver.models.realm_playgrounds import get_realm_playgrounds from zerver.models.realms import ( - EditTopicPolicyEnum, get_corresponding_policy_value_for_group_setting, get_realm_domains, get_realm_with_settings, @@ -332,9 +331,6 @@ def fetch_initial_state_data( state["realm_allow_message_editing"] = ( False if user_profile is None else realm.allow_message_editing ) - state["realm_edit_topic_policy"] = ( - EditTopicPolicyEnum.ADMINS_ONLY if user_profile is None else realm.edit_topic_policy - ) # This setting determines whether to send presence and also # whether to display of users list in the right sidebar; we diff --git a/zerver/migrations/0001_squashed_0569.py b/zerver/migrations/0001_squashed_0569.py index 346f64db5d..3c323e9dbc 100644 --- a/zerver/migrations/0001_squashed_0569.py +++ b/zerver/migrations/0001_squashed_0569.py @@ -30,6 +30,15 @@ class LegacyCommonMessagePolicyEnum(IntEnum): EVERYONE = 5 +class LegacyEditTopicPolicyEnum(IntEnum): + MEMBERS_ONLY = 1 + ADMINS_ONLY = 2 + FULL_MEMBERS_ONLY = 3 + MODERATORS_ONLY = 4 + EVERYONE = 5 + NOBODY = 6 + + def get_fts_sql() -> str: if settings.POSTGRESQL_MISSING_DICTIONARIES: fts_sql = """ @@ -743,9 +752,7 @@ class Migration(migrations.Migration): ), ( "edit_topic_policy", - models.PositiveSmallIntegerField( - default=zerver.models.realms.EditTopicPolicyEnum["EVERYONE"] - ), + models.PositiveSmallIntegerField(default=LegacyEditTopicPolicyEnum["EVERYONE"]), ), ( "invite_to_realm_policy", diff --git a/zerver/migrations/0621_remove_realm_edit_topic_policy.py b/zerver/migrations/0621_remove_realm_edit_topic_policy.py new file mode 100644 index 0000000000..6cc29718cf --- /dev/null +++ b/zerver/migrations/0621_remove_realm_edit_topic_policy.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.9 on 2024-10-27 16:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0620_alter_realm_can_move_messages_between_topics_group"), + ] + + operations = [ + migrations.RemoveField( + model_name="realm", + name="edit_topic_policy", + ), + ] diff --git a/zerver/models/realms.py b/zerver/models/realms.py index 4d36a452fa..9b5a0fc4d8 100644 --- a/zerver/models/realms.py +++ b/zerver/models/realms.py @@ -106,15 +106,6 @@ class CommonPolicyEnum(IntEnum): MODERATORS_ONLY = 4 -class EditTopicPolicyEnum(IntEnum): - MEMBERS_ONLY = 1 - ADMINS_ONLY = 2 - FULL_MEMBERS_ONLY = 3 - MODERATORS_ONLY = 4 - EVERYONE = 5 - NOBODY = 6 - - class InviteToRealmPolicyEnum(IntEnum): MEMBERS_ONLY = 1 ADMINS_ONLY = 2 @@ -268,8 +259,6 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub field.value for field in CreateWebPublicStreamPolicyEnum ] - EDIT_TOPIC_POLICY_TYPES = [field.value for field in EditTopicPolicyEnum] - DEFAULT_MOVE_MESSAGE_LIMIT_SECONDS = 7 * SECONDS_PER_DAY move_messages_within_stream_limit_seconds = models.PositiveIntegerField( @@ -311,9 +300,6 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub "UserGroup", on_delete=models.RESTRICT, related_name="+" ) - # Who in the organization is allowed to edit topics of any message. - edit_topic_policy = models.PositiveSmallIntegerField(default=EditTopicPolicyEnum.EVERYONE) - # Who in the organization is allowed to invite other users to organization. invite_to_realm_policy = models.PositiveSmallIntegerField( default=InviteToRealmPolicyEnum.MEMBERS_ONLY @@ -654,7 +640,6 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub digest_emails_enabled=bool, digest_weekday=int, disallow_disposable_email_addresses=bool, - edit_topic_policy=int, email_changes_disabled=bool, emails_restricted_to_domains=bool, enable_guest_user_indicator=bool, diff --git a/zerver/models/users.py b/zerver/models/users.py index a6806dee56..4efe99a9b1 100644 --- a/zerver/models/users.py +++ b/zerver/models/users.py @@ -817,7 +817,6 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): from zerver.models import Realm if policy_name not in Realm.REALM_PERMISSION_GROUP_SETTINGS and policy_name not in [ - "edit_topic_policy", "invite_to_stream_policy", "invite_to_realm_policy", ]: diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index db526a7692..bc313f4399 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4357,7 +4357,11 @@ paths: description: | An object containing the properties that have changed. - **Changes**: In Zulip 7.0 (feature level 183), the + **Changes**: In Zulip 10.0 (feature level 316), `edit_topic_policy` + property was removed and replaced by `can_move_messages_between_topics_group` + realm setting. + + In Zulip 7.0 (feature level 183), the `community_topic_editing_limit_seconds` property was removed. It was documented as potentially returned as a changed property in this event, but in fact it was only ever returned in the @@ -4633,27 +4637,6 @@ paths: description: | Whether the organization disallows disposable email addresses. - edit_topic_policy: - type: integer - description: | - The [policy][permission-level] for which users can edit topics of any message. - - - 1 = Members only - - 2 = Admins only - - 3 = [Full members][calc-full-member] only - - 4 = Moderators only - - 5 = Everyone - - 6 = Nobody - - See [`PATCH /messages/{message_id}`](/api/update-message) for details and history - of how message editing permissions work. - - **Changes**: Nobody added as an option in Zulip 7.0 (feature level 159). - - New in Zulip 5.0 (feature level 75). - - [permission-level]: /api/roles-and-permissions#permission-levels - [calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member email_changes_disabled: type: boolean description: | @@ -16928,29 +16911,6 @@ paths: history of how message editing permissions work. [config-message-editing]: /help/restrict-message-editing-and-deletion - realm_edit_topic_policy: - type: integer - description: | - Present if `realm` is present in `fetch_event_types`. - - The [policy][permission-level] for which users can edit topics of any message. - - - 1 = Members only - - 2 = Admins only - - 3 = [Full members][calc-full-member] only - - 4 = Moderators only - - 5 = Everyone - - 6 = Nobody - - See [`PATCH /messages/{message_id}`](/api/update-message) for details and - history of how message editing permissions work. - - **Changes**: Nobody added as an option in Zulip 7.0 (feature level 159). - - New in Zulip 5.0 (feature level 75). - - [permission-level]: /api/roles-and-permissions#permission-levels - [calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member realm_message_content_edit_limit_seconds: type: integer nullable: true diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index c58cc06903..b90eabcb6d 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -3743,7 +3743,6 @@ class RealmPropertyActionTest(BaseAction): default_code_block_language=["python", "javascript"], message_content_delete_limit_seconds=[1000, 1100, 1200], invite_to_realm_policy=Realm.INVITE_TO_REALM_POLICY_TYPES, - edit_topic_policy=Realm.EDIT_TOPIC_POLICY_TYPES, message_content_edit_limit_seconds=[1000, 1100, 1200, None], move_messages_within_stream_limit_seconds=[1000, 1100, 1200], move_messages_between_streams_limit_seconds=[1000, 1100, 1200], @@ -3804,7 +3803,6 @@ class RealmPropertyActionTest(BaseAction): if name in [ "allow_message_editing", - "edit_topic_policy", "message_content_edit_limit_seconds", ]: check_realm_update_dict("events[0]", events[0]) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 15e91e134f..16313727cc 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -156,7 +156,6 @@ class HomeTest(ZulipTestCase): "realm_direct_message_permission_group", "realm_disallow_disposable_email_addresses", "realm_domains", - "realm_edit_topic_policy", "realm_email_auth_enabled", "realm_email_changes_disabled", "realm_emails_restricted_to_domains", diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 6c09c8504f..dcac8f603c 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -849,7 +849,6 @@ class RealmTest(ZulipTestCase): message_content_delete_limit_seconds=-10, wildcard_mention_policy=10, invite_to_realm_policy=10, - edit_topic_policy=10, message_content_edit_limit_seconds=0, move_messages_within_stream_limit_seconds=0, move_messages_between_streams_limit_seconds=0, @@ -1697,7 +1696,6 @@ class RealmAPITest(ZulipTestCase): ], message_content_delete_limit_seconds=[1000, 1100, 1200], invite_to_realm_policy=Realm.INVITE_TO_REALM_POLICY_TYPES, - edit_topic_policy=Realm.EDIT_TOPIC_POLICY_TYPES, message_content_edit_limit_seconds=[1000, 1100, 1200], move_messages_within_stream_limit_seconds=[1000, 1100, 1200], move_messages_between_streams_limit_seconds=[1000, 1100, 1200], diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 397afde2ad..6aa7f5e4ed 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -52,7 +52,6 @@ from zerver.models.realms import ( BotCreationPolicyEnum, CommonPolicyEnum, DigestWeekdayEnum, - EditTopicPolicyEnum, InviteToRealmPolicyEnum, OrgTypeEnum, WildcardMentionPolicyEnum, @@ -116,7 +115,6 @@ def update_realm( ApiParamConfig("message_content_delete_limit_seconds"), ] = None, allow_message_editing: Json[bool] | None = None, - edit_topic_policy: Json[EditTopicPolicyEnum] | None = None, mandatory_topics: Json[bool] | None = None, message_content_edit_limit_seconds_raw: Annotated[ Json[int | str] | None, ApiParamConfig("message_content_edit_limit_seconds") From 18a8125dac0f8c50b62b6bf82688254511ee2dbe Mon Sep 17 00:00:00 2001 From: Aditya Kumar Kasaudhan Date: Sat, 26 Oct 2024 20:25:31 +0530 Subject: [PATCH 125/276] user_groups: Include group_id in success response on group creation. Previously, the `group_id` was not returned in the success response of the user group creation API. This commit updates the API to return a success response containing the unique ID of the user group with the key `group_id`. This enhancement allows clients to easily reference the newly created user group. Fixes: #29686 --- api_docs/changelog.md | 7 +++++++ version.py | 2 +- zerver/openapi/zulip.yaml | 25 ++++++++++++++++++++++++- zerver/views/user_groups.py | 3 +-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 30a0dbeae5..a306d3a361 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 317** + +* [`POST /user_groups/create`](/api/create-user-group): + Added `group_id` to the success response of the user group creation + endpoint, enabling clients to easily access the unique identifier + of the newly created user group. + **Feature level 316** * `PATCH /realm`, [`GET /events`](/api/get-events), diff --git a/version.py b/version.py index a1d505fa9e..d5826b3aaf 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 316 # Last bumped for `can_move_messages_between_topics_group` +API_FEATURE_LEVEL = 317 # Last bumped for inclusion of `group_id` in user group creation response. # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index bc313f4399..1ecc82690f 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -20457,7 +20457,30 @@ paths: contentType: application/json responses: "200": - $ref: "#/components/responses/SimpleSuccess" + description: | + A success response containing the unique ID of the user group. + This field provides a straightforward way to reference the + newly created user group. + + **Changes**: New in Zulip 10.0 (feature level 317). + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - required: + - group_id + additionalProperties: false + properties: + result: {} + msg: {} + ignored_parameters_unsupported: {} + group_id: + type: integer + description: | + The unique ID of the created user group. + example: {"msg": "", "result": "success", "group_id": 123} + "400": description: Bad request. content: diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index 04bae18a12..ae2473ec42 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -101,8 +101,7 @@ def add_user_group( add_subgroups_to_user_group( context.supergroup, context.direct_subgroups, acting_user=user_profile ) - - return json_success(request) + return json_success(request, data={"group_id": user_group.id}) @require_member_or_admin From fc50736f4e0f9c948aa1462a16669ed8c6b6da82 Mon Sep 17 00:00:00 2001 From: PieterCK Date: Mon, 12 Aug 2024 12:25:40 +0700 Subject: [PATCH 126/276] slack_data_import: Fix incorrect hyperlink conversion. Currently, Slack messages containing hyperlinks (e.g.,) are converted like normal links. This commit reformats Slack hyperlinks into Zulip-friendly markdown (e.g., [Foo!](http://foo.com)). Part of #32165. --- zerver/data_import/slack_message_conversion.py | 13 ++++++++++--- zerver/tests/fixtures/slack_message_conversion.json | 7 ++++++- zerver/tests/test_slack_message_conversion.py | 5 +++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/zerver/data_import/slack_message_conversion.py b/zerver/data_import/slack_message_conversion.py index d023e443b4..9887590523 100644 --- a/zerver/data_import/slack_message_conversion.py +++ b/zerver/data_import/slack_message_conversion.py @@ -157,13 +157,20 @@ def convert_markdown_syntax(text: str, regex: str, zulip_keyword: str) -> str: def convert_link_format(text: str) -> tuple[str, bool]: """ 1. Converts '' to 'https://foo.com' - 2. Converts '' to 'https://foo.com|foo' + 2. Converts '' to '[foo](https://foo.com)' """ has_link = False for match in re.finditer(LINK_REGEX, text, re.VERBOSE): - converted_text = match.group(0).replace(">", "").replace("<", "") + slack_url = match.group(0) + url_parts = slack_url[1:-1].split("|", maxsplit=1) + # Check if there's a pipe with text after it + if len(url_parts) == 2: + converted_url = f"[{url_parts[1]}]({url_parts[0]})" + else: + converted_url = url_parts[0] + has_link = True - text = text.replace(match.group(0), converted_text) + text = text.replace(slack_url, converted_url) return text, has_link diff --git a/zerver/tests/fixtures/slack_message_conversion.json b/zerver/tests/fixtures/slack_message_conversion.json index d5a038a5c9..7725743998 100644 --- a/zerver/tests/fixtures/slack_message_conversion.json +++ b/zerver/tests/fixtures/slack_message_conversion.json @@ -8,7 +8,12 @@ { "name": "slack_link_with_pipe", "input": ">Google logo today:\n>\n>Kinda boring", - "conversion_output": ">Google logo today:\n>https://foo.com|foo\n>Kinda boring" + "conversion_output": ">Google logo today:\n>[foo](https://foo.com)\n>Kinda boring" + }, + { + "name": "slack_link_with_pipes", + "input": ">Google logo today:\n>\n>Kinda boring", + "conversion_output": ">Google logo today:\n>[foo|oof](https://foo.com)\n>Kinda boring" }, { "name": "slack_link2", diff --git a/zerver/tests/test_slack_message_conversion.py b/zerver/tests/test_slack_message_conversion.py index f24dc857b8..40334327f3 100644 --- a/zerver/tests/test_slack_message_conversion.py +++ b/zerver/tests/test_slack_message_conversion.py @@ -113,6 +113,11 @@ class SlackMessageConversion(ZulipTestCase): self.assertEqual(text, "http://journals.plos.org/plosone/article") self.assertEqual(has_link, True) + message = "" + text, mentioned_users, has_link = convert_to_zulip_markdown(message, [], {}, slack_user_map) + self.assertEqual(text, "[Help logging in to CZO](http://chat.zulip.org/help/logging-in)") + self.assertEqual(has_link, True) + message = "" text, mentioned_users, has_link = convert_to_zulip_markdown(message, [], {}, slack_user_map) self.assertEqual(text, "mailto:foo@foo.com") From 42e15172550955ada485ba384d7148422a4dc4b6 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 29 Oct 2024 16:40:20 -0700 Subject: [PATCH 127/276] email_notifications: Prevent html2text from mangling Unicode. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit html2text mangles Unicode by default, with a --unicode-snob option to disable it. If I have to get called a “snob” for wanting to correctly support non-English languages, then uh, I’ll take one for the team. https://github.com/Alir3z4/html2text/blob/2024.2.26/html2text/config.py#L111-L150 Signed-off-by: Anders Kaseorg --- zerver/data_import/mattermost.py | 2 +- zerver/lib/email_notifications.py | 2 +- zerver/tests/test_email_notifications.py | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/zerver/data_import/mattermost.py b/zerver/data_import/mattermost.py index 9dcf0feeb0..af5a271a33 100644 --- a/zerver/data_import/mattermost.py +++ b/zerver/data_import/mattermost.py @@ -440,7 +440,7 @@ def process_raw_message_batch( ) # html2text is GPL licensed, so run it as a subprocess. - content = subprocess.check_output(["html2text"], input=content, text=True) + content = subprocess.check_output(["html2text", "--unicode-snob"], input=content, text=True) if len(content) > 10000: # nocoverage logging.info("skipping too-long message of length %s", len(content)) diff --git a/zerver/lib/email_notifications.py b/zerver/lib/email_notifications.py index f487605421..d870d7c13c 100644 --- a/zerver/lib/email_notifications.py +++ b/zerver/lib/email_notifications.py @@ -938,7 +938,7 @@ def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> N def convert_html_to_markdown(html: str) -> str: # html2text is GPL licensed, so run it as a subprocess. markdown = subprocess.check_output( - [os.path.join(sys.prefix, "bin", "html2text")], input=html, text=True + [os.path.join(sys.prefix, "bin", "html2text"), "--unicode-snob"], input=html, text=True ).strip() # We want images to get linked and inline previewed, but html2text will turn diff --git a/zerver/tests/test_email_notifications.py b/zerver/tests/test_email_notifications.py index 68c59a0a47..9ce37b5352 100644 --- a/zerver/tests/test_email_notifications.py +++ b/zerver/tests/test_email_notifications.py @@ -11,6 +11,7 @@ from django.utils.timezone import now as timezone_now from django_auth_ldap.config import LDAPSearch from zerver.lib.email_notifications import ( + convert_html_to_markdown, enqueue_welcome_emails, get_onboarding_email_schedule, send_account_registered_email, @@ -671,3 +672,10 @@ class TestCustomWelcomeEmailSender(ZulipTestCase): email_data = orjson.loads(scheduled_emails[0].data) self.assertEqual(email_data["from_name"], name) self.assertEqual(email_data["from_address"], email) + + +class TestHtmlToMarkdown(ZulipTestCase): + def test_html_to_markdown_unicode(self) -> None: + self.assertEqual( + convert_html_to_markdown("a rose is not a rosé"), "a rose is not a rosé" + ) From 3fe1e554a6b34a5f94bbc0f5903ae1db7ac6ec42 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Wed, 30 Oct 2024 18:56:22 +0530 Subject: [PATCH 128/276] echo: Fix send messages not visible when auto narrowed to recipient. We simply forgot to `add_to_narrow` locally echoed messages if the current narrow changed before we received confirmation from server. --- web/src/echo.ts | 1 + web/tests/echo.test.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/web/src/echo.ts b/web/src/echo.ts index 9d91a7e2ff..bcf4c0a2f4 100644 --- a/web/src/echo.ts +++ b/web/src/echo.ts @@ -557,6 +557,7 @@ export function process_from_server(messages: ServerMessage[]): ServerMessage[] // message content, but in practice, there's no harm to just // doing it unconditionally. msg_list.view.rerender_messages(msgs_to_rerender_or_add_to_narrow); + msg_list.add_messages(msgs_to_rerender_or_add_to_narrow, {}); } } } diff --git a/web/tests/echo.test.js b/web/tests/echo.test.js index ee91b04583..02c2f2dfaa 100644 --- a/web/tests/echo.test.js +++ b/web/tests/echo.test.js @@ -47,6 +47,7 @@ message_lists.current = { }, }, change_message_id: noop, + add_messages: noop, }; const home_msg_list = { view: { @@ -62,6 +63,7 @@ const home_msg_list = { }, preserver_rendered_state: true, change_message_id: noop, + add_messages: noop, }; message_lists.all_rendered_message_lists = () => [home_msg_list, message_lists.current]; message_lists.non_rendered_data = () => []; From 9231c9745484adf5bcd5aa16ebce455a6baf790e Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Wed, 30 Oct 2024 11:56:00 -0500 Subject: [PATCH 129/276] compose: Bring colors into variablized concord. --- web/styles/app_variables.css | 18 ++++++++++++++++++ web/styles/compose.css | 11 ++++++++--- web/styles/dark_theme.css | 3 +-- web/styles/input_pill.css | 6 +++++- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index ae27406cf5..8ad45feb09 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -671,6 +671,20 @@ --color-compose-send-control-button-focus-shadow: var( --color-compose-send-button-focus-shadow ); + --color-compose-recipient-box-text-color: inherit; + --color-compose-recipient-box-background-color: hsl(0deg 0% 100%); + /* Because of how the background color is assigned on + recipient-row elements, we need here to mix down the + border color from the compose text area, + --color-message-content-container-border, + with the compose box's background color, + --color-compose-box-background. */ + --color-compose-recipient-box-border-color: color-mix( + in srgb, + hsl(0deg 0% 0%) 10%, + hsl(232deg 20% 92%) + ); + --color-compose-recipient-box-has-focus: hsl(0deg 0% 57%); --color-compose-collapsed-reply-button-area-background: hsl(0deg 0% 100%); --color-compose-collapsed-reply-button-area-background-interactive: var( --color-compose-collapsed-reply-button-area-background @@ -1192,6 +1206,10 @@ --color-compose-send-control-button-focus-shadow: var( --color-compose-send-button-focus-shadow ); + --color-compose-recipient-box-text-color: inherit; + --color-compose-recipient-box-background-color: hsl(0deg 0% 0% / 20%); + --color-compose-recipient-box-border-color: hsl(0deg 0% 0% / 80%); + --color-compose-recipient-box-has-focus: hsl(0deg 0% 100% / 27%); --color-compose-collapsed-reply-button-area-background: hsl( 0deg 0% 0% / 20% ); diff --git a/web/styles/compose.css b/web/styles/compose.css index 0747b0a388..943b1a9158 100644 --- a/web/styles/compose.css +++ b/web/styles/compose.css @@ -904,16 +904,16 @@ textarea.new_message_textarea { grid-template-columns: minmax(0, 1fr) auto; align-items: stretch; flex: 1 1 0; - border: 1px solid hsl(0deg 0% 0% / 20%); + border: 1px solid var(--color-compose-recipient-box-border-color); border-radius: 3px; transition: border-color 0.2s ease; - background: hsl(0deg 0% 100%); + background: var(--color-compose-recipient-box-background-color); /* Give the recipient box, a `
                              `, the correct styles when focus is in the #stream_message_recipient_topic `` */ &:focus-within { - border-color: hsl(0deg 0% 67%); + border-color: var(--color-compose-recipient-box-has-focus); } #stream_message_recipient_topic, @@ -973,6 +973,11 @@ textarea.new_message_textarea { #compose_select_recipient_widget { width: auto; outline: none; + /* We override the component-level colors to + ensure concord with the topic box. */ + color: var(--color-compose-recipient-box-text-color); + background-color: var(--color-compose-recipient-box-background-color); + border-color: var(--color-compose-recipient-box-border-color); &.dropdown-widget-button { padding: 0 6px; diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index ccf6f7909e..84d5863e8b 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -345,8 +345,7 @@ #custom-expiration-time-input, #organization-permissions .dropdown-widget-button, #organization-settings .dropdown-widget-button, - #stream-advanced-configurations .dropdown-widget-button, - #compose_recipient_box { + #stream-advanced-configurations .dropdown-widget-button { background-color: hsl(0deg 0% 0% / 20%); border-color: hsl(0deg 0% 0% / 60%); color: inherit; diff --git a/web/styles/input_pill.css b/web/styles/input_pill.css index 3900e52fee..3c808f8c9b 100644 --- a/web/styles/input_pill.css +++ b/web/styles/input_pill.css @@ -180,7 +180,7 @@ } #compose-direct-recipient .pill-container { - border: 1px solid hsl(0deg 0% 0% / 20%); + border: 1px solid var(--color-compose-recipient-box-border-color); background-color: var( --color-background-compose-direct-recipient-pill-container ); @@ -194,6 +194,10 @@ content: attr(data-some-recipients-text); opacity: 0.5; } + + &:has(.input:focus) { + border-color: var(--color-compose-recipient-box-has-focus); + } } #invitee_emails_container .pill-container { From 17561d09a114b0accd045ea35507c06aaeaee084 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Wed, 30 Oct 2024 11:56:37 -0500 Subject: [PATCH 130/276] compose: Give tab-focused widget wrapper sensible border. --- web/styles/compose.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/styles/compose.css b/web/styles/compose.css index 943b1a9158..235d1383c5 100644 --- a/web/styles/compose.css +++ b/web/styles/compose.css @@ -1432,6 +1432,14 @@ textarea.new_message_textarea { justify-content: flex-start; height: var(--compose-recipient-box-min-height); + &:focus-visible { + outline: 0; + + #compose_select_recipient_widget { + border-color: var(--color-compose-recipient-box-has-focus); + } + } + .dropdown_widget_value { flex-grow: 1; text-overflow: ellipsis; From 0cab8df6815864bd46bc15dd1081f0677743f0f2 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Wed, 30 Oct 2024 12:03:46 -0500 Subject: [PATCH 131/276] compose: Extend 4px border-radius to topic box. All similar elements in the compose box--the channel/DM widget, the pill container on DMs, and the compose textarea--all use a 4px border-radius, correcting the topic box's outlier status. --- web/styles/compose.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/styles/compose.css b/web/styles/compose.css index 235d1383c5..dda8132757 100644 --- a/web/styles/compose.css +++ b/web/styles/compose.css @@ -905,7 +905,7 @@ textarea.new_message_textarea { align-items: stretch; flex: 1 1 0; border: 1px solid var(--color-compose-recipient-box-border-color); - border-radius: 3px; + border-radius: 4px; transition: border-color 0.2s ease; background: var(--color-compose-recipient-box-background-color); From a7e6d5d770c6f7ac272002734a9a4fa9ada33de7 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Tue, 29 Oct 2024 16:39:06 +0530 Subject: [PATCH 132/276] settings: Remove unused fields passed to settings template. There is no need to pass the value of group settings to template as rendering the UI is handled in JS. This was probably added due to the old enum value setting being passed to the template. --- web/src/admin.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/src/admin.js b/web/src/admin.js index 6713c2575e..28014c466d 100644 --- a/web/src/admin.js +++ b/web/src/admin.js @@ -142,8 +142,6 @@ export function build_page() { language_list, realm_default_language_name: get_language_name(realm.realm_default_language), realm_default_language_code: realm.realm_default_language, - realm_direct_message_initiator_group_id: realm.realm_direct_message_initiator_group, - realm_direct_message_permission_group_id: realm.realm_direct_message_permission_group, realm_waiting_period_threshold: realm.realm_waiting_period_threshold, realm_new_stream_announcements_stream_id: realm.realm_new_stream_announcements_stream_id, realm_signup_announcements_stream_id: realm.realm_signup_announcements_stream_id, @@ -184,9 +182,6 @@ export function build_page() { realm_invite_required: realm.realm_invite_required, can_create_user_groups: settings_data.user_can_create_user_groups(), policy_values: settings_config.common_policy_values, - realm_can_delete_any_message_group: realm.realm_can_delete_any_message_group, - realm_can_delete_own_message_group: realm.realm_can_delete_own_message_group, - realm_can_add_custom_emoji_group: realm.realm_can_add_custom_emoji_group, realm_can_add_custom_emoji_group_name: user_groups.get_user_group_from_id( realm.realm_can_add_custom_emoji_group, ).name, From 072da3b0d39054cdae22c5c8db5b101451e684d2 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 30 Oct 2024 08:46:57 +0530 Subject: [PATCH 133/276] settings: Extract template file for group setting pill UI. This helps in writing same code again and again for different settings. Can also update group settings to use this template in further commits. --- .../group_setting_value_pill_input.hbs | 8 +++ .../organization_permissions_admin.hbs | 60 +++++-------------- 2 files changed, 23 insertions(+), 45 deletions(-) create mode 100644 web/templates/settings/group_setting_value_pill_input.hbs diff --git a/web/templates/settings/group_setting_value_pill_input.hbs b/web/templates/settings/group_setting_value_pill_input.hbs new file mode 100644 index 0000000000..b4b8371f89 --- /dev/null +++ b/web/templates/settings/group_setting_value_pill_input.hbs @@ -0,0 +1,8 @@ +
                              + +
                              +
                              + {{~! Squash whitespace so that placeholder is displayed when empty. ~}} +
                              +
                              +
                              diff --git a/web/templates/settings/organization_permissions_admin.hbs b/web/templates/settings/organization_permissions_admin.hbs index 666eff06e5..cc353f326c 100644 --- a/web/templates/settings/organization_permissions_admin.hbs +++ b/web/templates/settings/organization_permissions_admin.hbs @@ -23,15 +23,9 @@
                              -
                              - -
                              -
                              - {{~! Squash whitespace so that placeholder is displayed when empty. ~}} -
                              -
                              -
                              + {{> group_setting_value_pill_input + setting_name="realm_create_multiuse_invite_group" + label=(t 'Who can create reusable invitation links')}}
                              @@ -73,15 +67,9 @@ {{> settings_save_discard_widget section_name="stream-permissions" }}
                              -
                              - -
                              -
                              - {{~! Squash whitespace so that placeholder is displayed when empty. ~}} -
                              -
                              -
                              + {{> group_setting_value_pill_input + setting_name="realm_can_create_public_channel_group" + label=(t 'Who can create public channels')}} {{> upgrade_tip_widget }} {{> settings_checkbox @@ -97,15 +85,9 @@ label=(t 'Who can create web-public channels') value_type="number"}} -
                              - -
                              -
                              - {{~! Squash whitespace so that placeholder is displayed when empty. ~}} -
                              -
                              -
                              + {{> group_setting_value_pill_input + setting_name="realm_can_create_private_channel_group" + label=(t 'Who can create private channels')}}
                              @@ -349,25 +331,13 @@
                              -
                              - -
                              -
                              - {{~! Squash whitespace so that placeholder is displayed when empty. ~}} -
                              -
                              -
                              + {{> group_setting_value_pill_input + setting_name="realm_can_create_groups" + label=(t 'Who can create user groups')}} -
                              - -
                              -
                              - {{~! Squash whitespace so that placeholder is displayed when empty. ~}} -
                              -
                              -
                              + {{> group_setting_value_pill_input + setting_name="realm_can_manage_all_groups" + label=(t 'Who can manage user groups')}} {{> ../dropdown_widget_with_label widget_name="realm_can_add_custom_emoji_group" From 891e58bb1a4239528937cc47482fb6c6ed25625d Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 30 Oct 2024 17:26:33 +0530 Subject: [PATCH 134/276] settings: Fix live-update of setting elements. Some of the group setting elements were not live-updated correctly since they were not present in realm_settings dict and sync_realm_settings is only called for settings present in that dict. --- web/src/server_events_dispatch.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 01f0afe75f..bf8566839e 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -210,9 +210,15 @@ export function dispatch_normal_event(event) { allow_message_editing: noop, avatar_changes_disabled: settings_account.update_avatar_change_display, bot_creation_policy: settings_bots.update_bot_permissions_ui, + can_add_custom_emoji_group: noop, + can_create_groups: noop, + can_create_private_channel_group: noop, + can_create_public_channel_group: noop, can_delete_any_message_group: noop, can_delete_own_message_group: noop, + can_manage_all_groups: noop, can_move_messages_between_channels_group: noop, + can_move_messages_between_topics_group: noop, create_multiuse_invite_group: noop, invite_to_stream_policy: noop, default_code_block_language: noop, From 5fbc46f82c390f6260064e388f15cf590cb2b559 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Tue, 29 Oct 2024 22:55:18 +0530 Subject: [PATCH 135/276] settings: Fix banner shown on custom emoji panel. Previously the banner was always shown to admins and to users who cannot add emoji with the banner mentioning who can add emoji. This commit updates the code to only show the banners who cannot add emoji irrespective of their role, and just mention that they do not have permission without any detail about who can add. --- web/src/admin.js | 4 ---- web/src/settings_emoji.ts | 16 ++-------------- web/templates/settings/emoji_settings_admin.hbs | 4 ++-- web/templates/settings/emoji_settings_tip.hbs | 11 ----------- 4 files changed, 4 insertions(+), 31 deletions(-) delete mode 100644 web/templates/settings/emoji_settings_tip.hbs diff --git a/web/src/admin.js b/web/src/admin.js index 28014c466d..95649f9803 100644 --- a/web/src/admin.js +++ b/web/src/admin.js @@ -21,7 +21,6 @@ import * as settings_sections from "./settings_sections"; import * as settings_toggle from "./settings_toggle"; import * as settings_users from "./settings_users"; import {current_user, realm} from "./state_data"; -import * as user_groups from "./user_groups"; const admin_settings_label = { // Organization profile @@ -182,9 +181,6 @@ export function build_page() { realm_invite_required: realm.realm_invite_required, can_create_user_groups: settings_data.user_can_create_user_groups(), policy_values: settings_config.common_policy_values, - realm_can_add_custom_emoji_group_name: user_groups.get_user_group_from_id( - realm.realm_can_add_custom_emoji_group, - ).name, ...settings_org.get_organization_settings_options(), demote_inactive_streams_values: settings_config.demote_inactive_streams_values, web_mark_read_on_scroll_policy_values: diff --git a/web/src/settings_emoji.ts b/web/src/settings_emoji.ts index 9237603744..4fab99a473 100644 --- a/web/src/settings_emoji.ts +++ b/web/src/settings_emoji.ts @@ -6,7 +6,6 @@ import render_confirm_deactivate_custom_emoji from "../templates/confirm_dialog/ import emoji_settings_warning_modal from "../templates/confirm_dialog/confirm_emoji_settings_warning.hbs"; import render_add_emoji from "../templates/settings/add_emoji.hbs"; import render_admin_emoji_list from "../templates/settings/admin_emoji_list.hbs"; -import render_settings_emoji_settings_tip from "../templates/settings/emoji_settings_tip.hbs"; import * as channel from "./channel"; import * as confirm_dialog from "./confirm_dialog"; @@ -19,10 +18,9 @@ import * as loading from "./loading"; import * as people from "./people"; import * as scroll_util from "./scroll_util"; import * as settings_data from "./settings_data"; -import {current_user, realm} from "./state_data"; +import {current_user} from "./state_data"; import * as ui_report from "./ui_report"; import * as upload_widget from "./upload_widget"; -import * as user_groups from "./user_groups"; import * as util from "./util"; const meta = { @@ -44,12 +42,6 @@ function can_delete_emoji(emoji: ServerEmoji): boolean { } export function update_custom_emoji_ui(): void { - const rendered_tip = render_settings_emoji_settings_tip({ - realm_can_add_custom_emoji_group_name: user_groups.get_user_group_from_id( - realm.realm_can_add_custom_emoji_group, - ).name, - }); - $("#emoji-settings").find(".emoji-settings-tip-container").html(rendered_tip); if (!settings_data.user_can_add_custom_emoji()) { $(".add-emoji-text").hide(); $("#add-custom-emoji-button").hide(); @@ -58,11 +50,7 @@ export function update_custom_emoji_ui(): void { } else { $(".add-emoji-text").show(); $("#add-custom-emoji-button").show(); - if (current_user.is_admin) { - $("#emoji-settings .emoji-settings-tip-container").show(); - } else { - $("#emoji-settings .emoji-settings-tip-container").hide(); - } + $("#emoji-settings .emoji-settings-tip-container").hide(); $(".org-settings-list li[data-section='emoji-settings'] .locked").hide(); } diff --git a/web/templates/settings/emoji_settings_admin.hbs b/web/templates/settings/emoji_settings_admin.hbs index c8c6839a7d..c184972190 100644 --- a/web/templates/settings/emoji_settings_admin.hbs +++ b/web/templates/settings/emoji_settings_admin.hbs @@ -1,6 +1,6 @@
                              -
                              - {{> emoji_settings_tip}} +
                              +
                              {{t "You do not have permission to add custom emoji."}}

                              {{t "Add extra emoji for members of the {realm_name} organization." }} diff --git a/web/templates/settings/emoji_settings_tip.hbs b/web/templates/settings/emoji_settings_tip.hbs deleted file mode 100644 index ce999169a5..0000000000 --- a/web/templates/settings/emoji_settings_tip.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{#if is_guest}} -

                              {{t "Guests cannot edit custom emoji." }}
                              -{{else if (eq realm_can_add_custom_emoji_group_name "role:administrators") }} -
                              {{t "This organization is configured so that only administrators can add custom emoji." }}
                              -{{else if (eq realm_can_add_custom_emoji_group_name "role:moderators")}} -
                              {{t 'This organization is configured so that administrators and moderators can add custom emoji.'}}
                              -{{else if (eq realm_can_add_custom_emoji_group_name "role:fullmembers")}} -
                              {{t 'This organization is configured so that full members can add custom emoji.'}}
                              -{{else}} -
                              {{t "This organization is configured so that any member of this organization can add custom emoji." }}
                              -{{/if}} From dce229ba17dad2adc5c92cfc476216bc985259f3 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Tue, 29 Oct 2024 18:59:02 +0530 Subject: [PATCH 136/276] settings: Use new pills UI for DM permission settings. We now use pills UI for direct_message_initiator_group and direct_message_permission_group setting. --- web/src/admin.js | 4 -- web/src/compose_recipient.ts | 2 +- web/src/compose_validate.ts | 2 +- web/src/people.ts | 10 ++- web/src/server_events_dispatch.js | 4 +- web/src/settings_components.ts | 22 +++++-- web/src/settings_org.ts | 66 +++++++++++++------ web/src/state_data.ts | 4 +- web/src/user_groups.ts | 18 +++++ .../organization_permissions_admin.hbs | 15 ++--- web/tests/dispatch.test.js | 2 +- 11 files changed, 96 insertions(+), 53 deletions(-) diff --git a/web/src/admin.js b/web/src/admin.js index 95649f9803..6eb939a6e6 100644 --- a/web/src/admin.js +++ b/web/src/admin.js @@ -273,10 +273,6 @@ export function build_page() { tippy.default($("#realm_can_access_all_users_group_widget_container")[0], opts); } - - settings_org.check_disable_direct_message_initiator_group_dropdown( - realm.realm_direct_message_permission_group, - ); } export function launch(section, user_settings_tab) { diff --git a/web/src/compose_recipient.ts b/web/src/compose_recipient.ts index 21d46a4e0a..7f5b61e2e1 100644 --- a/web/src/compose_recipient.ts +++ b/web/src/compose_recipient.ts @@ -259,7 +259,7 @@ function get_options_for_recipient_widget(): Option[] { name: $t({defaultMessage: "Direct message"}), }; - if (!user_groups.is_empty_group(realm.realm_direct_message_permission_group)) { + if (!user_groups.is_setting_group_empty(realm.realm_direct_message_permission_group)) { options.unshift(direct_messages_option); } else { options.push(direct_messages_option); diff --git a/web/src/compose_validate.ts b/web/src/compose_validate.ts index 926749d84d..098aa2a0e4 100644 --- a/web/src/compose_validate.ts +++ b/web/src/compose_validate.ts @@ -114,7 +114,7 @@ export function needs_subscribe_warning(user_id: number, stream_id: number): boo export function check_dm_permissions_and_get_error_string(user_ids_string: string): string { if (!people.user_can_direct_message(user_ids_string)) { - if (user_groups.is_empty_group(realm.realm_direct_message_permission_group)) { + if (user_groups.is_setting_group_empty(realm.realm_direct_message_permission_group)) { return $t({ defaultMessage: "Direct messages are disabled in this organization.", }); diff --git a/web/src/people.ts b/web/src/people.ts index 851b5a774a..3ee4e65ec2 100644 --- a/web/src/people.ts +++ b/web/src/people.ts @@ -17,7 +17,7 @@ import * as settings_data from "./settings_data"; import type {StateData, profile_datum_schema, user_schema} from "./state_data"; import {current_user, realm} from "./state_data"; import * as timerender from "./timerender"; -import {is_user_in_group} from "./user_groups"; +import {is_user_in_setting_group} from "./user_groups"; import {user_settings} from "./user_settings"; import * as util from "./util"; @@ -767,9 +767,8 @@ export function should_add_guest_user_indicator(user_id: number): boolean { } export function user_can_initiate_direct_message_thread(recipient_ids_string: string): boolean { - const direct_message_initiator_group_id = realm.realm_direct_message_initiator_group; const recipient_ids = user_ids_string_to_ids_array(recipient_ids_string); - if (is_user_in_group(direct_message_initiator_group_id, my_user_id)) { + if (is_user_in_setting_group(realm.realm_direct_message_initiator_group, my_user_id)) { return true; } for (const recipient of recipient_ids) { @@ -781,9 +780,8 @@ export function user_can_initiate_direct_message_thread(recipient_ids_string: st } export function user_can_direct_message(recipient_ids_string: string): boolean { - const direct_message_permission_group_id = realm.realm_direct_message_permission_group; const recipient_ids = user_ids_string_to_ids_array(recipient_ids_string); - if (is_user_in_group(direct_message_permission_group_id, my_user_id)) { + if (is_user_in_setting_group(realm.realm_direct_message_permission_group, my_user_id)) { return true; } @@ -792,7 +790,7 @@ export function user_can_direct_message(recipient_ids_string: string): boolean { if (is_valid_bot_user(recipient_id) || recipient_id === my_user_id) { continue; } - if (is_user_in_group(direct_message_permission_group_id, recipient_id)) { + if (is_user_in_setting_group(realm.realm_direct_message_permission_group, recipient_id)) { return true; } other_human_recipients_exist = true; diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index bf8566839e..920581967d 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -321,9 +321,7 @@ export function dispatch_normal_event(event) { key === "direct_message_initiator_group" || key === "direct_message_permission_group" ) { - settings_org.check_disable_direct_message_initiator_group_dropdown( - realm.realm_direct_message_permission_group, - ); + settings_org.check_disable_direct_message_initiator_group_widget(); compose_closed_ui.update_buttons_for_private(); compose_recipient.check_posting_policy_for_compose_box(); } diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index c4699c2a48..81cc70333a 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -484,8 +484,6 @@ const dropdown_widget_map = new Map([ ["realm_can_delete_own_message_group", null], ["realm_can_move_messages_between_channels_group", null], ["realm_can_move_messages_between_topics_group", null], - ["realm_direct_message_initiator_group", null], - ["realm_direct_message_permission_group", null], ]); export function get_widget_for_dropdown_list_settings( @@ -803,15 +801,15 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_can_delete_own_message_group": case "realm_can_move_messages_between_channels_group": case "realm_can_move_messages_between_topics_group": - case "realm_direct_message_initiator_group": - case "realm_direct_message_permission_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; case "realm_can_create_groups": case "realm_can_create_public_channel_group": case "realm_can_create_private_channel_group": case "realm_can_manage_all_groups": - case "realm_create_multiuse_invite_group": { + case "realm_create_multiuse_invite_group": + case "realm_direct_message_initiator_group": + case "realm_direct_message_permission_group": { const pill_widget = get_group_setting_widget(property_name); assert(pill_widget !== null); proposed_val = get_group_setting_widget_value(pill_widget); @@ -1476,6 +1474,8 @@ export const group_setting_widget_map = new Map void; }): void { const pill_widget = group_setting_pill.create_pills($pill_container, setting_name, "realm"); const opts: { @@ -1612,9 +1616,15 @@ export function create_realm_group_setting_widget({ ".settings-subsection-parent", ); pill_widget.onPillCreate(() => { + if (pill_update_callback !== undefined) { + pill_update_callback(); + } save_discard_realm_settings_widget_status_handler($save_discard_widget_container); }); pill_widget.onPillRemove(() => { + if (pill_update_callback !== undefined) { + pill_update_callback(); + } save_discard_realm_settings_widget_status_handler($save_discard_widget_container); }); } diff --git a/web/src/settings_org.ts b/web/src/settings_org.ts index 5c8306d2a9..f6fb670cf7 100644 --- a/web/src/settings_org.ts +++ b/web/src/settings_org.ts @@ -438,11 +438,35 @@ function set_create_web_public_stream_dropdown_visibility(): void { ); } -export function check_disable_direct_message_initiator_group_dropdown(current_value: number): void { - if (user_groups.is_empty_group(current_value)) { - $("#realm_direct_message_initiator_group_widget").prop("disabled", true); - } else { - $("#realm_direct_message_initiator_group_widget").prop("disabled", false); +export function check_disable_direct_message_initiator_group_widget(): void { + const direct_message_permission_group_widget = settings_components.get_group_setting_widget( + "realm_direct_message_permission_group", + ); + if (direct_message_permission_group_widget === null) { + // direct_message_permission_group_widget can be null if + // the settings overlay is not opened yet. + return; + } + assert(direct_message_permission_group_widget !== null); + const direct_message_permission_value = settings_components.get_group_setting_widget_value( + direct_message_permission_group_widget, + ); + if (user_groups.is_setting_group_empty(direct_message_permission_value)) { + $("#id_realm_direct_message_initiator_group").find(".input").prop("contenteditable", false); + $("#id_realm_direct_message_initiator_group") + .closest(".input-group") + .addClass("group_setting_disabled"); + settings_components.disable_opening_typeahead_on_clicking_label( + $("#id_realm_direct_message_initiator_group").closest(".input-group"), + ); + } else if (current_user.is_admin) { + $("#id_realm_direct_message_initiator_group").find(".input").prop("contenteditable", true); + $("#id_realm_direct_message_initiator_group") + .closest(".input-group") + .removeClass("group_setting_disabled"); + settings_components.enable_opening_typeahead_on_clicking_label( + $("#id_realm_direct_message_initiator_group").closest(".input-group"), + ); } } @@ -559,9 +583,7 @@ function update_dependent_subsettings(property_name: string): void { set_create_web_public_stream_dropdown_visibility(); break; case "realm_direct_message_permission_group": - check_disable_direct_message_initiator_group_dropdown( - realm.realm_direct_message_permission_group, - ); + check_disable_direct_message_initiator_group_widget(); break; } } @@ -583,8 +605,6 @@ export function discard_realm_property_element_changes(elem: HTMLElement): void case "realm_signup_announcements_stream_id": case "realm_zulip_update_announcements_stream_id": case "realm_default_code_block_language": - case "realm_direct_message_initiator_group": - case "realm_direct_message_permission_group": case "realm_can_add_custom_emoji_group": case "realm_can_access_all_users_group": case "realm_can_create_web_public_channel_group": @@ -602,7 +622,9 @@ export function discard_realm_property_element_changes(elem: HTMLElement): void case "realm_can_create_public_channel_group": case "realm_can_create_private_channel_group": case "realm_can_manage_all_groups": - case "realm_create_multiuse_invite_group": { + case "realm_create_multiuse_invite_group": + case "realm_direct_message_initiator_group": + case "realm_direct_message_permission_group": { const pill_widget = settings_components.get_group_setting_widget(property_name); assert(pill_widget !== null); settings_components.set_group_setting_widget_value( @@ -963,6 +985,8 @@ export function set_up_dropdown_widget_for_realm_group_settings(): void { "can_create_private_channel_group", "can_manage_all_groups", "create_multiuse_invite_group", + "direct_message_initiator_group", + "direct_message_permission_group", ]); for (const setting_name of realm_group_permission_settings) { if (settings_using_pills_ui.has(setting_name)) { @@ -974,16 +998,6 @@ export function set_up_dropdown_widget_for_realm_group_settings(): void { | ((current_value: string | number | undefined) => void) | undefined; switch (setting_name) { - case "direct_message_permission_group": { - dropdown_list_item_click_callback = ( - current_value: string | number | undefined, - ): void => { - assert(typeof current_value === "number"); - check_disable_direct_message_initiator_group_dropdown(current_value); - }; - - break; - } case "can_delete_any_message_group": case "can_delete_own_message_group": { dropdown_list_item_click_callback = @@ -1193,8 +1207,18 @@ function initialize_group_setting_widgets(): void { $pill_container: $("#id_realm_can_manage_all_groups"), setting_name: "can_manage_all_groups", }); + settings_components.create_realm_group_setting_widget({ + $pill_container: $("#id_realm_direct_message_initiator_group"), + setting_name: "direct_message_initiator_group", + }); + settings_components.create_realm_group_setting_widget({ + $pill_container: $("#id_realm_direct_message_permission_group"), + setting_name: "direct_message_permission_group", + pill_update_callback: check_disable_direct_message_initiator_group_widget, + }); enable_or_disable_group_permission_settings(); + check_disable_direct_message_initiator_group_widget(); } export function build_page(): void { diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 571802fe09..5e46396aaa 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -313,8 +313,8 @@ export const realm_schema = z.object({ realm_description: z.string(), realm_digest_emails_enabled: z.boolean(), realm_digest_weekday: z.number(), - realm_direct_message_initiator_group: z.number(), - realm_direct_message_permission_group: z.number(), + realm_direct_message_initiator_group: group_setting_value_schema, + realm_direct_message_permission_group: group_setting_value_schema, realm_disallow_disposable_email_addresses: z.boolean(), realm_domains: z.array( z.object({ diff --git a/web/src/user_groups.ts b/web/src/user_groups.ts index fadaf54c42..d6dc0216ca 100644 --- a/web/src/user_groups.ts +++ b/web/src/user_groups.ts @@ -266,6 +266,24 @@ export function is_empty_group(user_group_id: number): boolean { return true; } +export function is_setting_group_empty(setting_group: GroupSettingValue): boolean { + if (typeof setting_group === "number") { + return is_empty_group(setting_group); + } + + if (setting_group.direct_members.length > 0) { + return false; + } + + for (const subgroup_id of setting_group.direct_subgroups) { + if (!is_empty_group(subgroup_id)) { + return false; + } + } + + return true; +} + export function get_user_groups_of_user(user_id: number): UserGroup[] { const user_groups_realm = get_realm_user_groups(); const groups_of_user = user_groups_realm.filter((group) => diff --git a/web/templates/settings/organization_permissions_admin.hbs b/web/templates/settings/organization_permissions_admin.hbs index cc353f326c..6a2dd15a5e 100644 --- a/web/templates/settings/organization_permissions_admin.hbs +++ b/web/templates/settings/organization_permissions_admin.hbs @@ -113,15 +113,14 @@ {{> settings_save_discard_widget section_name="direct-message-permissions" }}
                              - {{> ../dropdown_widget_with_label - widget_name="realm_direct_message_permission_group" - label=(t 'Who can authorize a direct message conversation') - value_type="number" }} - {{> ../dropdown_widget_with_label - widget_name="realm_direct_message_initiator_group" - label=(t 'Who can start a direct message conversation') - value_type="number" }} + {{> group_setting_value_pill_input + setting_name="realm_direct_message_permission_group" + label=(t 'Who can authorize a direct message conversation')}} + + {{> group_setting_value_pill_input + setting_name="realm_direct_message_initiator_group" + label=(t 'Who can start a direct message conversation')}}
                              diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index a518b15c62..e22dd1f73a 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -466,7 +466,7 @@ run_test("realm settings", ({override}) => { override(current_user, "is_admin", true); override(realm, "realm_date_created", new Date("2023-01-01Z")); - override(settings_org, "check_disable_direct_message_initiator_group_dropdown", noop); + override(settings_org, "check_disable_direct_message_initiator_group_widget", noop); override(settings_org, "sync_realm_settings", noop); override(settings_bots, "update_bot_permissions_ui", noop); override(settings_emoji, "update_custom_emoji_ui", noop); From fb1c7fffa20a906ae4701f947d8e11c380b0dd99 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Tue, 29 Oct 2024 15:59:01 +0530 Subject: [PATCH 137/276] settings: Use new UI for can_add_custom_emoji_group setting. --- web/src/settings_components.ts | 5 +++-- web/src/settings_org.ts | 7 ++++++- web/src/state_data.ts | 2 +- web/templates/settings/organization_permissions_admin.hbs | 7 +++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index 81cc70333a..7ed1b1be73 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -478,7 +478,6 @@ const dropdown_widget_map = new Map([ ["realm_default_code_block_language", null], ["can_remove_subscribers_group", null], ["realm_can_access_all_users_group", null], - ["realm_can_add_custom_emoji_group", null], ["realm_can_create_web_public_channel_group", null], ["realm_can_delete_any_message_group", null], ["realm_can_delete_own_message_group", null], @@ -795,7 +794,6 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_zulip_update_announcements_stream_id": case "realm_default_code_block_language": case "realm_can_access_all_users_group": - case "realm_can_add_custom_emoji_group": case "realm_can_create_web_public_channel_group": case "realm_can_delete_any_message_group": case "realm_can_delete_own_message_group": @@ -803,6 +801,7 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_can_move_messages_between_topics_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; + case "realm_can_add_custom_emoji_group": case "realm_can_create_groups": case "realm_can_create_public_channel_group": case "realm_can_create_private_channel_group": @@ -1469,6 +1468,7 @@ export const group_setting_widget_map = new Map ../dropdown_widget_with_label - widget_name="realm_can_add_custom_emoji_group" - label=(t 'Who can add custom emoji') - value_type="number"}} + {{> group_setting_value_pill_input + setting_name="realm_can_add_custom_emoji_group" + label=(t 'Who can add custom emoji')}}
                              From d9f4c473fb1723e42f9e71a2d3beb22d8f5f06a1 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 30 Oct 2024 11:51:51 +0530 Subject: [PATCH 138/276] settings: Do not disable time limit settings. Previously we disabled time limit settings for moving messages when non-admin and non-moderators users were not allowed move messages as the time limit does not apply to admins and moderators. And the time limit setting to delete messages was disabled when all the users who were allowed to delete their own message also had permission to delete any message since time limit does not apply to users who were allowed to delete any message. Now, as we use the new UI and allow the permission settings to be set to anonymous groups, we just do not disable the time limit setting to avoid complexity and we anyways mention about cases when time limit is not applicable. --- web/src/settings_org.ts | 129 --------------------------------- web/tests/settings_org.test.js | 2 - 2 files changed, 131 deletions(-) diff --git a/web/src/settings_org.ts b/web/src/settings_org.ts index c7889050a1..6d27aafaa4 100644 --- a/web/src/settings_org.ts +++ b/web/src/settings_org.ts @@ -271,90 +271,8 @@ function set_msg_edit_limit_dropdown(): void { settings_components.set_time_limit_setting("realm_message_content_edit_limit_seconds"); } -function message_move_limit_setting_enabled( - related_setting_name: - | "realm_can_move_messages_between_topics_group" - | "realm_can_move_messages_between_channels_group", -): boolean { - const user_group_id = settings_components.get_dropdown_list_widget_setting_value( - $(`#id_${related_setting_name}`), - ); - assert(typeof user_group_id === "number"); - const user_group_name = user_groups.get_user_group_from_id(user_group_id).name; - if ( - user_group_name === "role:administrators" || - user_group_name === "role:moderators" || - user_group_name === "role:nobody" - ) { - return false; - } - - return true; -} - -function enable_or_disable_related_message_move_time_limit_setting( - setting_name: MessageMoveTimeLimitSetting, - disable_setting: boolean, -): void { - const $setting_elem = $(`#id_${CSS.escape(setting_name)}`); - const $custom_input_elem = $setting_elem.parent().find(".time-limit-custom-input"); - - settings_ui.disable_sub_setting_onchange(disable_setting, $setting_elem.attr("id")!, true); - settings_ui.disable_sub_setting_onchange(disable_setting, $custom_input_elem.attr("id")!, true); -} - function set_msg_move_limit_setting(property_name: MessageMoveTimeLimitSetting): void { settings_components.set_time_limit_setting(property_name); - - let disable_setting; - if (property_name === "realm_move_messages_within_stream_limit_seconds") { - disable_setting = message_move_limit_setting_enabled( - "realm_can_move_messages_between_topics_group", - ); - } else { - disable_setting = message_move_limit_setting_enabled( - "realm_can_move_messages_between_channels_group", - ); - } - enable_or_disable_related_message_move_time_limit_setting(property_name, disable_setting); -} - -function message_delete_limit_setting_enabled(): boolean { - // This function is used to check whether the time-limit setting - // should be enabled. The setting is disabled when every user - // who is allowed to delete their own messages is also allowed - // to delete any message in the organization. - const realm_can_delete_own_message_group_id = - settings_components.get_dropdown_list_widget_setting_value( - $("#id_realm_can_delete_own_message_group"), - ); - const realm_can_delete_any_message_group_id = - settings_components.get_dropdown_list_widget_setting_value( - $("#id_realm_can_delete_any_message_group"), - ); - assert(typeof realm_can_delete_any_message_group_id === "number"); - const can_delete_any_message_subgroups = user_groups.get_recursive_subgroups( - user_groups.get_user_group_from_id(realm_can_delete_any_message_group_id), - ); - assert(can_delete_any_message_subgroups !== undefined); - can_delete_any_message_subgroups.add(realm_can_delete_any_message_group_id); - assert(typeof realm_can_delete_own_message_group_id === "number"); - return !can_delete_any_message_subgroups.has(realm_can_delete_own_message_group_id); -} - -function check_disable_message_delete_limit_setting_dropdown(): void { - settings_ui.disable_sub_setting_onchange( - message_delete_limit_setting_enabled(), - "id_realm_message_content_delete_limit_seconds", - true, - ); - if ($("#id_realm_message_content_delete_limit_minutes").length) { - settings_ui.disable_sub_setting_onchange( - message_delete_limit_setting_enabled(), - "id_realm_message_content_delete_limit_minutes", - true, - ); - } } function set_msg_delete_limit_dropdown(): void { @@ -550,18 +468,6 @@ function update_dependent_subsettings(property_name: string): void { case "realm_allow_message_editing": update_message_edit_sub_settings(realm.realm_allow_message_editing); break; - case "realm_can_delete_any_message_group": - check_disable_message_delete_limit_setting_dropdown(); - break; - case "realm_can_delete_own_message_group": - check_disable_message_delete_limit_setting_dropdown(); - break; - case "realm_can_move_messages_between_channels_group": - set_msg_move_limit_setting("realm_move_messages_between_streams_limit_seconds"); - break; - case "realm_can_move_messages_between_topics_group": - set_msg_move_limit_setting("realm_move_messages_within_stream_limit_seconds"); - break; case "realm_org_join_restrictions": set_org_join_restrictions_dropdown(); break; @@ -926,7 +832,6 @@ function set_up_dropdown_widget( setting_name: keyof Realm, setting_options: () => dropdown_widget.Option[], setting_type: string, - custom_dropdown_widget_callback?: (current_value: string | number | undefined) => void, ): void { const $save_discard_widget_container = $(`#id_${CSS.escape(setting_name)}`).closest( ".settings-subsection-parent", @@ -955,9 +860,6 @@ function set_up_dropdown_widget( settings_components.save_discard_realm_settings_widget_status_handler( $save_discard_widget_container, ); - if (custom_dropdown_widget_callback !== undefined) { - custom_dropdown_widget_callback(this_widget.current_value); - } }, default_id: z.union([z.string(), z.number()]).parse(realm[setting_name]), unique_id_type, @@ -995,39 +897,10 @@ export function set_up_dropdown_widget_for_realm_group_settings(): void { } const get_setting_options = (): UserGroupForDropdownListWidget[] => user_groups.get_realm_user_groups_for_dropdown_list_widget(setting_name, "realm"); - let dropdown_list_item_click_callback: - | ((current_value: string | number | undefined) => void) - | undefined; - switch (setting_name) { - case "can_delete_any_message_group": - case "can_delete_own_message_group": { - dropdown_list_item_click_callback = - check_disable_message_delete_limit_setting_dropdown; - - break; - } - case "can_move_messages_between_channels_group": { - dropdown_list_item_click_callback = () => { - set_msg_move_limit_setting("realm_move_messages_between_streams_limit_seconds"); - }; - - break; - } - case "can_move_messages_between_topics_group": { - dropdown_list_item_click_callback = () => { - set_msg_move_limit_setting("realm_move_messages_within_stream_limit_seconds"); - }; - - break; - } - // No default - } - set_up_dropdown_widget( realm_schema.keyof().parse("realm_" + setting_name), get_setting_options, "group", - dropdown_list_item_click_callback, ); } } @@ -1436,8 +1309,6 @@ export function build_page(): void { }); } - check_disable_message_delete_limit_setting_dropdown(); - realm_icon.build_realm_icon_widget(upload_realm_logo_or_icon); if (realm.zulip_plan_is_not_limited) { realm_logo.build_realm_logo_widget(upload_realm_logo_or_icon, false); diff --git a/web/tests/settings_org.test.js b/web/tests/settings_org.test.js index 66af886a2c..14f13161c8 100644 --- a/web/tests/settings_org.test.js +++ b/web/tests/settings_org.test.js @@ -492,8 +492,6 @@ function test_discard_changes_button({override}, discard_changes) { } test("set_up", ({override, override_rewire}) => { - override_rewire(settings_org, "check_disable_message_delete_limit_setting_dropdown", noop); - override_rewire(settings_org, "message_move_limit_setting_enabled", noop); override(realm, "realm_available_video_chat_providers", { jitsi_meet: { id: 1, From b8bc20e87ca6749f13debfdb20a2d77ec6dcdb8c Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 30 Oct 2024 12:20:59 +0530 Subject: [PATCH 139/276] settings: Use new pills UI for move message permission settings. --- web/src/settings_components.ts | 10 ++++++---- web/src/settings_org.ts | 14 ++++++++++++-- web/src/state_data.ts | 4 ++-- .../settings/organization_permissions_admin.hbs | 14 ++++++-------- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index 7ed1b1be73..d77a519707 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -481,8 +481,6 @@ const dropdown_widget_map = new Map([ ["realm_can_create_web_public_channel_group", null], ["realm_can_delete_any_message_group", null], ["realm_can_delete_own_message_group", null], - ["realm_can_move_messages_between_channels_group", null], - ["realm_can_move_messages_between_topics_group", null], ]); export function get_widget_for_dropdown_list_settings( @@ -797,8 +795,6 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_can_create_web_public_channel_group": case "realm_can_delete_any_message_group": case "realm_can_delete_own_message_group": - case "realm_can_move_messages_between_channels_group": - case "realm_can_move_messages_between_topics_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; case "realm_can_add_custom_emoji_group": @@ -806,6 +802,8 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_can_create_public_channel_group": case "realm_can_create_private_channel_group": case "realm_can_manage_all_groups": + case "realm_can_move_messages_between_channels_group": + case "realm_can_move_messages_between_topics_group": case "realm_create_multiuse_invite_group": case "realm_direct_message_initiator_group": case "realm_direct_message_permission_group": { @@ -1473,6 +1471,8 @@ export const group_setting_widget_map = new Map settings_save_discard_widget section_name="moving-msgs" }}
                              - {{> ../dropdown_widget_with_label - widget_name="realm_can_move_messages_between_topics_group" - label=(t 'Who can move messages to another topic') - value_type="number" }} + {{> group_setting_value_pill_input + setting_name="realm_can_move_messages_between_topics_group" + label=(t 'Who can move messages to another topic')}}
                              @@ -196,10 +195,9 @@
                              - {{> ../dropdown_widget_with_label - widget_name="realm_can_move_messages_between_channels_group" - label=(t 'Who can move messages to another channel') - value_type="number" }} + {{> group_setting_value_pill_input + setting_name="realm_can_move_messages_between_channels_group" + label=(t 'Who can move messages to another channel')}}
                              From 8e0a8dfa3226c6316d6c7be6ff49346abe2133d5 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 30 Oct 2024 12:45:27 +0530 Subject: [PATCH 140/276] settings: Use new pills UI for message delete permissions. --- web/src/settings_components.ts | 10 ++++++---- web/src/settings_org.ts | 14 ++++++++++++-- web/src/state_data.ts | 4 ++-- .../settings/organization_permissions_admin.hbs | 14 ++++++-------- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index d77a519707..7916fcc4e9 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -479,8 +479,6 @@ const dropdown_widget_map = new Map([ ["can_remove_subscribers_group", null], ["realm_can_access_all_users_group", null], ["realm_can_create_web_public_channel_group", null], - ["realm_can_delete_any_message_group", null], - ["realm_can_delete_own_message_group", null], ]); export function get_widget_for_dropdown_list_settings( @@ -793,14 +791,14 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea case "realm_default_code_block_language": case "realm_can_access_all_users_group": case "realm_can_create_web_public_channel_group": - case "realm_can_delete_any_message_group": - case "realm_can_delete_own_message_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; case "realm_can_add_custom_emoji_group": case "realm_can_create_groups": case "realm_can_create_public_channel_group": case "realm_can_create_private_channel_group": + case "realm_can_delete_any_message_group": + case "realm_can_delete_own_message_group": case "realm_can_manage_all_groups": case "realm_can_move_messages_between_channels_group": case "realm_can_move_messages_between_topics_group": @@ -1470,6 +1468,8 @@ export const group_setting_widget_map = new Map settings_save_discard_widget section_name="msg-deletion" }}
                              - {{> ../dropdown_widget_with_label - widget_name="realm_can_delete_any_message_group" - label=(t 'Who can delete any message') - value_type="number" }} + {{> group_setting_value_pill_input + setting_name="realm_can_delete_any_message_group" + label=(t 'Who can delete any message')}} - {{> ../dropdown_widget_with_label - widget_name="realm_can_delete_own_message_group" - label=(t 'Who can delete their own messages') - value_type="number" }} + {{> group_setting_value_pill_input + setting_name="realm_can_delete_own_message_group" + label=(t 'Who can delete their own messages')}}
                              +
                              + +
                              ) icon in the upper right to show it. -1. Click the **search** () icon at the top of the right sidebar to open the - search box. - -1. Type the name of the user you are looking for. +1. Type the name of the user you are looking for in the search box. !!! keyboard_tip "" diff --git a/web/e2e-tests/message-basics.test.ts b/web/e2e-tests/message-basics.test.ts index 7bcc170670..eceed790ba 100644 --- a/web/e2e-tests/message-basics.test.ts +++ b/web/e2e-tests/message-basics.test.ts @@ -429,7 +429,7 @@ async function test_users_search(page: Page): Promise { await assert_in_list(page, "aaron"); // Enter the search box and test selected suggestion navigation - await page.click("#user_filter_icon"); + await page.click(".user-list-filter"); await page.waitForSelector("#buddy-list-other-users .highlighted_user", {visible: true}); await assert_selected(page, "Desdemona"); await assert_not_selected(page, "Cordelia, Lear's daughter"); diff --git a/web/src/activity_ui.ts b/web/src/activity_ui.ts index cd49215a90..7148b8077f 100644 --- a/web/src/activity_ui.ts +++ b/web/src/activity_ui.ts @@ -212,7 +212,7 @@ export function narrow_for_user_id(opts: {user_id: number}): void { assert(narrow_by_email); narrow_by_email(email); assert(user_filter !== undefined); - user_filter.clear_and_hide_search(); + user_filter.clear_search(); } function keydown_enter_key(): void { @@ -274,9 +274,9 @@ export function initiate_search(): void { } } -export function escape_search(): void { +export function clear_search(): void { if (user_filter) { - user_filter.clear_and_hide_search(); + user_filter.clear_search(); } } diff --git a/web/src/buddy_list.ts b/web/src/buddy_list.ts index ca16f19deb..82378a3974 100644 --- a/web/src/buddy_list.ts +++ b/web/src/buddy_list.ts @@ -77,7 +77,6 @@ type BuddyListRenderData = { pm_ids_set: Set; total_human_subscribers_count: number; other_users_count: number; - total_human_users: number; hide_headers: boolean; all_participant_ids: Set; }; @@ -87,8 +86,7 @@ function get_render_data(): BuddyListRenderData { const pm_ids_set = narrow_state.pm_ids_set(); const total_human_subscribers_count = get_total_human_subscriber_count(current_sub, pm_ids_set); - const total_human_users = people.get_active_human_count(); - const other_users_count = total_human_users - total_human_subscribers_count; + const other_users_count = people.get_active_human_count() - total_human_subscribers_count; const hide_headers = should_hide_headers(current_sub, pm_ids_set); const all_participant_ids = buddy_data.get_conversation_participants(); @@ -97,7 +95,6 @@ function get_render_data(): BuddyListRenderData { pm_ids_set, total_human_subscribers_count, other_users_count, - total_human_users, hide_headers, all_participant_ids, }; @@ -247,9 +244,8 @@ export class BuddyList extends BuddyListConf { ); } } else { - const total_human_users = people.get_active_human_count(); const other_users_count = - total_human_users - total_human_subscribers_count; + people.get_active_human_count() - total_human_subscribers_count; tooltip_text = $t( { defaultMessage: @@ -429,8 +425,7 @@ export class BuddyList extends BuddyListConf { } this.current_filter = narrow_state.filter(); - const {current_sub, total_human_subscribers_count, other_users_count, total_human_users} = - this.render_data; + const {current_sub, total_human_subscribers_count, other_users_count} = this.render_data; $(".buddy-list-subsection-header").empty(); // If we're in the mode of hiding headers, that means we're only showing the "other users" @@ -446,16 +441,9 @@ export class BuddyList extends BuddyListConf { $("#buddy-list-users-matching-view-container").toggleClass("no-display", true); } - // Usually we show the user counts in the headers, but if we're hiding - // those headers then we show the total user count in the main title. - const default_userlist_title = $t({defaultMessage: "USERS"}); if (hide_headers) { - const formatted_count = get_formatted_sub_count(total_human_users); - const userlist_title = `${default_userlist_title} (${formatted_count})`; - $("#userlist-title").text(userlist_title); return; } - $("#userlist-title").text(default_userlist_title); let header_text; if (current_sub) { diff --git a/web/src/hotkey.js b/web/src/hotkey.js index 0c6fb3f2ef..735a9bbaf7 100644 --- a/web/src/hotkey.js +++ b/web/src/hotkey.js @@ -306,7 +306,7 @@ export function process_escape_key(e) { if (processing_text()) { if (activity_ui.searching()) { - activity_ui.escape_search(); + activity_ui.clear_search(); return true; } diff --git a/web/src/resize.ts b/web/src/resize.ts index 52ddfabf02..53b12e694a 100644 --- a/web/src/resize.ts +++ b/web/src/resize.ts @@ -33,8 +33,7 @@ function get_new_heights(): { const usable_height = viewport_height - Number.parseInt($("#right-sidebar").css("paddingTop"), 10) - - ($("#userlist-header").outerHeight(true) ?? 0) - - ($("#user_search_section:not(.notdisplayed)").outerHeight(true) ?? 0); + ($("#userlist-header").outerHeight(true) ?? 0); const buddy_list_wrapper_max_height = Math.max(80, usable_height); diff --git a/web/src/tippyjs.ts b/web/src/tippyjs.ts index d2c6f1d5b0..8079251829 100644 --- a/web/src/tippyjs.ts +++ b/web/src/tippyjs.ts @@ -698,6 +698,7 @@ export function initialize(): void { tippy.delegate("body", { target: "#userlist-header-search", + delay: LONG_HOVER_DELAY, placement: "top", appendTo: () => document.body, onShow(instance) { diff --git a/web/src/user_search.ts b/web/src/user_search.ts index e66ea11dad..4898942eef 100644 --- a/web/src/user_search.ts +++ b/web/src/user_search.ts @@ -3,7 +3,6 @@ import assert from "minimalistic-assert"; import * as buddy_data from "./buddy_data"; import * as popovers from "./popovers"; -import * as resize from "./resize"; import * as sidebar_ui from "./sidebar_ui"; export class UserSearch { @@ -11,7 +10,7 @@ export class UserSearch { // above the buddy list. We rely on other code to manage the // details of populating the list when we change. - $widget = $("#user_search_section").expectOne(); + $widget = $("#userlist-header-search").expectOne(); $input = $("input.user-list-filter").expectOne(); _reset_items: () => void; _update_list: () => void; @@ -25,12 +24,11 @@ export class UserSearch { $("#clear_search_people_button").on("click", () => { this.clear_search(); }); - $("#userlist-header-search").on("click", () => { - this.toggle_filter_displayed(); - }); this.$input.on("input", () => { - buddy_data.set_is_searching_users(this.$input.val() !== ""); + const input_is_empty = this.$input.val() === ""; + buddy_data.set_is_searching_users(!input_is_empty); + $("#clear_search_people_button").toggleClass("hidden", input_is_empty); opts.update_list(); }); this.$input.on("focus", (e) => { @@ -52,54 +50,17 @@ export class UserSearch { return this.$input.is(":focus"); } - empty(): boolean { - return this.text() === ""; - } - // This clears search input but doesn't close // the search widget unless it was already empty. clear_search(): void { buddy_data.set_is_searching_users(false); - - if (this.empty()) { - this.close_widget(); - return; - } + $("#clear_search_people_button").toggleClass("hidden", true); this.$input.val(""); this.$input.trigger("blur"); this._reset_items(); } - // This always clears and closes search. - clear_and_hide_search(): void { - this.clear_search(); - this._update_list(); - this.close_widget(); - } - - hide_widget(): void { - this.$widget.addClass("notdisplayed"); - resize.resize_sidebars(); - } - - show_widget(): void { - // Hide all the popovers. - popovers.hide_all(); - this.$widget.removeClass("notdisplayed"); - resize.resize_sidebars(); - } - - widget_shown(): boolean { - return this.$widget.hasClass("notdisplayed"); - } - - close_widget(): void { - this.$input.trigger("blur"); - this.hide_widget(); - this._reset_items(); - } - expand_column(): void { const $column = this.$input.closest(".app-main [class^='column-']"); if (!$column.hasClass("expanded")) { @@ -114,21 +75,12 @@ export class UserSearch { initiate_search(): void { this.expand_column(); - this.show_widget(); // Needs to be called when input is visible after fix_invite_user_button_flicker. setTimeout(() => { this.$input.trigger("focus"); }, 0); } - toggle_filter_displayed(): void { - if (this.widget_shown()) { - this.initiate_search(); - } else { - this.clear_and_hide_search(); - } - } - on_focus(e: JQuery.FocusEvent): void { this._on_focus(); e.stopPropagation(); diff --git a/web/styles/right_sidebar.css b/web/styles/right_sidebar.css index ba32252d0a..60be2c9d61 100644 --- a/web/styles/right_sidebar.css +++ b/web/styles/right_sidebar.css @@ -363,37 +363,59 @@ $user_status_emoji_width: 24px; grid-template-rows: var(--line-height-sidebar-row-prominent); grid-template-columns: minmax(0, 1fr) auto; align-items: center; + margin-bottom: 10px; #userlist-header-search { display: grid; grid-template-rows: var(--line-height-sidebar-row-prominent); - grid-template-columns: minmax(0, 1fr) 20px; + grid-template-columns: minmax(0, 1fr) 30px; align-items: center; - } - #userlist-title { - margin: 0; - } + & .user-list-filter { + grid-area: 1 / 1 / 2 / 3; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + /* Prevent text from colliding with #clear_search_button */ + padding-right: 28px; + height: var(--line-height-sidebar-row-prominent); + box-sizing: border-box; + } - #user_filter_icon { - opacity: 0.5; - justify-self: center; + #clear_search_people_button { + grid-area: 1 / 2 / 2 / 3; + padding: 0; + background: none; + color: var(--color-text-clear-search-button); + display: grid; - &:hover { - opacity: 1; - cursor: pointer; + &:hover { + color: var(--color-text-clear-search-button-hover); + } + + &:focus, + &:focus-visible, + &:active { + box-shadow: none; + outline: none; + } + + .zulip-icon-close { + align-self: center; + } } } - /* hovering over the userlist-header creates the same highlight effect as hovering over the user_filter_icon */ - &:hover > #user_filter_icon { - opacity: 1; - cursor: pointer; - } - #buddy-list-menu-icon { + color: var(--color-vdots-visible); justify-content: center; display: grid; + width: 25px; + margin-left: 5px; + + &:hover { + color: var(--color-vdots-hover); + } } } @@ -407,29 +429,3 @@ $user_status_emoji_width: 24px; from the legacy value. */ margin-top: calc(25px - (var(--legacy-body-line-height-unitless) * 1em)); } - -#user_search_section { - display: grid; - grid-template-columns: minmax(0, 1fr) 28px; - grid-template-rows: var(--line-height-sidebar-row-prominent); - white-space: nowrap; - margin-bottom: 10px; - - & .user-list-filter { - grid-area: 1 / 1 / 2 / 3; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - /* Prevent text from colliding with .clear_search_button */ - padding-right: 28px; - /* Push back against inherited styles; let CSS Grid be in - charge of the height. */ - height: auto; - } - - .clear_search_button { - grid-area: 1 / 2 / 2 / 3; - position: static; - padding: 0; - } -} diff --git a/web/templates/right_sidebar.hbs b/web/templates/right_sidebar.hbs index 7d6e80d695..157a64a2f2 100644 --- a/web/templates/right_sidebar.hbs +++ b/web/templates/right_sidebar.hbs @@ -2,22 +2,16 @@ From 523dc7e2be5ce18f9d6a46eb777cff4a55dc91b8 Mon Sep 17 00:00:00 2001 From: opmkumar Date: Mon, 28 Oct 2024 01:36:37 +0530 Subject: [PATCH 166/276] invite-user-modal: Add general class names for improved extensibility. Adds general class names in invite_user_modal.hbs for custom time the input and unit so that these elements more easily be extended for use in other modals with a user specified custom time. Updates the listener in invite.ts that was using the removed "custom-expiration-time" class to instead use the id for the input and unit div, "custom-invite-expiration-time". Corresponding updates have been made to the relevant CSS files to ensure consistent styling and future scalability. Co-authored-by: Ujjawal Modi Co-authored-by: Lauryn Menard --- web/src/invite.ts | 2 +- web/styles/dark_theme.css | 2 +- web/styles/zulip.css | 6 +++--- web/templates/invite_user_modal.hbs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/invite.ts b/web/src/invite.ts index 1b5d966c0f..626abf7220 100644 --- a/web/src/invite.ts +++ b/web/src/invite.ts @@ -451,7 +451,7 @@ function open_invite_user_modal(e: JQuery.ClickEvent): void } }); - $(".custom-expiration-time").on("change", () => { + $("#custom-invite-expiration-time").on("change", () => { custom_expiration_time_input = util.check_time_input( $("input#custom-expiration-time-input").val()!, ); diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index 84d5863e8b..cce37efb8b 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -342,7 +342,7 @@ select, .pill-container, .user-status-content-wrapper, - #custom-expiration-time-input, + .custom-time-input-value, #organization-permissions .dropdown-widget-button, #organization-settings .dropdown-widget-button, #stream-advanced-configurations .dropdown-widget-button { diff --git a/web/styles/zulip.css b/web/styles/zulip.css index eeb1c9755e..7b9886e5b9 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -1261,7 +1261,7 @@ div.toggle_resolve_topic_spinner .loading_indicator_spinner { } } -#custom-expiration-time-input, +.custom-time-input-value, #invite-user-form { margin: 0; } @@ -1285,7 +1285,7 @@ div.toggle_resolve_topic_spinner .loading_indicator_spinner { width: 100%; } -#custom-expiration-time-input { +.custom-time-input-value { width: 5ch; margin-right: 15px; @@ -1307,7 +1307,7 @@ div.toggle_resolve_topic_spinner .loading_indicator_spinner { } } -#custom-expiration-time-unit { +.custom-time-input-unit { width: auto; } diff --git a/web/templates/invite_user_modal.hbs b/web/templates/invite_user_modal.hbs index fdcade3f55..cda8a092ee 100644 --- a/web/templates/invite_user_modal.hbs +++ b/web/templates/invite_user_modal.hbs @@ -43,8 +43,8 @@

                              - - +

                              -
                              +
                              -

                              +

                              @@ -49,7 +49,7 @@ {{/each}} -

                              +

                              From b677a62f641f8b7898a1746e0d26bf8cd031852f Mon Sep 17 00:00:00 2001 From: Maneesh Shukla Date: Wed, 9 Oct 2024 20:24:07 +0530 Subject: [PATCH 170/276] portico: Customise "Find your accounts" page. Updates "Find your accounts" page to display a relevant message for self-hosted servers, with no changes to the Zulip Cloud version of the page. Fixes part of #30116. --- templates/zerver/find_account.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/zerver/find_account.html b/templates/zerver/find_account.html index 4b88d14283..7e15c9c2fe 100644 --- a/templates/zerver/find_account.html +++ b/templates/zerver/find_account.html @@ -38,12 +38,12 @@ {% else %} -
                              + From 257ce8bca22da96e9d86f62b1a7e0267274ff58b Mon Sep 17 00:00:00 2001 From: aniebietafia Date: Tue, 8 Oct 2024 23:02:24 +0100 Subject: [PATCH 172/276] integrations: Create incoming webhook for Airbyte. Note about the documentation: There are currently two "Save changes" buttons on the Airbyte "Notifications" settings page, so the instructions specify which one to use for clarity. --- static/images/integrations/airbyte/001.png | Bin 0 -> 39721 bytes .../integrations/bot_avatars/airbyte.png | Bin 0 -> 3137 bytes static/images/integrations/logos/airbyte.svg | Bin 0 -> 2244 bytes zerver/lib/integrations.py | 2 + zerver/webhooks/airbyte/__init__.py | 0 zerver/webhooks/airbyte/doc.md | 28 +++++ .../fixtures/airbyte_job_payload_failure.json | 83 +++++++++++++++ .../fixtures/airbyte_job_payload_success.json | 83 +++++++++++++++ .../test_airbyte_job_hello_world_failure.json | 3 + .../test_airbyte_job_hello_world_success.json | 4 + zerver/webhooks/airbyte/tests.py | 70 +++++++++++++ zerver/webhooks/airbyte/view.py | 97 ++++++++++++++++++ 12 files changed, 370 insertions(+) create mode 100644 static/images/integrations/airbyte/001.png create mode 100644 static/images/integrations/bot_avatars/airbyte.png create mode 100644 static/images/integrations/logos/airbyte.svg create mode 100644 zerver/webhooks/airbyte/__init__.py create mode 100644 zerver/webhooks/airbyte/doc.md create mode 100644 zerver/webhooks/airbyte/fixtures/airbyte_job_payload_failure.json create mode 100644 zerver/webhooks/airbyte/fixtures/airbyte_job_payload_success.json create mode 100644 zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_failure.json create mode 100644 zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_success.json create mode 100644 zerver/webhooks/airbyte/tests.py create mode 100644 zerver/webhooks/airbyte/view.py diff --git a/static/images/integrations/airbyte/001.png b/static/images/integrations/airbyte/001.png new file mode 100644 index 0000000000000000000000000000000000000000..f90bf6e885c78a07e4aa88e2930f99957172e20c GIT binary patch literal 39721 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

                              #VX?PZe*@A0>%2*M;1 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 bc<2SeC&iPOyV1HREa0V~qODvFvkCt%Z7K|9 literal 0 HcmV?d00001 diff --git a/static/images/integrations/bot_avatars/airbyte.png b/static/images/integrations/bot_avatars/airbyte.png new file mode 100644 index 0000000000000000000000000000000000000000..1c563d017dad624310a027c6dcec70820ae24b99 GIT binary patch literal 3137 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 W0Df2iS5+GgW-0CrYQuj{Zhitj2DlLb literal 0 HcmV?d00001 diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index 6c94734bb4..a8274eb413 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -343,6 +343,7 @@ EMBEDDED_BOTS: list[EmbeddedBotIntegration] = [ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [ WebhookIntegration("airbrake", ["monitoring"]), + WebhookIntegration("airbyte", ["monitoring"]), WebhookIntegration( "alertmanager", ["monitoring"], @@ -712,6 +713,7 @@ NO_SCREENSHOT_WEBHOOKS = { DOC_SCREENSHOT_CONFIG: dict[str, list[BaseScreenshotConfig]] = { "airbrake": [ScreenshotConfig("error_message.json")], + "airbyte": [ScreenshotConfig("airbyte_job_payload_success.json")], "alertmanager": [ ScreenshotConfig("alert.json", extra_params={"name": "topic", "desc": "description"}) ], diff --git a/zerver/webhooks/airbyte/__init__.py b/zerver/webhooks/airbyte/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zerver/webhooks/airbyte/doc.md b/zerver/webhooks/airbyte/doc.md new file mode 100644 index 0000000000..488384a035 --- /dev/null +++ b/zerver/webhooks/airbyte/doc.md @@ -0,0 +1,28 @@ +# Zulip Airbyte integration + +Get Zulip notifications from Airbyte. + +{start_tabs} + +1. {!create-channel.md!} + +1. {!create-an-incoming-webhook.md!} + +1. {!generate-webhook-url-basic.md!} + +1. In Airbyte, go to your project settings. Click **Notifications**, + and toggle the **Webhook** button for the notifications you'd like + to receive. + +1. Enter the URL generated above in the **Webhook URL** field. Click the + **Save changes** button at the bottom of the page. + +{end_tabs} + +{!congrats.md!} + +![](/static/images/integrations/airbyte/001.png) + +### Related documentation + +{!webhooks-url-specification.md!} diff --git a/zerver/webhooks/airbyte/fixtures/airbyte_job_payload_failure.json b/zerver/webhooks/airbyte/fixtures/airbyte_job_payload_failure.json new file mode 100644 index 0000000000..bb8a76f9d7 --- /dev/null +++ b/zerver/webhooks/airbyte/fixtures/airbyte_job_payload_failure.json @@ -0,0 +1,83 @@ +{ + "text": "Your connection Google Sheets → Postgres from Google Sheets to Postgres failed\nThis happened with Checking source connection failed - please review this connection's configuration to prevent future syncs from failing\n\nYou can access its logs here: https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/connections/aa941643-07ea-48a2-9035-024575491720\n\nJob ID: 20441143", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Sync completed: " + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Source:*" + }, + { + "type": "mrkdwn", + "text": "" + }, + { + "type": "mrkdwn", + "text": "*Destination:*" + }, + { + "type": "mrkdwn", + "text": "" + }, + { + "type": "mrkdwn", + "text": "*Duration:*" + }, + { + "type": "mrkdwn", + "text": "1 min 23 sec" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Sync Summary:*\n1400 record(s) extracted / 1400 record(s) loaded\n281 kB extracted / 281 kB loaded\n" + } + } + ], + "data": { + "workspace": { + "id": "84d2dd6e-82aa-406e-91f3-bf8dbf176e69", + "name": "Zulip Airbyte Integration", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69" + }, + "connection": { + "id": "aa941643-07ea-48a2-9035-024575491720", + "name": "Google Sheets → Postgres", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/connections/aa941643-07ea-48a2-9035-024575491720" + }, + "source": { + "id": "363c0ea3-e989-4051-9f54-d41b794d6621", + "name": "Google Sheets", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/source/363c0ea3-e989-4051-9f54-d41b794d6621" + }, + "destination": { + "id": "b3a05072-e3c8-435a-8e6e-4a5c601039c6", + "name": "Postgres", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/destination/b3a05072-e3c8-435a-8e6e-4a5c601039c6" + }, + "jobId": 20441143, + "startedAt": "2024-10-22T20:27:59Z", + "finishedAt": "2024-10-22T20:29:22Z", + "bytesEmitted": 0, + "bytesCommitted": 0, + "recordsEmitted": 0, + "recordsCommitted": 0, + "errorMessage": "Checking source connection failed - please review this connection's configuration to prevent future syncs from failing", + "durationFormatted": "28 sec", + "bytesEmittedFormatted": "0 B", + "bytesCommittedFormatted": "0 B", + "success": false, + "durationInSeconds": 28 + } +} diff --git a/zerver/webhooks/airbyte/fixtures/airbyte_job_payload_success.json b/zerver/webhooks/airbyte/fixtures/airbyte_job_payload_success.json new file mode 100644 index 0000000000..d573f8b63d --- /dev/null +++ b/zerver/webhooks/airbyte/fixtures/airbyte_job_payload_success.json @@ -0,0 +1,83 @@ +{ + "text": "Your connection Google Sheets → Postgres from Google Sheets to Postgres succeeded\nThis was for null\n\nYou can access its logs here: https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/connections/aa941643-07ea-48a2-9035-024575491720\n\nJob ID: 20441143", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Sync completed: " + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Source:*" + }, + { + "type": "mrkdwn", + "text": "" + }, + { + "type": "mrkdwn", + "text": "*Destination:*" + }, + { + "type": "mrkdwn", + "text": "" + }, + { + "type": "mrkdwn", + "text": "*Duration:*" + }, + { + "type": "mrkdwn", + "text": "1 min 23 sec" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Sync Summary:*\n1400 record(s) extracted / 1400 record(s) loaded\n281 kB extracted / 281 kB loaded\n" + } + } + ], + "data": { + "workspace": { + "id": "84d2dd6e-82aa-406e-91f3-bf8dbf176e69", + "name": "Zulip Airbyte Integration", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69" + }, + "connection": { + "id": "aa941643-07ea-48a2-9035-024575491720", + "name": "Google Sheets → Postgres", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/connections/aa941643-07ea-48a2-9035-024575491720" + }, + "source": { + "id": "363c0ea3-e989-4051-9f54-d41b794d6621", + "name": "Google Sheets", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/source/363c0ea3-e989-4051-9f54-d41b794d6621" + }, + "destination": { + "id": "b3a05072-e3c8-435a-8e6e-4a5c601039c6", + "name": "Postgres", + "url": "https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/destination/b3a05072-e3c8-435a-8e6e-4a5c601039c6" + }, + "jobId": 20441143, + "startedAt": "2024-10-22T20:27:59Z", + "finishedAt": "2024-10-22T20:29:22Z", + "bytesEmitted": 288179, + "bytesCommitted": 288179, + "recordsEmitted": 1400, + "recordsCommitted": 1400, + "errorMessage": null, + "durationFormatted": "1 min 23 sec", + "bytesEmittedFormatted": "281 kB", + "bytesCommittedFormatted": "281 kB", + "success": true, + "durationInSeconds": 83 + } +} diff --git a/zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_failure.json b/zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_failure.json new file mode 100644 index 0000000000..5df5ad4aed --- /dev/null +++ b/zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_failure.json @@ -0,0 +1,3 @@ +{ + "text": "Hello World! This is a test from Airbyte to try slack notification settings for sync failures." +} diff --git a/zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_success.json b/zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_success.json new file mode 100644 index 0000000000..1323fd41c0 --- /dev/null +++ b/zerver/webhooks/airbyte/fixtures/test_airbyte_job_hello_world_success.json @@ -0,0 +1,4 @@ +{ + "text": "Hello World! This is a test from Airbyte to try slack notification settings for sync successes." +} + diff --git a/zerver/webhooks/airbyte/tests.py b/zerver/webhooks/airbyte/tests.py new file mode 100644 index 0000000000..1a997bc292 --- /dev/null +++ b/zerver/webhooks/airbyte/tests.py @@ -0,0 +1,70 @@ +from zerver.lib.test_classes import WebhookTestCase + + +class AirbyteHookTests(WebhookTestCase): + STREAM_NAME = "airbyte" + URL_TEMPLATE = "/api/v1/external/airbyte?api_key={api_key}&stream={stream}" + FIXTURE_DIR_NAME = "airbyte" + CHANNEL_NAME = "test" + WEBHOOK_DIR_NAME = "airbyte" + + def test_airbyte_job_success(self) -> None: + expected_topic = "Zulip Airbyte Integration - Google Sheets → Postgres" + + expected_message = """:green_circle: Airbyte sync **succeeded** for [Google Sheets → Postgres](https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/connections/aa941643-07ea-48a2-9035-024575491720). + + +* **Source:** [Google Sheets](https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/source/363c0ea3-e989-4051-9f54-d41b794d6621) +* **Destination:** [Postgres](https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/destination/b3a05072-e3c8-435a-8e6e-4a5c601039c6) +* **Records:** 1400 emitted, 1400 committed +* **Bytes:** 281 kB emitted, 281 kB committed +* **Duration:** 1 min 23 sec""" + + self.check_webhook( + "airbyte_job_payload_success", + expected_topic, + expected_message, + content_type="application/json", + ) + + def test_airbyte_job_failure(self) -> None: + expected_topic = "Zulip Airbyte Integration - Google Sheets → Postgres" + expected_message = """:red_circle: Airbyte sync **failed** for [Google Sheets → Postgres](https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/connections/aa941643-07ea-48a2-9035-024575491720). + + +* **Source:** [Google Sheets](https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/source/363c0ea3-e989-4051-9f54-d41b794d6621) +* **Destination:** [Postgres](https://cloud.airbyte.com/workspaces/84d2dd6e-82aa-406e-91f3-bf8dbf176e69/destination/b3a05072-e3c8-435a-8e6e-4a5c601039c6) +* **Records:** 0 emitted, 0 committed +* **Bytes:** 0 B emitted, 0 B committed +* **Duration:** 28 sec + +**Error message:** Checking source connection failed - please review this connection's configuration to prevent future syncs from failing""" + + self.check_webhook( + "airbyte_job_payload_failure", + expected_topic, + expected_message, + content_type="application/json", + ) + + def test_airbyte_job_hello_world_success(self) -> None: + expected_topic = "Airbyte notification" + expected_message = """Hello World! This is a test from Airbyte to try slack notification settings for sync successes.""" + + self.check_webhook( + "test_airbyte_job_hello_world_success", + expected_topic, + expected_message, + content_type="application/json", + ) + + def test_airbyte_job_hello_world_failure(self) -> None: + expected_topic = "Airbyte notification" + expected_message = """Hello World! This is a test from Airbyte to try slack notification settings for sync failures.""" + + self.check_webhook( + "test_airbyte_job_hello_world_failure", + expected_topic, + expected_message, + content_type="application/json", + ) diff --git a/zerver/webhooks/airbyte/view.py b/zerver/webhooks/airbyte/view.py new file mode 100644 index 0000000000..f422d0b144 --- /dev/null +++ b/zerver/webhooks/airbyte/view.py @@ -0,0 +1,97 @@ +# Webhooks for external integrations. + +from django.http import HttpRequest, HttpResponse + +from zerver.decorator import webhook_view +from zerver.lib.response import json_success +from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint +from zerver.lib.validator import WildValue, check_bool, check_int, check_string +from zerver.lib.webhooks.common import check_send_webhook_message +from zerver.models import UserProfile + +AIRBYTE_TOPIC_TEMPLATE = "{workspace} - {connection}" + +AIRBYTE_MESSAGE_TEMPLATE = """\ +{sync_status_emoji} Airbyte sync **{status}** for [{connection_name}]({connection_url}). + + +* **Source:** [{source_name}]({source_url}) +* **Destination:** [{destination_name}]({destination_url}) +* **Records:** {records_emitted} emitted, {records_committed} committed +* **Bytes:** {bytes_emitted} emitted, {bytes_committed} committed +* **Duration:** {duration} +""" + + +def extract_data_from_payload(payload_data: WildValue) -> dict[str, str | int | bool]: + data: dict[str, str | int | bool] = { + "workspace_name": payload_data["workspace"]["name"].tame(check_string), + "connection_name": payload_data["connection"]["name"].tame(check_string), + "source_name": payload_data["source"]["name"].tame(check_string), + "destination_name": payload_data["destination"]["name"].tame(check_string), + "connection_url": payload_data["connection"]["url"].tame(check_string), + "source_url": payload_data["source"]["url"].tame(check_string), + "destination_url": payload_data["destination"]["url"].tame(check_string), + "successful_sync": payload_data["success"].tame(check_bool), + "duration_formatted": payload_data["durationFormatted"].tame(check_string), + "records_emitted": payload_data["recordsEmitted"].tame(check_int), + "records_committed": payload_data["recordsCommitted"].tame(check_int), + "bytes_emitted_formatted": payload_data["bytesEmittedFormatted"].tame(check_string), + "bytes_committed_formatted": payload_data["bytesCommittedFormatted"].tame(check_string), + } + + if not data["successful_sync"]: + data["error_message"] = payload_data["errorMessage"].tame(check_string) + + return data + + +def format_message_from_data(data: dict[str, str | int | bool]) -> str: + content = AIRBYTE_MESSAGE_TEMPLATE.format( + sync_status_emoji=":green_circle:" if data["successful_sync"] else ":red_circle:", + status="succeeded" if data["successful_sync"] else "failed", + connection_name=data["connection_name"], + connection_url=data["connection_url"], + source_name=data["source_name"], + source_url=data["source_url"], + destination_name=data["destination_name"], + destination_url=data["destination_url"], + duration=data["duration_formatted"], + records_emitted=data["records_emitted"], + records_committed=data["records_committed"], + bytes_emitted=data["bytes_emitted_formatted"], + bytes_committed=data["bytes_committed_formatted"], + ) + + if not data["successful_sync"]: + error_message = data["error_message"] + content += f"\n**Error message:** {error_message}" + + return content + + +def create_topic_from_data(data: dict[str, str | int | bool]) -> str: + return AIRBYTE_TOPIC_TEMPLATE.format( + workspace=data["workspace_name"], + connection=data["connection_name"], + ) + + +@webhook_view("Airbyte") +@typed_endpoint +def api_airbyte_webhook( + request: HttpRequest, + user_profile: UserProfile, + *, + payload: JsonBodyPayload[WildValue], +) -> HttpResponse: + if "data" in payload: + data = extract_data_from_payload(payload["data"]) + content = format_message_from_data(data) + topic = create_topic_from_data(data) + else: + # Test Airbyte notification payloads only contain this field. + content = payload["text"].tame(check_string) + topic = "Airbyte notification" + check_send_webhook_message(request, user_profile, topic, content) + return json_success(request) From d37ebef0c00f8838728ab05adee5ba4565bc5d97 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Fri, 1 Nov 2024 11:27:41 -0700 Subject: [PATCH 173/276] docs: Fix typos in new contintuing work guide. --- docs/contributing/continuing-unfinished-work.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/continuing-unfinished-work.md b/docs/contributing/continuing-unfinished-work.md index c4d92bde04..4fc291d3ae 100644 --- a/docs/contributing/continuing-unfinished-work.md +++ b/docs/contributing/continuing-unfinished-work.md @@ -67,10 +67,10 @@ When you use or build upon someone else's unmerged work, it is both professionally and ethically necessary to [properly credit][coauthor-git-guide] their contributions in the commit history of work that you submit. Git, used properly, does a good job of -preserving commits originally authorship information. +preserving the original authorship of commits. However, it's normal to find yourself making changes to commits -originally authored by others contributors, whether resolving merge +originally authored by other contributors, whether resolving merge conflicts when doing `git rebase` or fixing bugs to create an atomically correct commit compliant with Zulip's [commit guidelines](../contributing/commit-discipline.md). From 9ad85445f83ce242b58b9735e39a40ed24f458e4 Mon Sep 17 00:00:00 2001 From: Maneesh Shukla Date: Wed, 23 Oct 2024 19:58:19 +0530 Subject: [PATCH 174/276] settings: Add a class to the target span. This commit adds a specific class to the target span element, making the code more robust and less reliant on generic selectors. Fixes part of #26691. --- web/templates/dialog_widget.hbs | 2 +- web/templates/user_profile_modal.hbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/templates/dialog_widget.hbs b/web/templates/dialog_widget.hbs index f187e92b7d..ecba618cb4 100644 --- a/web/templates/dialog_widget.hbs +++ b/web/templates/dialog_widget.hbs @@ -20,7 +20,7 @@ {{/unless}}

                              diff --git a/web/templates/user_profile_modal.hbs b/web/templates/user_profile_modal.hbs index 84cb9d77f8..508c5c0a39 100644 --- a/web/templates/user_profile_modal.hbs +++ b/web/templates/user_profile_modal.hbs @@ -140,7 +140,7 @@
                              From 666e7bf433f8b1b4dd957d875c445210dfb06717 Mon Sep 17 00:00:00 2001 From: Maneesh Shukla Date: Thu, 17 Oct 2024 00:25:47 +0530 Subject: [PATCH 175/276] settings: Extract common logic for show_spinner. Consolidate the repeated logic for showing spinners into a shared `show_spinner` function in `loading.ts`. This eliminates code duplication between `show_button_spinner` and `show_dialog_spinner`, streamlining spinner initialization and button disabling. Fixes part of #26691. --- web/src/dialog_widget.ts | 11 +---------- web/src/loading.ts | 15 +++++++++++++++ web/src/user_profile.ts | 8 +------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/web/src/dialog_widget.ts b/web/src/dialog_widget.ts index db81a4e1ec..ee879afe29 100644 --- a/web/src/dialog_widget.ts +++ b/web/src/dialog_widget.ts @@ -102,17 +102,8 @@ export function show_dialog_spinner(): void { $(`${dialog_widget_selector} .modal__btn`).prop("disabled", true); const $spinner = $(`${dialog_widget_selector} .modal__spinner`); - const dialog_submit_button_span_width = $(".dialog_submit_button span").width(); - const dialog_submit_button_span_height = $(".dialog_submit_button span").height(); - // Hide the submit button after computing its height, since submit - // buttons with long text might affect the size of the button. - $(".dialog_submit_button span").hide(); - - loading.make_indicator($spinner, { - width: dialog_submit_button_span_width, - height: dialog_submit_button_span_height, - }); + loading.show_spinner($(".dialog_submit_button"), $spinner); } // Supports a callback to be called once the modal finishes closing. diff --git a/web/src/loading.ts b/web/src/loading.ts index 1d74935121..f504ba6f57 100644 --- a/web/src/loading.ts +++ b/web/src/loading.ts @@ -91,3 +91,18 @@ export function show_button_spinner($elt: JQuery, using_dark_theme: boolean): vo } $elt.css("display", "inline-block"); } + +export function show_spinner($button_element: JQuery, $spinner: JQuery): void { + const span_width = $button_element.find(".submit-button-text").width(); + const span_height = $button_element.find(".submit-button-text").height(); + + // Hide the submit button after computing its height, since submit + // buttons with long text might affect the size of the button. + $button_element.find(".submit-button-text").hide(); + + // Create the loading indicator + make_indicator($spinner, { + width: span_width, + height: span_height, + }); +} diff --git a/web/src/user_profile.ts b/web/src/user_profile.ts index d0156e552e..98a99c31bd 100644 --- a/web/src/user_profile.ts +++ b/web/src/user_profile.ts @@ -88,14 +88,8 @@ const EMBEDDED_BOT_TYPE = "4"; export function show_button_spinner($button: JQuery): void { const $spinner = $button.find(".modal__spinner"); - const dialog_submit_button_span_width = $button.find("span").width(); - const dialog_submit_button_span_height = $button.find("span").height(); $button.prop("disabled", true); - $button.find("span").hide(); - loading.make_indicator($spinner, { - width: dialog_submit_button_span_width, - height: dialog_submit_button_span_height, - }); + loading.show_spinner($button, $spinner); } export function hide_button_spinner($button: JQuery): void { From 158cb6c7470953fd0709d198f306cf4c43cd12a8 Mon Sep 17 00:00:00 2001 From: Maneesh Shukla Date: Thu, 17 Oct 2024 00:29:20 +0530 Subject: [PATCH 176/276] settings: Extract common logic for hide_spinners. Move the redundant code for hiding spinners and re-enabling buttons into a common `hide_spinner` function inside `loading.ts`. This reduces duplication between `hide_button_spinner` and `hide_dialog_spinner`. Fixes #26691. --- web/src/dialog_widget.ts | 5 ++--- web/src/loading.ts | 8 ++++++++ web/src/user_profile.ts | 3 +-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/web/src/dialog_widget.ts b/web/src/dialog_widget.ts index ee879afe29..a1155752af 100644 --- a/web/src/dialog_widget.ts +++ b/web/src/dialog_widget.ts @@ -88,12 +88,11 @@ type RequestOpts = { }; export function hide_dialog_spinner(): void { - $(".dialog_submit_button span").show(); const dialog_widget_selector = current_dialog_widget_selector(); + const $spinner = $(`${dialog_widget_selector} .modal__spinner`); $(`${dialog_widget_selector} .modal__btn`).prop("disabled", false); - const $spinner = $(`${dialog_widget_selector} .modal__spinner`); - loading.destroy_indicator($spinner); + loading.hide_spinner($(".dialog_submit_button"), $spinner); } export function show_dialog_spinner(): void { diff --git a/web/src/loading.ts b/web/src/loading.ts index f504ba6f57..cf9173cf01 100644 --- a/web/src/loading.ts +++ b/web/src/loading.ts @@ -106,3 +106,11 @@ export function show_spinner($button_element: JQuery, $spinner: JQuery): void { height: span_height, }); } + +export function hide_spinner($button_element: JQuery, $spinner: JQuery): void { + // Show the span + $button_element.find(".submit-button-text").show(); + + // Destroy the loading indicator + destroy_indicator($spinner); +} diff --git a/web/src/user_profile.ts b/web/src/user_profile.ts index 98a99c31bd..4e0342f552 100644 --- a/web/src/user_profile.ts +++ b/web/src/user_profile.ts @@ -95,8 +95,7 @@ export function show_button_spinner($button: JQuery): void { export function hide_button_spinner($button: JQuery): void { const $spinner = $button.find(".modal__spinner"); $button.prop("disabled", false); - $button.find("span").show(); - loading.destroy_indicator($spinner); + loading.hide_spinner($button, $spinner); } function compare_by_name( From e4a2bf1f299484172735c5c7e6e31116fe844f33 Mon Sep 17 00:00:00 2001 From: Alya Abbott Date: Thu, 31 Oct 2024 17:40:51 -0700 Subject: [PATCH 177/276] help: Update docs to describe group-based permissions. --- help/create-user-groups.md | 7 +++++++ ...view-organization-settings-instructions.md | 6 ++++-- help/include/sidebar_index.md | 3 +-- help/include/user-groups-applications.md | 9 +++++++++ help/include/user-groups-intro.md | 18 ++++-------------- help/manage-user-groups.md | 6 ++++++ help/moving-to-zulip.md | 3 +++ help/review-your-organization-settings.md | 19 ------------------- help/roles-and-permissions.md | 5 +++-- help/user-groups.md | 2 ++ 10 files changed, 39 insertions(+), 39 deletions(-) create mode 100644 help/include/user-groups-applications.md delete mode 100644 help/review-your-organization-settings.md diff --git a/help/create-user-groups.md b/help/create-user-groups.md index 11969156ae..6e32aa8420 100644 --- a/help/create-user-groups.md +++ b/help/create-user-groups.md @@ -2,6 +2,13 @@ {!user-groups-intro.md!} +Many organizations find it helpful to create groups for: + +- Each team, e.g., “mobile”, “design”, or “IT”. +- Leadership roles, e.g., “managers”, “engineering-leads”. + +{!user-groups-applications.md!} + ## How to create a user group {!how-to-create-a-user-group.md!} diff --git a/help/include/review-organization-settings-instructions.md b/help/include/review-organization-settings-instructions.md index 50b7236871..50c8fd5d46 100644 --- a/help/include/review-organization-settings-instructions.md +++ b/help/include/review-organization-settings-instructions.md @@ -1,5 +1,7 @@ -Review the settings for your organization to set everything up how you -want it to be. +Review the settings for your organization to set everything up how you want it +to be. + +{!user-groups-intro.md!} {start_tabs} diff --git a/help/include/sidebar_index.md b/help/include/sidebar_index.md index 44a4c30225..dbc93afd40 100644 --- a/help/include/sidebar_index.md +++ b/help/include/sidebar_index.md @@ -23,11 +23,11 @@ ## Setting up your organization * [Migrating from other chat tools](/help/migrating-from-other-chat-tools) * [Create your organization profile](/help/create-your-organization-profile) +* [Create user groups](/help/create-user-groups) * [Customize organization settings](/help/customize-organization-settings) * [Create channels](/help/create-channels) * [Customize settings for new users](/help/customize-settings-for-new-users) * [Invite users to join](/help/invite-users-to-join) -* [Create user groups](/help/create-user-groups) * [Set up integrations](/help/set-up-integrations) ## Account basics @@ -168,7 +168,6 @@ # Zulip administration ## Organization basics -* [Review your organization's settings](/help/review-your-organization-settings) * [Organization type](/help/organization-type) * [Communities directory](/help/communities-directory) * [Import from Mattermost](/help/import-from-mattermost) diff --git a/help/include/user-groups-applications.md b/help/include/user-groups-applications.md new file mode 100644 index 0000000000..4ba1f71ea8 --- /dev/null +++ b/help/include/user-groups-applications.md @@ -0,0 +1,9 @@ +Groups provide an easy way to refer to multiple users at once. You can: + +- [Mention](/help/mention-a-user-or-group) a group of users, + [notifying](/help/dm-mention-alert-notifications) everyone in the group as if + they were personally mentioned. +- Compose a [direct message](/help/direct-messages) to a user group. This + automatically puts all the users in the group into the addressee field. +- Subscribe a user group to a channel. This individually subscribes all the users + in the group. diff --git a/help/include/user-groups-intro.md b/help/include/user-groups-intro.md index d9bd4dfd72..0618ab30ff 100644 --- a/help/include/user-groups-intro.md +++ b/help/include/user-groups-intro.md @@ -1,14 +1,4 @@ -User groups make it easier to manage any organization with multiple teams or job -functions. You can: - -- Assign permissions to user groups. -- Add a user group to another user group. -- Subscribe a user group to a channel. This individually subscribes all the users - in the group. -- [Mention](/help/mention-a-user-or-group) a group of users, - [notifying](/help/dm-mention-alert-notifications) everyone in the group as if - they were personally mentioned. -- Compose a [direct message](/help/direct-messages) to a user group. This - automatically puts all the users in the group into the addressee field. - -You may want to create a user group for each team in your organization. +User groups offer a flexible way to manage permissions in your organization. +Most permissions in Zulip can be granted to any combination of +[roles](/help/roles-and-permissions), [groups](/help/user-groups), and +individual [users](/help/manage-a-user). diff --git a/help/manage-user-groups.md b/help/manage-user-groups.md index f23cced441..58247afc08 100644 --- a/help/manage-user-groups.md +++ b/help/manage-user-groups.md @@ -2,6 +2,8 @@ {!user-groups-intro.md!} +{!user-groups-applications.md!} + ## Create a user group !!! tip "" @@ -62,6 +64,10 @@ ## Add groups and users to a group +You can add users to a group, or add a group to any other group. Nesting groups +makes them easier to maintain. For example, moving a user from one team group to +another can automatically update what department group they belong to. + {start_tabs} {tab|desktop-web} diff --git a/help/moving-to-zulip.md b/help/moving-to-zulip.md index 9d6786079e..93d3eec27a 100644 --- a/help/moving-to-zulip.md +++ b/help/moving-to-zulip.md @@ -116,6 +116,9 @@ you will need to upgrade your plan. 1. [Create your organization profile](/help/create-your-organization-profile), which is displayed on your organization's registration and login pages. +1. [Create user groups](/help/create-user-groups), which offer a flexible way to + manage permissions. + 1. Review [organization permissions](/help/roles-and-permissions), such as who can invite users, create channels, etc. diff --git a/help/review-your-organization-settings.md b/help/review-your-organization-settings.md deleted file mode 100644 index 22bbdfeded..0000000000 --- a/help/review-your-organization-settings.md +++ /dev/null @@ -1,19 +0,0 @@ -# Review your organization settings - -It's good to periodically review your organization's settings. Zulip is in -active development and is constantly adding new features, and as your -organization grows, features that may not have been applicable at a small -scale may be applicable now. - -Note that most organization settings are visible to all users, though -generally only organization administrators can interact with them. - -### Review your organization settings - -{start_tabs} - -{relative|gear|organization-settings} - -1. Click on each tab on the left. - -{end_tabs} diff --git a/help/roles-and-permissions.md b/help/roles-and-permissions.md index 0fe3b2d0fb..7692461cca 100644 --- a/help/roles-and-permissions.md +++ b/help/roles-and-permissions.md @@ -1,12 +1,12 @@ # Roles and permissions -## User roles - User roles make it convenient to configure different permissions for different users in your organization. You can decide what role a user will have when you [send them an invitation](/help/invite-new-users), and later [change a user's role](/help/roles-and-permissions#change-a-users-role) if needed. +{!user-groups-intro.md!} + !!! tip "" Learn about [channel permissions](/help/channel-permissions), including @@ -65,6 +65,7 @@ role](/help/roles-and-permissions#change-a-users-role) if needed. ## Related articles * [Change a user's role](/help/change-a-users-role) +* [User groups](/help/user-groups) * [Channel permissions](/help/channel-permissions) * [Inviting new users](/help/invite-new-users) * [Zulip Cloud billing](/help/zulip-cloud-billing) diff --git a/help/user-groups.md b/help/user-groups.md index 1d523d5e37..326a4ce574 100644 --- a/help/user-groups.md +++ b/help/user-groups.md @@ -2,6 +2,8 @@ {!user-groups-intro.md!} +{!user-groups-applications.md!} + ## Browse and join user groups {start_tabs} From 0f30c93a2fd0dea3f365a5155b5c25dfeaa05b49 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Fri, 1 Nov 2024 14:11:53 -0500 Subject: [PATCH 178/276] left_sidebar: Solidify grid placement of filter-clearing button. --- web/styles/left_sidebar.css | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index fc97e9c401..1e19cbf620 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -81,11 +81,10 @@ margin: 3px 0; .clear_search_button { - grid-area: row-content; - justify-self: self-end; + grid-area: clear-button; /* Override app-component positioning. */ position: static; - padding-right: 4px; + padding: 0; } } @@ -1098,12 +1097,11 @@ li.top_left_scheduled_messages { .topic_search_section { grid-template-columns: - var(--left-sidebar-toggle-width-offset) 0 0 [filter-box-start] minmax( - 0, - 1fr - ) + var(--left-sidebar-toggle-width-offset) 0 0 + [filter-box-start] minmax(0, 1fr) minmax(0, max-content) - 30px [filter-box-end] 0; + [clear-button-start] var(--left-sidebar-vdots-width) + [clear-button-end filter-box-end] 0; } .topic-box .zero_count { From 8dd0d7f48d8c64289dbb307a0c2f47b55c2d3560 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Fri, 1 Nov 2024 12:56:14 -0700 Subject: [PATCH 179/276] =?UTF-8?q?reindex-textual-data:=20Remove=20Postgr?= =?UTF-8?q?eSQL=20=E2=89=A5=2011=20check.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We removed PostgreSQL 10 support long ago in 6.0-beta1~88. Signed-off-by: Anders Kaseorg --- scripts/setup/reindex-textual-data | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/setup/reindex-textual-data b/scripts/setup/reindex-textual-data index 3557672268..954a9955ff 100755 --- a/scripts/setup/reindex-textual-data +++ b/scripts/setup/reindex-textual-data @@ -48,12 +48,6 @@ pg_args["connect_timeout"] = "600" conn = psycopg2.connect(connection_factory=None, **pg_args) conn.autocommit = True -pg_server_version = conn.server_version -can_concurrently = pg_server_version >= 110000 # Version 11.0.0 - -if options.concurrently and not can_concurrently: - raise RuntimeError("Only PostgreSQL 11 and above can REINDEX CONCURRENTLY.") - cursor = conn.cursor() cursor.execute( """ From 2bb87aebec239760b7904509c690b26c9989b1d7 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Fri, 1 Nov 2024 12:51:40 -0700 Subject: [PATCH 180/276] install: Remove PostgreSQL 12 support. PostgreSQL 12 reaches end of life on November 14, 2024. Signed-off-by: Anders Kaseorg --- docs/overview/changelog.md | 4 + docs/production/deployment.md | 2 +- docs/production/install-existing-server.md | 2 +- puppet/zulip/manifests/profile/postgresql.pp | 2 +- .../12/postgresql.conf.template.erb | 812 ------------------ scripts/lib/check-database-compatibility | 4 +- scripts/lib/install | 4 +- 7 files changed, 11 insertions(+), 819 deletions(-) delete mode 100644 puppet/zulip/templates/postgresql/12/postgresql.conf.template.erb diff --git a/docs/overview/changelog.md b/docs/overview/changelog.md index 7d4d8ed404..07367a50e9 100644 --- a/docs/overview/changelog.md +++ b/docs/overview/changelog.md @@ -21,6 +21,10 @@ _Unreleased_ synchronizing role, and otherwise functions like the old one, except Zulip custom profile fields are referred to with the prefix `custom__`. See the updated comment documentation in `/etc/zulip/settings.py` for details. +- PostgreSQL 12 is no longer supported; if you are currently using it, + you will need to [upgrade + PostgreSQL](../production/upgrade.md#upgrading-postgresql) before + upgrading Zulip. ## Zulip Server 9.x series diff --git a/docs/production/deployment.md b/docs/production/deployment.md index 35ebf37993..0b37fee39a 100644 --- a/docs/production/deployment.md +++ b/docs/production/deployment.md @@ -68,7 +68,7 @@ as well as those mentioned in the [install](install.md#installer-options) documentation: - `--postgresql-version`: Sets the version of PostgreSQL that will be - installed. We currently support PostgreSQL 12, 13, 14, 15, and 16, with 16 + installed. We currently support PostgreSQL 13, 14, 15, and 16, with 16 being the default. - `--postgresql-database-name=exampledbname`: With this option, you diff --git a/docs/production/install-existing-server.md b/docs/production/install-existing-server.md index 11a9a9f871..4566f41728 100644 --- a/docs/production/install-existing-server.md +++ b/docs/production/install-existing-server.md @@ -75,7 +75,7 @@ $ sudo service puppet stop ### PostgreSQL -Zulip expects to install PostgreSQL 12, and find that listening on +Zulip expects to install PostgreSQL 16, and find that listening on port 5432; any other version of PostgreSQL that is detected at install time will cause the install to abort. If you already have PostgreSQL installed, you can pass `--postgresql-version=` to the installer to diff --git a/puppet/zulip/manifests/profile/postgresql.pp b/puppet/zulip/manifests/profile/postgresql.pp index 933af589b6..40d223a924 100644 --- a/puppet/zulip/manifests/profile/postgresql.pp +++ b/puppet/zulip/manifests/profile/postgresql.pp @@ -30,7 +30,7 @@ class zulip::profile::postgresql { group => 'postgres', } - if $version in ['12','13','14'] { + if $version in ['13','14'] { $postgresql_conf_file = "${zulip::postgresql_base::postgresql_confdir}/postgresql.conf" file { $postgresql_conf_file: ensure => file, diff --git a/puppet/zulip/templates/postgresql/12/postgresql.conf.template.erb b/puppet/zulip/templates/postgresql/12/postgresql.conf.template.erb deleted file mode 100644 index 7c0fea8fd0..0000000000 --- a/puppet/zulip/templates/postgresql/12/postgresql.conf.template.erb +++ /dev/null @@ -1,812 +0,0 @@ -# ----------------------------- -# PostgreSQL configuration file -# ----------------------------- -# -# This file consists of lines of the form: -# -# name = value -# -# (The "=" is optional.) Whitespace may be used. Comments are introduced with -# "#" anywhere on a line. The complete list of parameter names and allowed -# values can be found in the PostgreSQL documentation. -# -# The commented-out settings shown in this file represent the default values. -# Re-commenting a setting is NOT sufficient to revert it to the default value; -# you need to reload the server. -# -# This file is read on server startup and when the server receives a SIGHUP -# signal. If you edit the file on a running system, you have to SIGHUP the -# server for the changes to take effect, run "pg_ctl reload", or execute -# "SELECT pg_reload_conf()". Some parameters, which are marked below, -# require a server shutdown and restart to take effect. -# -# Any parameter can also be given as a command-line option to the server, e.g., -# "postgres -c log_connections=on". Some parameters can be changed at run time -# with the "SET" SQL command. -# -# Memory units: kB = kilobytes Time units: ms = milliseconds -# MB = megabytes s = seconds -# GB = gigabytes min = minutes -# TB = terabytes h = hours -# d = days - - -#------------------------------------------------------------------------------ -# FILE LOCATIONS -#------------------------------------------------------------------------------ - -# The default values of these variables are driven from the -D command-line -# option or PGDATA environment variable, represented here as ConfigDir. - -data_directory = '<%= scope["zulip::postgresql_base::postgresql_datadir"] %>' # use data in another directory - # (change requires restart) -hba_file = '<%= scope["zulip::postgresql_base::postgresql_confdir"] %>/pg_hba.conf' # host-based authentication file - # (change requires restart) -ident_file = '<%= scope["zulip::postgresql_base::postgresql_confdir"] %>/pg_ident.conf' # ident configuration file - # (change requires restart) - -# If external_pid_file is not explicitly set, no extra PID file is written. -external_pid_file = '/var/run/postgresql/<%= scope["zulip::postgresql_common::version"] %>-main.pid' # write an extra PID file - # (change requires restart) - - -#------------------------------------------------------------------------------ -# CONNECTIONS AND AUTHENTICATION -#------------------------------------------------------------------------------ - -# - Connection Settings - - -#listen_addresses = 'localhost' # what IP address(es) to listen on; - # comma-separated list of addresses; - # defaults to 'localhost'; use '*' for all - # (change requires restart) -port = 5432 # (change requires restart) -max_connections = 100 # (change requires restart) -#superuser_reserved_connections = 3 # (change requires restart) -unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories - # (change requires restart) -#unix_socket_group = '' # (change requires restart) -#unix_socket_permissions = 0777 # begin with 0 to use octal notation - # (change requires restart) -#bonjour = off # advertise server via Bonjour - # (change requires restart) -#bonjour_name = '' # defaults to the computer name - # (change requires restart) - -# - TCP settings - -# see "man 7 tcp" for details - -#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; - # 0 selects the system default -#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; - # 0 selects the system default -#tcp_keepalives_count = 0 # TCP_KEEPCNT; - # 0 selects the system default -#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; - # 0 selects the system default - -# - Authentication - - -#authentication_timeout = 1min # 1s-600s -#password_encryption = md5 # md5 or scram-sha-256 -#db_user_namespace = off - -# GSSAPI using Kerberos -#krb_server_keyfile = '' -#krb_caseins_users = off - -# - SSL - - -ssl = on -#ssl_ca_file = '' -ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' -#ssl_crl_file = '' -ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' -#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers -#ssl_prefer_server_ciphers = on -#ssl_ecdh_curve = 'prime256v1' -#ssl_min_protocol_version = 'TLSv1' -#ssl_max_protocol_version = '' -#ssl_dh_params_file = '' -#ssl_passphrase_command = '' -#ssl_passphrase_command_supports_reload = off - - -#------------------------------------------------------------------------------ -# RESOURCE USAGE (except WAL) -#------------------------------------------------------------------------------ - -# - Memory - - -shared_buffers = 128MB # min 128kB - # (change requires restart) -#huge_pages = try # on, off, or try - # (change requires restart) -#temp_buffers = 8MB # min 800kB -#max_prepared_transactions = 0 # zero disables the feature - # (change requires restart) -# Caution: it is not advisable to set max_prepared_transactions nonzero unless -# you actively intend to use prepared transactions. -#work_mem = 4MB # min 64kB -#maintenance_work_mem = 64MB # min 1MB -#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem -#max_stack_depth = 2MB # min 100kB -#shared_memory_type = mmap # the default is the first option - # supported by the operating system: - # mmap - # sysv - # windows - # (change requires restart) -dynamic_shared_memory_type = posix # the default is the first option - # supported by the operating system: - # posix - # sysv - # windows - # mmap - # (change requires restart) - -# - Disk - - -#temp_file_limit = -1 # limits per-process temp file space - # in kB, or -1 for no limit - -# - Kernel Resources - - -#max_files_per_process = 1000 # min 25 - # (change requires restart) - -# - Cost-Based Vacuum Delay - - -#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) -#vacuum_cost_page_hit = 1 # 0-10000 credits -#vacuum_cost_page_miss = 10 # 0-10000 credits -#vacuum_cost_page_dirty = 20 # 0-10000 credits -#vacuum_cost_limit = 200 # 1-10000 credits - -# - Background Writer - - -#bgwriter_delay = 200ms # 10-10000ms between rounds -#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables -#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round -#bgwriter_flush_after = 512kB # measured in pages, 0 disables - -# - Asynchronous Behavior - - -#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching -#max_worker_processes = 8 # (change requires restart) -#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers -#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers -#parallel_leader_participation = on -#max_parallel_workers = 8 # maximum number of max_worker_processes that - # can be used in parallel operations -#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate - # (change requires restart) -#backend_flush_after = 0 # measured in pages, 0 disables - - -#------------------------------------------------------------------------------ -# WRITE-AHEAD LOG -#------------------------------------------------------------------------------ - -# - Settings - - -#wal_level = replica # minimal, replica, or logical - # (change requires restart) -#fsync = on # flush data to disk for crash safety - # (turning this off can cause - # unrecoverable data corruption) -#synchronous_commit = on # synchronization level; - # off, local, remote_write, remote_apply, or on -#wal_sync_method = fsync # the default is the first option - # supported by the operating system: - # open_datasync - # fdatasync (default on Linux) - # fsync - # fsync_writethrough - # open_sync -#full_page_writes = on # recover from partial page writes -#wal_compression = off # enable compression of full-page writes -#wal_log_hints = off # also do full page writes of non-critical updates - # (change requires restart) -#wal_init_zero = on # zero-fill new WAL files -#wal_recycle = on # recycle WAL files -#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers - # (change requires restart) -#wal_writer_delay = 200ms # 1-10000 milliseconds -#wal_writer_flush_after = 1MB # measured in pages, 0 disables - -#commit_delay = 0 # range 0-100000, in microseconds -#commit_siblings = 5 # range 1-1000 - -# - Checkpoints - - -#checkpoint_timeout = 5min # range 30s-1d -max_wal_size = 1GB -min_wal_size = 80MB -#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0 -#checkpoint_flush_after = 256kB # measured in pages, 0 disables -#checkpoint_warning = 30s # 0 disables - -# - Archiving - - -#archive_mode = off # enables archiving; off, on, or always - # (change requires restart) -#archive_command = '' # command to use to archive a logfile segment - # placeholders: %p = path of file to archive - # %f = file name only - # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' -#archive_timeout = 0 # force a logfile segment switch after this - # number of seconds; 0 disables - -# - Archive Recovery - - -# These are only used in recovery mode. - -#restore_command = '' # command to use to restore an archived logfile segment - # placeholders: %p = path of file to restore - # %f = file name only - # e.g. 'cp /mnt/server/archivedir/%f %p' - # (change requires restart) -#archive_cleanup_command = '' # command to execute at every restartpoint -#recovery_end_command = '' # command to execute at completion of recovery - -# - Recovery Target - - -# Set these only when performing a targeted recovery. - -#recovery_target = '' # 'immediate' to end recovery as soon as a - # consistent state is reached - # (change requires restart) -#recovery_target_name = '' # the named restore point to which recovery will proceed - # (change requires restart) -#recovery_target_time = '' # the time stamp up to which recovery will proceed - # (change requires restart) -#recovery_target_xid = '' # the transaction ID up to which recovery will proceed - # (change requires restart) -#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed - # (change requires restart) -#recovery_target_inclusive = on # Specifies whether to stop: - # just after the specified recovery target (on) - # just before the recovery target (off) - # (change requires restart) -#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID - # (change requires restart) -#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' - # (change requires restart) - - -#------------------------------------------------------------------------------ -# REPLICATION -#------------------------------------------------------------------------------ - -# - Sending Servers - - -# Set these on the master and on any standby that will send replication data. - -#max_wal_senders = 10 # max number of walsender processes - # (change requires restart) -#wal_keep_segments = 0 # in logfile segments; 0 disables -#wal_sender_timeout = 60s # in milliseconds; 0 disables - -#max_replication_slots = 10 # max number of replication slots - # (change requires restart) -#track_commit_timestamp = off # collect timestamp of transaction commit - # (change requires restart) - -# - Master Server - - -# These settings are ignored on a standby server. - -#synchronous_standby_names = '' # standby servers that provide sync rep - # method to choose sync standbys, number of sync standbys, - # and comma-separated list of application_name - # from standby(s); '*' = all -#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed - -# - Standby Servers - - -# These settings are ignored on a master server. - -#primary_conninfo = '' # connection string to sending server - # (change requires restart) -#primary_slot_name = '' # replication slot on sending server - # (change requires restart) -#promote_trigger_file = '' # file name whose presence ends recovery -#hot_standby = on # "off" disallows queries during recovery - # (change requires restart) -#max_standby_archive_delay = 30s # max delay before canceling queries - # when reading WAL from archive; - # -1 allows indefinite delay -#max_standby_streaming_delay = 30s # max delay before canceling queries - # when reading streaming WAL; - # -1 allows indefinite delay -#wal_receiver_status_interval = 10s # send replies at least this often - # 0 disables -#hot_standby_feedback = off # send info from standby to prevent - # query conflicts -#wal_receiver_timeout = 60s # time that receiver waits for - # communication from master - # in milliseconds; 0 disables -#wal_retrieve_retry_interval = 5s # time to wait before retrying to - # retrieve WAL after a failed attempt -#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery - -# - Subscribers - - -# These settings are ignored on a publisher. - -#max_logical_replication_workers = 4 # taken from max_worker_processes - # (change requires restart) -#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers - - -#------------------------------------------------------------------------------ -# QUERY TUNING -#------------------------------------------------------------------------------ - -# - Planner Method Configuration - - -#enable_bitmapscan = on -#enable_hashagg = on -#enable_hashjoin = on -#enable_indexscan = on -#enable_indexonlyscan = on -#enable_material = on -#enable_mergejoin = on -#enable_nestloop = on -#enable_parallel_append = on -#enable_seqscan = on -#enable_sort = on -#enable_tidscan = on -#enable_partitionwise_join = off -#enable_partitionwise_aggregate = off -#enable_parallel_hash = on -#enable_partition_pruning = on - -# - Planner Cost Constants - - -#seq_page_cost = 1.0 # measured on an arbitrary scale -#random_page_cost = 4.0 # same scale as above -#cpu_tuple_cost = 0.01 # same scale as above -#cpu_index_tuple_cost = 0.005 # same scale as above -#cpu_operator_cost = 0.0025 # same scale as above -#parallel_tuple_cost = 0.1 # same scale as above -#parallel_setup_cost = 1000.0 # same scale as above - -#jit_above_cost = 100000 # perform JIT compilation if available - # and query more expensive than this; - # -1 disables -#jit_inline_above_cost = 500000 # inline small functions if query is - # more expensive than this; -1 disables -#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if - # query is more expensive than this; - # -1 disables - -#min_parallel_table_scan_size = 8MB -#min_parallel_index_scan_size = 512kB -#effective_cache_size = 4GB - -# - Genetic Query Optimizer - - -#geqo = on -#geqo_threshold = 12 -#geqo_effort = 5 # range 1-10 -#geqo_pool_size = 0 # selects default based on effort -#geqo_generations = 0 # selects default based on effort -#geqo_selection_bias = 2.0 # range 1.5-2.0 -#geqo_seed = 0.0 # range 0.0-1.0 - -# - Other Planner Options - - -#default_statistics_target = 100 # range 1-10000 -#constraint_exclusion = partition # on, off, or partition -#cursor_tuple_fraction = 0.1 # range 0.0-1.0 -#from_collapse_limit = 8 -#join_collapse_limit = 8 # 1 disables collapsing of explicit - # JOIN clauses -#force_parallel_mode = off -#jit = on # allow JIT compilation -#plan_cache_mode = auto # auto, force_generic_plan or - # force_custom_plan - - -#------------------------------------------------------------------------------ -# REPORTING AND LOGGING -#------------------------------------------------------------------------------ - -# - Where to Log - - -#log_destination = 'stderr' # Valid values are combinations of - # stderr, csvlog, syslog, and eventlog, - # depending on platform. csvlog - # requires logging_collector to be on. - -# This is used when logging to stderr: -#logging_collector = off # Enable capturing of stderr and csvlog - # into log files. Required to be on for - # csvlogs. - # (change requires restart) - -# These are only used if logging_collector is on: -#log_directory = 'log' # directory where log files are written, - # can be absolute or relative to PGDATA -#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, - # can include strftime() escapes -#log_file_mode = 0600 # creation mode for log files, - # begin with 0 to use octal notation -#log_truncate_on_rotation = off # If on, an existing log file with the - # same name as the new log file will be - # truncated rather than appended to. - # But such truncation only occurs on - # time-driven rotation, not on restarts - # or size-driven rotation. Default is - # off, meaning append to existing files - # in all cases. -#log_rotation_age = 1d # Automatic rotation of logfiles will - # happen after that time. 0 disables. -#log_rotation_size = 10MB # Automatic rotation of logfiles will - # happen after that much log output. - # 0 disables. - -# These are relevant when logging to syslog: -#syslog_facility = 'LOCAL0' -#syslog_ident = 'postgres' -#syslog_sequence_numbers = on -#syslog_split_messages = on - -# This is only relevant when logging to eventlog (win32): -# (change requires restart) -#event_source = 'PostgreSQL' - -# - When to Log - - -#log_min_messages = warning # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic - -#log_min_error_statement = error # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic (effectively off) - -#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements - # and their durations, > 0 logs only - # statements running at least this number - # of milliseconds - -#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements - # are logged regardless of their duration. 1.0 logs all - # statements from all transactions, 0.0 never logs. - -# - What to Log - - -#debug_print_parse = off -#debug_print_rewritten = off -#debug_print_plan = off -#debug_pretty_print = on -#log_checkpoints = off -#log_connections = off -#log_disconnections = off -#log_duration = off -#log_error_verbosity = default # terse, default, or verbose messages -#log_hostname = off -log_line_prefix = '%m [%p] %q%u@%d ' # special values: - # %a = application name - # %u = user name - # %d = database name - # %r = remote host and port - # %h = remote host - # %p = process ID - # %t = timestamp without milliseconds - # %m = timestamp with milliseconds - # %n = timestamp with milliseconds (as a Unix epoch) - # %i = command tag - # %e = SQL state - # %c = session ID - # %l = session line number - # %s = session start timestamp - # %v = virtual transaction ID - # %x = transaction ID (0 if none) - # %q = stop here in non-session - # processes - # %% = '%' - # e.g. '<%%u%%%d> ' -#log_lock_waits = off # log lock waits >= deadlock_timeout -#log_statement = 'none' # none, ddl, mod, all -#log_replication_commands = off -#log_temp_files = -1 # log temporary files equal or larger - # than the specified size in kilobytes; - # -1 disables, 0 logs all temp files -log_timezone = 'UTC' - -#------------------------------------------------------------------------------ -# PROCESS TITLE -#------------------------------------------------------------------------------ - -cluster_name = '<%= scope["zulip::postgresql_common::version"] %>/main' # added to process titles if nonempty - # (change requires restart) -#update_process_title = on - - -#------------------------------------------------------------------------------ -# STATISTICS -#------------------------------------------------------------------------------ - -# - Query and Index Statistics Collector - - -#track_activities = on -#track_counts = on -#track_io_timing = off -#track_functions = none # none, pl, all -#track_activity_query_size = 1024 # (change requires restart) -stats_temp_directory = '/var/run/postgresql/<%= scope["zulip::postgresql_common::version"] %>-main.pg_stat_tmp' - - -# - Monitoring - - -#log_parser_stats = off -#log_planner_stats = off -#log_executor_stats = off -#log_statement_stats = off - - -#------------------------------------------------------------------------------ -# AUTOVACUUM -#------------------------------------------------------------------------------ - -#autovacuum = on # Enable autovacuum subprocess? 'on' - # requires track_counts to also be on. -#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and - # their durations, > 0 logs only - # actions running at least this number - # of milliseconds. -#autovacuum_max_workers = 3 # max number of autovacuum subprocesses - # (change requires restart) -#autovacuum_naptime = 1min # time between autovacuum runs -#autovacuum_vacuum_threshold = 50 # min number of row updates before - # vacuum -#autovacuum_analyze_threshold = 50 # min number of row updates before - # analyze -#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum -#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze -#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum - # (change requires restart) -#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age - # before forced vacuum - # (change requires restart) -#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for - # autovacuum, in milliseconds; - # -1 means use vacuum_cost_delay -#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for - # autovacuum, -1 means use - # vacuum_cost_limit - - -#------------------------------------------------------------------------------ -# CLIENT CONNECTION DEFAULTS -#------------------------------------------------------------------------------ - -# - Statement Behavior - - -#client_min_messages = notice # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # log - # notice - # warning - # error -#search_path = '"$user", public' # schema names -#row_security = on -#default_tablespace = '' # a tablespace name, '' uses the default -#temp_tablespaces = '' # a list of tablespace names, '' uses - # only default tablespace -#default_table_access_method = 'heap' -#check_function_bodies = on -#default_transaction_isolation = 'read committed' -#default_transaction_read_only = off -#default_transaction_deferrable = off -#session_replication_role = 'origin' -#statement_timeout = 0 # in milliseconds, 0 is disabled -#lock_timeout = 0 # in milliseconds, 0 is disabled -#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled -#vacuum_freeze_min_age = 50000000 -#vacuum_freeze_table_age = 150000000 -#vacuum_multixact_freeze_min_age = 5000000 -#vacuum_multixact_freeze_table_age = 150000000 -#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples - # before index cleanup, 0 always performs - # index cleanup -#bytea_output = 'hex' # hex, escape -#xmlbinary = 'base64' -#xmloption = 'content' -#gin_fuzzy_search_limit = 0 -#gin_pending_list_limit = 4MB - -# - Locale and Formatting - - -datestyle = 'iso, mdy' -#intervalstyle = 'postgres' -timezone = 'UTC' -#timezone_abbreviations = 'Default' # Select the set of available time zone - # abbreviations. Currently, there are - # Default - # Australia (historical usage) - # India - # You can create your own file in - # share/timezonesets/. -#extra_float_digits = 1 # min -15, max 3; any value >0 actually - # selects precise output mode -#client_encoding = sql_ascii # actually, defaults to database - # encoding - -# These settings are initialized by initdb, but they can be changed. -lc_messages = 'C.UTF-8' # locale for system error message - # strings -lc_monetary = 'C.UTF-8' # locale for monetary formatting -lc_numeric = 'C.UTF-8' # locale for number formatting -lc_time = 'C.UTF-8' # locale for time formatting - -# default configuration for text search -default_text_search_config = 'pg_catalog.english' - -# - Shared Library Preloading - - -#shared_preload_libraries = '' # (change requires restart) -#local_preload_libraries = '' -#session_preload_libraries = '' -#jit_provider = 'llvmjit' # JIT library to use - -# - Other Defaults - - -#dynamic_library_path = '$libdir' - - -#------------------------------------------------------------------------------ -# LOCK MANAGEMENT -#------------------------------------------------------------------------------ - -#deadlock_timeout = 1s -#max_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_relation = -2 # negative values mean - # (max_pred_locks_per_transaction - # / -max_pred_locks_per_relation) - 1 -#max_pred_locks_per_page = 2 # min 0 - - -#------------------------------------------------------------------------------ -# VERSION AND PLATFORM COMPATIBILITY -#------------------------------------------------------------------------------ - -# - Previous PostgreSQL Versions - - -#array_nulls = on -#backslash_quote = safe_encoding # on, off, or safe_encoding -#escape_string_warning = on -#lo_compat_privileges = off -#operator_precedence_warning = off -#quote_all_identifiers = off -#standard_conforming_strings = on -#synchronize_seqscans = on - -# - Other Platforms and Clients - - -#transform_null_equals = off - - -#------------------------------------------------------------------------------ -# ERROR HANDLING -#------------------------------------------------------------------------------ - -#exit_on_error = off # terminate session on any error? -#restart_after_crash = on # reinitialize after backend crash? -#data_sync_retry = off # retry or panic on failure to fsync - # data? - # (change requires restart) - - -#------------------------------------------------------------------------------ -# CONFIG FILE INCLUDES -#------------------------------------------------------------------------------ - -# These options allow settings to be loaded from files other than the -# default postgresql.conf. Note that these are directives, not variable -# assignments, so they can usefully be given more than once. - -include_dir = 'conf.d' # include files ending in '.conf' from - # a directory, e.g., 'conf.d' -#include_if_exists = '...' # include file only if it exists -#include = '...' # include file - - -#------------------------------------------------------------------------------ -# CUSTOMIZED OPTIONS -#------------------------------------------------------------------------------ - -# Add settings for extensions here -timezone = 'UTC' -default_text_search_config = 'zulip.english_us_search' - -log_destination = 'stderr' -logging_collector = on -log_directory = '/var/log/postgresql' -log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' -log_rotation_age = 7d -log_rotation_size = 100MB -log_min_duration_statement = 500 -log_line_prefix = '%m [%c]: [%l-1] ' -log_checkpoints = on -log_lock_waits = on -log_temp_files = 0 -log_autovacuum_min_duration = 100 - -autovacuum_freeze_max_age = 2000000000 -vacuum_freeze_min_age = 1000000000 -vacuum_freeze_table_age = 1800000000 - -# Performance settings -max_connections = 1000 -maintenance_work_mem = <%= scope["zulip::profile::postgresql::maintenance_work_mem"] %>MB -effective_cache_size = <%= scope["zulip::profile::postgresql::effective_cache_size"] %>MB -work_mem = <%= scope["zulip::profile::postgresql::work_mem"] %>MB -shared_buffers = <%= scope["zulip::profile::postgresql::shared_buffers"] %>MB -wal_buffers = 4MB -checkpoint_completion_target = 0.7 -<% unless @random_page_cost.nil? -%> -random_page_cost = <%= @random_page_cost %> -<% end -%> -<% unless @effective_io_concurrency.nil? -%> -effective_io_concurrency = <%= @effective_io_concurrency %> -<% end -%> - -<% unless @listen_addresses.nil? -%> -listen_addresses = <%= @listen_addresses %> -<% end -%> - -<% if @s3_backups_bucket != '' -%> -# WAL backups to S3 (may also be used for replication) -archive_mode = on -archive_command = '/usr/bin/timeout 10m /usr/local/bin/env-wal-g wal-push %p' -restore_command = '/usr/local/bin/env-wal-g wal-fetch "%f" "%p"' -<% end -%> -<% unless @replication_primary.nil? || @replication_user.nil? -%> -# Streaming replication -primary_conninfo = 'host=<%= @replication_primary %> user=<%= @replication_user -%> -<% if @replication_password != '' %> password=<%= @replication_password %><% end -%> -<% unless @ssl_mode.nil? %> sslmode=<%= @ssl_mode %><% end -%> -' -<% end -%> - -<% unless @ssl_cert_file.nil? -%> -ssl_cert_file = '<%= @ssl_cert_file %>' # (change requires restart) -<% end -%> -<% unless @ssl_key_file.nil? -%> -ssl_key_file = '<%= @ssl_key_file %>' # (change requires restart) -<% end -%> -<% unless @ssl_ca_file.nil? -%> -ssl_ca_file = '<%= @ssl_ca_file %>' # (change requires restart) -<% end -%> diff --git a/scripts/lib/check-database-compatibility b/scripts/lib/check-database-compatibility index 8ed3a97d12..9e9332c429 100755 --- a/scripts/lib/check-database-compatibility +++ b/scripts/lib/check-database-compatibility @@ -62,10 +62,10 @@ if os.path.exists("/etc/init.d/postgresql") and os.path.exists("/etc/zulip/zulip ) sys.exit(1) -if django_pg_version < 12: +if django_pg_version < 13: logging.critical("Unsupported PostgreSQL version: %d", postgresql_version) logging.info( - "Please upgrade to PostgreSQL 12 or newer first.\n" + "Please upgrade to PostgreSQL 13 or newer first.\n" "See https://zulip.readthedocs.io/en/stable/production/" "upgrade.html#upgrading-postgresql" ) diff --git a/scripts/lib/install b/scripts/lib/install index 7066077c3e..a68d5aff9b 100755 --- a/scripts/lib/install +++ b/scripts/lib/install @@ -199,8 +199,8 @@ if [ "$EXTERNAL_HOST" = zulip.example.com ] \ fi case "$POSTGRESQL_VERSION" in - [0-9] | [0-9].* | 1[01] | 1[01].*) - echo "error: PostgreSQL 12 or newer is required." >&2 + [0-9] | [0-9].* | 1[0-2] | 1[0-2].*) + echo "error: PostgreSQL 13 or newer is required." >&2 exit 1 ;; esac From 990c0616d3677c8ebf078b1329340b0adee763d5 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Fri, 1 Nov 2024 15:42:00 -0700 Subject: [PATCH 181/276] docs: Document 10.x postgres support. --- docs/production/postgresql-support-table.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/production/postgresql-support-table.md b/docs/production/postgresql-support-table.md index 9e34c4be16..8623e99e55 100644 --- a/docs/production/postgresql-support-table.md +++ b/docs/production/postgresql-support-table.md @@ -7,3 +7,4 @@ | 7.x | 12, 13, 14, 15 | | 8.x | 12, 13, 14, 15, 16 | | 9.x | 12, 13, 14, 15, 16 | +| 10.x (unreleased) | 13, 14, 15, 16 | From ea80791b96ce9c9de9306cc51c3e07532920f38c Mon Sep 17 00:00:00 2001 From: afeefuddin Date: Sat, 2 Nov 2024 02:15:57 +0530 Subject: [PATCH 182/276] demo_organizations_ui: Convert module to TypeScript. --- tools/test-js-with-node | 2 +- ...zations_ui.js => demo_organizations_ui.ts} | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) rename web/src/{demo_organizations_ui.js => demo_organizations_ui.ts} (83%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 7184560081..148ae37f49 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -94,7 +94,7 @@ EXEMPT_FILES = make_set( "web/src/css_variables.js", "web/src/custom_profile_fields_ui.ts", "web/src/debug.ts", - "web/src/demo_organizations_ui.js", + "web/src/demo_organizations_ui.ts", "web/src/deprecated_feature_notice.ts", "web/src/desktop_integration.js", "web/src/desktop_notifications.ts", diff --git a/web/src/demo_organizations_ui.js b/web/src/demo_organizations_ui.ts similarity index 83% rename from web/src/demo_organizations_ui.js rename to web/src/demo_organizations_ui.ts index 5bf20ad7eb..5412dcf0c2 100644 --- a/web/src/demo_organizations_ui.js +++ b/web/src/demo_organizations_ui.ts @@ -1,4 +1,5 @@ import $ from "jquery"; +import {z} from "zod"; import render_convert_demo_organization_form from "../templates/settings/convert_demo_organization_form.hbs"; import render_demo_organization_warning from "../templates/settings/demo_organization_warning.hbs"; @@ -11,9 +12,11 @@ import {get_demo_organization_deadline_days_remaining} from "./navbar_alerts"; import * as settings_config from "./settings_config"; import * as settings_data from "./settings_data"; import * as settings_org from "./settings_org"; +import type {RequestOpts} from "./settings_ui"; import {current_user, realm} from "./state_data"; +import type {HTMLSelectOneElement} from "./types"; -export function insert_demo_organization_warning() { +export function insert_demo_organization_warning(): void { const days_remaining = get_demo_organization_deadline_days_remaining(); const rendered_demo_organization_warning = render_demo_organization_warning({ is_demo_organization: realm.demo_organization_scheduled_deletion_date, @@ -23,7 +26,7 @@ export function insert_demo_organization_warning() { $(".organization-box").find(".settings-section").prepend($(rendered_demo_organization_warning)); } -export function handle_demo_organization_conversion() { +export function handle_demo_organization_conversion(): void { $(".convert-demo-organization-button").on("click", () => { if (!current_user.is_owner) { return; @@ -39,7 +42,7 @@ export function handle_demo_organization_conversion() { realm_org_type_values: settings_org.get_org_type_dropdown_options(), }); - function demo_organization_conversion_post_render() { + function demo_organization_conversion_post_render(): void { const $convert_submit_button = $( "#demo-organization-conversion-modal .dialog_submit_button", ); @@ -53,8 +56,10 @@ export function handle_demo_organization_conversion() { } else { // Disable submit button if either form field blank. $("#convert-demo-organization-form").on("input change", () => { - const string_id = $("#new_subdomain").val().trim(); - const org_type = $("#add_organization_type").val(); + const string_id = $("input#new_subdomain").val()!.trim(); + const org_type = $( + "select:not([multiple])#add_organization_type", + ).val()!; $convert_submit_button.prop( "disabled", string_id === "" || @@ -65,15 +70,16 @@ export function handle_demo_organization_conversion() { } } - function submit_subdomain() { + function submit_subdomain(): void { const $string_id = $("#new_subdomain"); const $organization_type = $("#add_organization_type"); const data = { string_id: $string_id.val(), org_type: $organization_type.val(), }; - const opts = { - success_continuation(data) { + const opts: RequestOpts = { + success_continuation(raw_data) { + const data = z.object({realm_url: z.string()}).parse(raw_data); window.location.href = data.realm_url; }, }; From 9371bdb81d2fa71f9428da1448feaee905a1ff5e Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Fri, 1 Nov 2024 16:23:05 +0530 Subject: [PATCH 183/276] invites: Add `durable=True` to transaction in `do_invite_users`. This commit adds 'durable=True' to the outermost transaction in 'do_invite_users'. It also adds 'savepoint=False' to inner transaction.atomic decorators to avoid creating savepoints. --- zerver/actions/invites.py | 4 ++-- zerver/lib/send_email.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zerver/actions/invites.py b/zerver/actions/invites.py index 2197ac0da3..11534c7127 100644 --- a/zerver/actions/invites.py +++ b/zerver/actions/invites.py @@ -173,7 +173,7 @@ def check_invite_limit(realm: Realm, num_invitees: int) -> None: ) -@transaction.atomic +@transaction.atomic(durable=True) def do_invite_users( user_profile: UserProfile, invitee_emails: Collection[str], @@ -414,7 +414,7 @@ def do_revoke_multi_use_invite(multiuse_invite: MultiuseInvite) -> None: notify_invites_changed(realm, changed_invite_referrer=multiuse_invite.referred_by) -@transaction.atomic +@transaction.atomic(savepoint=False) def do_send_user_invite_email( prereg_user: PreregistrationUser, *, diff --git a/zerver/lib/send_email.py b/zerver/lib/send_email.py index 8803672843..348e192565 100644 --- a/zerver/lib/send_email.py +++ b/zerver/lib/send_email.py @@ -378,7 +378,7 @@ def send_future_email( # For logging the email assert (to_user_ids is None) ^ (to_emails is None) - with transaction.atomic(): + with transaction.atomic(savepoint=False): email = ScheduledEmail.objects.create( type=EMAIL_TYPES[template_name], scheduled_timestamp=timezone_now() + delay, From 0fb5657131df30c21a62e3583e60d295191f18d8 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Fri, 1 Nov 2024 16:42:53 +0530 Subject: [PATCH 184/276] transaction: Add durable=True to outermost transaction.atomic decorator. This commit adds 'durable=True' to the outermost transactions of the following functions: * do_create_multiuse_invite_link * do_revoke_user_invite * do_revoke_multi_use_invite * sync_ldap_user_data * do_reactivate_remote_server * do_deactivate_remote_server * bulk_handle_digest_email * handle_customer_migration_from_server_to_realm * add_reaction * remove_reaction * deactivate_user_group It helps to avoid creating unintended savepoints in the future. This is as a part of our plan to explicitly mark all the transaction.atomic decorators with either 'savepoint=False' or 'durable=True' as required. * 'savepoint=True' is used in special cases. --- corporate/lib/stripe.py | 4 ++-- zerver/actions/invites.py | 6 +++--- zerver/lib/digest.py | 2 +- zerver/management/commands/sync_ldap_user_data.py | 2 +- zerver/views/reactions.py | 4 ++-- zerver/views/user_groups.py | 2 +- zilencer/views.py | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index a6d585a406..381e06cb73 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -5231,7 +5231,7 @@ def ensure_customer_does_not_have_active_plan(customer: Customer) -> None: raise UpgradeWithExistingPlanError -@transaction.atomic +@transaction.atomic(durable=True) def do_reactivate_remote_server(remote_server: RemoteZulipServer) -> None: """ Utility function for reactivating deactivated registrations. @@ -5253,7 +5253,7 @@ def do_reactivate_remote_server(remote_server: RemoteZulipServer) -> None: ) -@transaction.atomic +@transaction.atomic(durable=True) def do_deactivate_remote_server( remote_server: RemoteZulipServer, billing_session: RemoteServerBillingSession ) -> None: diff --git a/zerver/actions/invites.py b/zerver/actions/invites.py index 11534c7127..e2c783dcae 100644 --- a/zerver/actions/invites.py +++ b/zerver/actions/invites.py @@ -362,7 +362,7 @@ def do_get_invites_controlled_by_user(user_profile: UserProfile) -> list[dict[st return invites -@transaction.atomic +@transaction.atomic(durable=True) def do_create_multiuse_invite_link( referred_by: UserProfile, invited_as: int, @@ -386,7 +386,7 @@ def do_create_multiuse_invite_link( ) -@transaction.atomic +@transaction.atomic(durable=True) def do_revoke_user_invite(prereg_user: PreregistrationUser) -> None: email = prereg_user.email realm = prereg_user.realm @@ -403,7 +403,7 @@ def do_revoke_user_invite(prereg_user: PreregistrationUser) -> None: notify_invites_changed(realm, changed_invite_referrer=prereg_user.referred_by) -@transaction.atomic +@transaction.atomic(durable=True) def do_revoke_multi_use_invite(multiuse_invite: MultiuseInvite) -> None: realm = multiuse_invite.referred_by.realm diff --git a/zerver/lib/digest.py b/zerver/lib/digest.py index f233017cf0..69f5cd7fd9 100644 --- a/zerver/lib/digest.py +++ b/zerver/lib/digest.py @@ -387,7 +387,7 @@ def get_digest_context(user: UserProfile, cutoff: float) -> dict[str, Any]: raise AssertionError("Unreachable") -@transaction.atomic +@transaction.atomic(durable=True) def bulk_handle_digest_email(user_ids: list[int], cutoff: float) -> None: # We go directly to the database to get user objects, # since inactive users are likely to not be in the cache. diff --git a/zerver/management/commands/sync_ldap_user_data.py b/zerver/management/commands/sync_ldap_user_data.py index 5b885e364d..0f4f14f57b 100644 --- a/zerver/management/commands/sync_ldap_user_data.py +++ b/zerver/management/commands/sync_ldap_user_data.py @@ -19,7 +19,7 @@ log_to_file(logger, settings.LDAP_SYNC_LOG_PATH) # Run this on a cron job to pick up on name changes. -@transaction.atomic +@transaction.atomic(durable=True) def sync_ldap_user_data( user_profiles: QuerySet[UserProfile], deactivation_protection: bool = True ) -> None: diff --git a/zerver/views/reactions.py b/zerver/views/reactions.py index c1b9c92597..55ad9c91bb 100644 --- a/zerver/views/reactions.py +++ b/zerver/views/reactions.py @@ -12,7 +12,7 @@ from zerver.models import Reaction, UserProfile # transaction.atomic is required since we use FOR UPDATE queries in access_message -@transaction.atomic +@transaction.atomic(durable=True) @typed_endpoint def add_reaction( request: HttpRequest, @@ -29,7 +29,7 @@ def add_reaction( # transaction.atomic is required since we use FOR UPDATE queries in access_message -@transaction.atomic +@transaction.atomic(durable=True) @typed_endpoint def remove_reaction( request: HttpRequest, diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index ae2473ec42..2f44c2be18 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -207,7 +207,7 @@ def edit_user_group( @typed_endpoint -@transaction.atomic +@transaction.atomic(durable=True) def deactivate_user_group( request: HttpRequest, user_profile: UserProfile, diff --git a/zilencer/views.py b/zilencer/views.py index 8bc6c96261..fb3ec2aea7 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -1035,7 +1035,7 @@ def get_human_user_realm_uuids( return billable_realm_uuids -@transaction.atomic +@transaction.atomic(durable=True) def handle_customer_migration_from_server_to_realm( server: RemoteZulipServer, ) -> None: From 3d597bb9b0339b0fd0771d397a89b233474009ba Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Fri, 1 Nov 2024 17:13:55 +0530 Subject: [PATCH 185/276] delete_message_backend: Add `durable=True` to the outermost transaction. This commit adds 'durable=True' to the outermost transaction in 'delete_message_backend'. It also adds 'savepoint=False' to inner transaction.atomic decorator to avoid creating savepoint. This is as a part of our plan to explicitly mark all the transaction.atomic decorators with either 'savepoint=False' or 'durable=True' as required. * 'savepoint=True' is used in special cases. --- zerver/lib/retention.py | 3 ++- zerver/views/message_edit.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/zerver/lib/retention.py b/zerver/lib/retention.py index f18554811a..694f8318a8 100644 --- a/zerver/lib/retention.py +++ b/zerver/lib/retention.py @@ -141,7 +141,7 @@ def run_archiving( message_count = 0 while True: start_time = time.time() - with transaction.atomic(): + with transaction.atomic(savepoint=False): archive_transaction = ArchiveTransaction.objects.create(type=type, realm=realm) new_chunk = move_rows( Message, @@ -396,6 +396,7 @@ def archive_stream_messages( logger.info("Done. Archived %s messages.", message_count) +@transaction.atomic(durable=True) def archive_messages(chunk_size: int = MESSAGE_BATCH_SIZE) -> None: logger.info("Starting the archiving process with chunk_size %s", chunk_size) diff --git a/zerver/views/message_edit.py b/zerver/views/message_edit.py index 838d40ff2b..18a43615f4 100644 --- a/zerver/views/message_edit.py +++ b/zerver/views/message_edit.py @@ -167,7 +167,7 @@ def validate_can_delete_message(user_profile: UserProfile, message: Message) -> return -@transaction.atomic +@transaction.atomic(durable=True) @typed_endpoint def delete_message_backend( request: HttpRequest, From c4f74f470d14a4690e2e32b30654c56d84776512 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Fri, 1 Nov 2024 19:53:30 +0530 Subject: [PATCH 186/276] remote_server_post_analytics: Add durable=True to outermost transaction. This commit adds 'durable=True' to the outermost transaction in 'remote_server_post_analytics'. It also adds 'savepoint=False' to inner transaction.atomic decorator to avoid creating savepoint. This is as a part of our plan to explicitly mark all the transaction.atomic decorators with either 'savepoint=False' or 'durable=True' as required. * 'savepoint=True' is used in special cases. --- corporate/lib/stripe.py | 2 +- zerver/tests/test_push_notifications.py | 6 ++---- zilencer/views.py | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 381e06cb73..00ca44c0f9 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -2197,7 +2197,7 @@ class BillingSession(ABC): # event_time should roughly be timezone_now(). Not designed to handle # event_times in the past or future - @transaction.atomic + @transaction.atomic(savepoint=False) def make_end_of_cycle_updates_if_needed( self, plan: CustomerPlan, event_time: datetime ) -> tuple[CustomerPlan | None, LicenseLedger | None]: diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index ccbf4be4c4..141be5d3af 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -14,7 +14,6 @@ import orjson import responses import time_machine from django.conf import settings -from django.db import transaction from django.db.models import F, Q from django.http.response import ResponseHeaders from django.test import override_settings @@ -2145,9 +2144,7 @@ class AnalyticsBouncerTest(BouncerTestCase): plan_type=RemoteRealm.PLAN_TYPE_SELF_MANAGED, ) - with transaction.atomic(), self.assertLogs("zulip.analytics", level="WARNING") as m: - # The usual atomic() wrapper to avoid IntegrityError breaking the test's - # transaction. + with self.assertLogs("zulip.analytics", level="WARNING") as m: send_server_data_to_push_bouncer() self.assertEqual(m.output, ["WARNING:zulip.analytics:Duplicate registration detected."]) @@ -2705,6 +2702,7 @@ class AnalyticsBouncerTest(BouncerTestCase): # Now we want to test the other side of this - bouncer's handling # of a deleted realm. with ( + self.captureOnCommitCallbacks(execute=True), self.assertLogs(logger, level="WARNING") as analytics_logger, mock.patch( "corporate.lib.stripe.RemoteRealmBillingSession.on_paid_plan", return_value=True diff --git a/zilencer/views.py b/zilencer/views.py index fb3ec2aea7..430f4bbb29 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -50,7 +50,7 @@ from zerver.lib.push_notifications import ( send_apple_push_notification, send_test_push_notification_directly_to_devices, ) -from zerver.lib.queue import queue_json_publish +from zerver.lib.queue import queue_event_on_commit from zerver.lib.remote_server import ( InstallationCountDataForAnalytics, RealmAuditLogDataForAnalytics, @@ -1012,7 +1012,7 @@ def update_remote_realm_data_for_server( } for context in new_locally_deleted_remote_realms_on_paid_plan_contexts: email_dict["context"] = context - queue_json_publish("email_senders", email_dict) + queue_event_on_commit("email_senders", email_dict) def get_human_user_realm_uuids( @@ -1156,7 +1156,7 @@ def handle_customer_migration_from_server_to_realm( @typed_endpoint -@transaction.atomic +@transaction.atomic(durable=True) def remote_server_post_analytics( request: HttpRequest, server: RemoteZulipServer, @@ -1267,7 +1267,7 @@ def remote_server_post_analytics( # 'last_audit_log_update' needs to be an atomic operation. # This helps to rely on 'last_audit_log_update' to assume # RemoteRealmAuditLog and LicenseLedger are up-to-date. - with transaction.atomic(): + with transaction.atomic(savepoint=False): # Important: Do not return early if we receive 0 rows; we must # updated last_audit_log_update even if there are no new rows, # to help identify server whose ability to connect to this From f351f9482779565f6c7d0c920df206ab60589a71 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Fri, 1 Nov 2024 21:46:32 +0530 Subject: [PATCH 187/276] do_change_plan_type: Mark the transaction to not create savepoints. This commit adds 'savepoint=False' to the transaction.atomic decorators of do_change_plan_type as we don't intend to create savepoints when the function is called inside an outer transaction. --- corporate/lib/stripe.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 00ca44c0f9..c7d91151db 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -4006,6 +4006,7 @@ class RealmBillingSession(BillingSession): return customer @override + @transaction.atomic(savepoint=False) def do_change_plan_type( self, *, tier: int | None, is_sponsored: bool = False, background_update: bool = False ) -> None: @@ -4404,7 +4405,7 @@ class RemoteRealmBillingSession(BillingSession): return customer @override - @transaction.atomic + @transaction.atomic(savepoint=False) def do_change_plan_type( self, *, tier: int | None, is_sponsored: bool = False, background_update: bool = False ) -> None: # nocoverage @@ -4841,7 +4842,7 @@ class RemoteServerBillingSession(BillingSession): return customer @override - @transaction.atomic + @transaction.atomic(savepoint=False) def do_change_plan_type( self, *, tier: int | None, is_sponsored: bool = False, background_update: bool = False ) -> None: From a34577f82e7a3f335944c9a641778026d10566b1 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Fri, 1 Nov 2024 21:58:39 +0530 Subject: [PATCH 188/276] user_groups: Add savepoint=False to avoid creating savepoints. add_subgroups_to_user_group and remove_subgroups_from_user_group are already inside outer db transactions. This commit explicitly adds 'savepoint=False' to avoid creating savepoints. --- zerver/actions/user_groups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zerver/actions/user_groups.py b/zerver/actions/user_groups.py index e6fc48dc66..42d66ee0c6 100644 --- a/zerver/actions/user_groups.py +++ b/zerver/actions/user_groups.py @@ -366,7 +366,7 @@ def do_send_subgroups_update_event( send_event_on_commit(user_group.realm, event, active_user_ids(user_group.realm_id)) -@transaction.atomic +@transaction.atomic(savepoint=False) def add_subgroups_to_user_group( user_group: NamedUserGroup, subgroups: list[NamedUserGroup], @@ -406,7 +406,7 @@ def add_subgroups_to_user_group( do_send_subgroups_update_event("add_subgroups", user_group, subgroup_ids) -@transaction.atomic +@transaction.atomic(savepoint=False) def remove_subgroups_from_user_group( user_group: NamedUserGroup, subgroups: list[NamedUserGroup], From b149d5fb7053d63d22fe3a1d5299071a330f903f Mon Sep 17 00:00:00 2001 From: opmkumar Date: Sat, 2 Nov 2024 01:57:48 +0530 Subject: [PATCH 189/276] refactor: Update on-change event to use custom-expiration-time-input. Replaces custom-invite-expiration-time with custom-expiration-time-input and custom-expiration-time-unit in the on-change event. --- web/src/invite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/invite.ts b/web/src/invite.ts index 1b13a87d91..85dd98dda3 100644 --- a/web/src/invite.ts +++ b/web/src/invite.ts @@ -439,7 +439,7 @@ function open_invite_user_modal(e: JQuery.ClickEvent): void } }); - $("#custom-invite-expiration-time").on("change", () => { + $("#custom-expiration-time-input, #custom-expiration-time-unit").on("change", () => { custom_expiration_time_input = util.check_time_input( $("input#custom-expiration-time-input").val()!, ); From ec43a66f26487b981d6a6fde8f2b933017dd330c Mon Sep 17 00:00:00 2001 From: whilstsomebody Date: Sat, 26 Oct 2024 10:40:29 +0530 Subject: [PATCH 190/276] update_ui: Standardize pencil icon color for todo and poll. Ensure consistent pencil icon color for both todo and poll features in hover and non-hover states across light and dark themes. Fixes #30339. --- web/styles/widgets.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/styles/widgets.css b/web/styles/widgets.css index 638984a15c..00e1d2298d 100644 --- a/web/styles/widgets.css +++ b/web/styles/widgets.css @@ -272,10 +272,11 @@ input { .poll-edit-question, .todo-edit-task-list-title { - opacity: 0.4; + color: var(--color-message-action-visible); - &:hover { - opacity: 1; + &:hover, + &:focus-visible { + color: var(--color-message-action-interactive); } } From 54b8a9233b0d150073ec02ba227de5948281e749 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Fri, 1 Nov 2024 16:16:22 -0700 Subject: [PATCH 191/276] groups: Clarify meaning of global admin setting. The previous wording was ambiguous as to whether this permission means the ability to administer all groups in the organization. --- help/manage-user-groups.md | 19 ++++++++++--------- .../organization_permissions_admin.hbs | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/help/manage-user-groups.md b/help/manage-user-groups.md index 58247afc08..71ff469703 100644 --- a/help/manage-user-groups.md +++ b/help/manage-user-groups.md @@ -36,7 +36,7 @@ !!! warn "" - Guests can never manage user groups, add anyone else to a group, or remove + Guests can never administer user groups, add anyone else to a group, or remove anyone else from a group, even if they belong to a group that has permissions to do so. @@ -54,7 +54,7 @@ 1. Select the **General** tab on the right. -1. Under **Group permissions**, configure **Who can manage this group**, **Who +1. Under **Group permissions**, configure **Who can administer this group**, **Who can mention this group**, **Who can add members to this group**, **Who can join this group**, and **Who can leave this group**. @@ -141,16 +141,17 @@ so. {end_tabs} -## Configure who can manage user groups +## Configure who can administer all user groups {!admin-only.md!} -You can configure who can manage groups in your organization. Guests can never -manage user groups, even if they belong to a group that has permissions to do -so. +You can configure who can administer all user groups in your +organization. Guests can never administer user groups, even if they +belong to a group that has permissions to do so. -In addition, you can [give users permission](#configure-group-permissions) to -manage a specific group. +In addition, you can [give users +permission](#configure-group-permissions) to administer a specific +group. {start_tabs} @@ -158,7 +159,7 @@ manage a specific group. {settings_tab|organization-permissions} -1. Under **Other permissions**, configure **Who can manage user groups**. +1. Under **Other permissions**, configure **Who can administer all user groups**. {!save-changes.md!} diff --git a/web/templates/settings/organization_permissions_admin.hbs b/web/templates/settings/organization_permissions_admin.hbs index aff1f71ff2..6da7fc12f1 100644 --- a/web/templates/settings/organization_permissions_admin.hbs +++ b/web/templates/settings/organization_permissions_admin.hbs @@ -332,7 +332,7 @@ {{> group_setting_value_pill_input setting_name="realm_can_manage_all_groups" - label=(t 'Who can manage user groups')}} + label=(t 'Who can administer all user groups')}} {{> group_setting_value_pill_input setting_name="realm_can_add_custom_emoji_group" From d556c0e0a5e9e8ba0ff548354bf2ba6ef2c97d4b Mon Sep 17 00:00:00 2001 From: evykassirer Date: Fri, 1 Nov 2024 16:37:37 -0700 Subject: [PATCH 192/276] buddy_list: Move user link a bit left to line up with empty message. More context here: https://chat.zulip.org/#narrow/channel/101-design/topic/userlist.20avatar.20slow.20load/near/1974268 --- web/styles/right_sidebar.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/styles/right_sidebar.css b/web/styles/right_sidebar.css index 60be2c9d61..f8e6e03998 100644 --- a/web/styles/right_sidebar.css +++ b/web/styles/right_sidebar.css @@ -51,7 +51,7 @@ $user_status_emoji_width: 24px; } .buddy-list-user-link { - margin-left: 15px; + margin-left: 5px; } } From 858fdeee3927238a206643b74680d63dec020647 Mon Sep 17 00:00:00 2001 From: Harsh Date: Mon, 21 Oct 2024 19:39:02 +0530 Subject: [PATCH 193/276] import: Add function to normalize messages to import. This adds `normalize_body_for_import` to normalize messages from third-party importers by removing NUL bytes and also updates import test files data to test this. Fixes #31930. --- zerver/data_import/import_util.py | 3 +++ zerver/lib/message.py | 6 ++++++ .../fixtures/mattermost_fixtures/direct_channel/export.json | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/zerver/data_import/import_util.py b/zerver/data_import/import_util.py index 3953f58697..630ba4fdc2 100644 --- a/zerver/data_import/import_util.py +++ b/zerver/data_import/import_util.py @@ -17,6 +17,7 @@ from django.utils.timezone import now as timezone_now from zerver.data_import.sequencer import NEXT_ID from zerver.lib.avatar_hash import user_avatar_base_path_from_ids +from zerver.lib.message import normalize_body_for_import from zerver.lib.mime_types import guess_extension from zerver.lib.partial import partial from zerver.lib.stream_color import STREAM_ASSIGNMENT_COLORS as STREAM_COLORS @@ -499,6 +500,8 @@ def build_message( has_link: bool = False, has_attachment: bool = True, ) -> ZerverFieldsT: + # check and remove NULL Bytes if any. + content = normalize_body_for_import(content) zulip_message = Message( rendered_content_version=1, # this is Zulip specific id=message_id, diff --git a/zerver/lib/message.py b/zerver/lib/message.py index 570e9b39d5..07cb28f6a4 100644 --- a/zerver/lib/message.py +++ b/zerver/lib/message.py @@ -196,6 +196,12 @@ def normalize_body(body: str) -> str: return truncate_content(body, settings.MAX_MESSAGE_LENGTH, "\n[message truncated]") +def normalize_body_for_import(body: str) -> str: + if "\x00" in body: + body = re.sub(r"\x00", "", body) + return truncate_content(body, settings.MAX_MESSAGE_LENGTH, "\n[message truncated]") + + def truncate_topic(topic_name: str) -> str: return truncate_content(topic_name, MAX_TOPIC_NAME_LENGTH, "...") diff --git a/zerver/tests/fixtures/mattermost_fixtures/direct_channel/export.json b/zerver/tests/fixtures/mattermost_fixtures/direct_channel/export.json index be785e3875..73762c80f1 100644 --- a/zerver/tests/fixtures/mattermost_fixtures/direct_channel/export.json +++ b/zerver/tests/fixtures/mattermost_fixtures/direct_channel/export.json @@ -17,7 +17,7 @@ {"type":"direct_post","direct_post":{"channel_members":["ron","harry"],"user":"ron","message":"hey harry","create_at":1566376137676,"flagged_by":null,"reactions":null,"replies":null,"attachments":[{"path":"20210622/teams/noteam/channels/mcrm7xee5bnpzn7u9ktsd91dwy/users/knq189b88fdxbdkeeasdynia4o/o3to4ezua3bajj31mzpkn96n5e/harry-ron.jpg"}]}} {"type":"direct_post","direct_post":{"channel_members":["ron","harry"],"user":"harry","message":"what's up","create_at":1566376318568,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}} {"type":"direct_post","direct_post":{"channel_members":["ron","harry","ginny"],"user":"ginny","message":"Who is going to Hogsmeade this weekend?","create_at":1566376226493,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}} -{"type":"direct_post","direct_post":{"channel_members":["ron","harry","ginny"],"user":"harry","message":"I am going.","create_at":1566376311350,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}} +{"type":"direct_post","direct_post":{"channel_members":["ron","harry","ginny"],"user":"harry","message":"\u0000\u0001\u0001Hello How Are you\u0001\u0000\u0000\u0000\u0000","create_at":1566376311350,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}} {"type":"direct_post","direct_post":{"channel_members":["ron","harry","ginny"],"user":"ron","message":"I am going as well","create_at":1566376286363,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}} {"type":"direct_post","direct_post":{"channel_members":["harry","voldemort"],"user":"voldemort","message":"Hey Harry.","create_at":1566376318569,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}} {"type":"direct_post","direct_post":{"channel_members":["harry","voldemort"],"user":"harry","message":"Ahh. Here we go again.","create_at":1566376318579,"flagged_by":null,"reactions":null,"replies":null,"attachments":null}} From efa5b3cce115f3e7cbcd83d0a35997dfcee9257c Mon Sep 17 00:00:00 2001 From: Sayam Samal Date: Mon, 28 Oct 2024 19:11:55 +0530 Subject: [PATCH 194/276] stream_popover: Handle no messages condition for a topic being moved. Previously, when there were no messages in the topic being moved, the banner in the move topic modal only showed a "Failed" error banner which did not convey the actual error to the user. Also, a server call was being made even when there were no messages in the topic being moved. This commit explicitly handles the non-existent topic, prevents a call to the server when the message_id is undefined, and displays a more informative error banner to the user. --- web/src/stream_popover.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/src/stream_popover.js b/web/src/stream_popover.js index f627063061..a1c2127a5b 100644 --- a/web/src/stream_popover.js +++ b/web/src/stream_popover.js @@ -491,6 +491,16 @@ export async function build_move_topic_to_stream_popover( current_stream_id, old_topic_name, (message_id) => { + if (message_id === undefined) { + // There are no messages in the given topic, so we show an error banner + // and return, preventing any attempts to move a non-existent topic. + dialog_widget.hide_dialog_spinner(); + ui_report.client_error( + $t_html({defaultMessage: "There are no messages to move."}), + $("#move_topic_modal #dialog_error"), + ); + return; + } message_edit.move_topic_containing_message_to_stream( message_id, select_stream_id, From 65c9b249b7b7f0999bc1f1250b046346edac10c3 Mon Sep 17 00:00:00 2001 From: Kislay Udbhav Verma Date: Sun, 27 Oct 2024 16:50:08 +0530 Subject: [PATCH 195/276] markdown: Refactor classes handling stream topic links. The classes StreamPattern and StreamTopicPattern both had a separate copy of the function `find_stream_id` which did the same thing. Since another Pattern will be added as a part of #31920, it is a good idea to move that function into a superclass which is then inherited by all the related patterns. Fixes part of #31920 --- zerver/lib/markdown/__init__.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/zerver/lib/markdown/__init__.py b/zerver/lib/markdown/__init__.py index 74962206f8..f004d7035f 100644 --- a/zerver/lib/markdown/__init__.py +++ b/zerver/lib/markdown/__init__.py @@ -1984,7 +1984,7 @@ class UserGroupMentionPattern(CompiledInlineProcessor): return None, None, None -class StreamPattern(CompiledInlineProcessor): +class StreamTopicMessageProcessor(CompiledInlineProcessor): def find_stream_id(self, name: str) -> int | None: db_data: DbData | None = self.zmd.zulip_db_data if db_data is None: @@ -1992,6 +1992,8 @@ class StreamPattern(CompiledInlineProcessor): stream_id = db_data.stream_names.get(name) return stream_id + +class StreamPattern(StreamTopicMessageProcessor): @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str @@ -2016,14 +2018,7 @@ class StreamPattern(CompiledInlineProcessor): return el, m.start(), m.end() -class StreamTopicPattern(CompiledInlineProcessor): - def find_stream_id(self, name: str) -> int | None: - db_data: DbData | None = self.zmd.zulip_db_data - if db_data is None: - return None - stream_id = db_data.stream_names.get(name) - return stream_id - +class StreamTopicPattern(StreamTopicMessageProcessor): @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str From 000cc7bcde3a5d2d3d531191abd98f27b6320049 Mon Sep 17 00:00:00 2001 From: Kislay Udbhav Verma Date: Sun, 27 Oct 2024 18:40:11 +0530 Subject: [PATCH 196/276] markdown: Add support for a pretty syntax for message links. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Links to zulip messages can now be written as `#**channel_name > topic_name @ message_id**.` The `message_id` is replaced with `💬` in the rendered message. Fixes part of #31920 --- zerver/lib/markdown/__init__.py | 53 +++++++++++++++++++++++++++++++++ zerver/tests/test_markdown.py | 22 ++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/zerver/lib/markdown/__init__.py b/zerver/lib/markdown/__init__.py index f004d7035f..bffebd7634 100644 --- a/zerver/lib/markdown/__init__.py +++ b/zerver/lib/markdown/__init__.py @@ -194,6 +194,31 @@ def get_compiled_stream_topic_link_regex() -> Pattern[str]: ) +STREAM_TOPIC_MESSAGE_LINK_REGEX = rf""" + {BEFORE_MENTION_ALLOWED_REGEX} # Start after whitespace or specified chars + \#\*\* # and after hash sign followed by double asterisks + (?P[^\*>]+) # stream name can contain anything except > + > # > acts as separator + (?P[^\*]+) # topic name can contain anything + @ + (?P\d+) # message id + \*\* # ends by double asterisks + """ + + +@lru_cache(None) +def get_compiled_stream_topic_message_link_regex() -> Pattern[str]: + # Not using verbose_compile as it adds ^(.*?) and + # (.*?)$ which cause extra overhead of matching + # pattern which is not required. + # With new InlineProcessor these extra patterns + # are not required. + return re.compile( + STREAM_TOPIC_MESSAGE_LINK_REGEX, + re.DOTALL | re.VERBOSE, + ) + + @lru_cache(None) def get_web_link_regex() -> Pattern[str]: # We create this one time, but not at startup. So the @@ -2041,6 +2066,29 @@ class StreamTopicPattern(StreamTopicMessageProcessor): return el, m.start(), m.end() +class StreamTopicMessagePattern(StreamTopicMessageProcessor): + @override + def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 + self, m: Match[str], data: str + ) -> tuple[Element | str | None, int | None, int | None]: + stream_name = m.group("stream_name") + topic_name = m.group("topic_name") + message_id = m.group("message_id") + + stream_id = self.find_stream_id(stream_name) + if stream_id is None or topic_name is None: + return None, None, None + el = Element("a") + el.set("class", "message-link") + stream_url = encode_stream(stream_id, stream_name) + topic_url = hash_util_encode(topic_name) + link = f"/#narrow/channel/{stream_url}/topic/{topic_url}/near/{message_id}" + el.set("href", link) + text = f"#{stream_name} > {topic_name} @ 💬" + el.text = markdown.util.AtomicString(text) + return el, m.start(), m.end() + + def possible_linked_stream_names(content: str) -> set[str]: return { *re.findall(STREAM_LINK_REGEX, content, re.VERBOSE), @@ -2305,6 +2353,11 @@ class ZulipMarkdown(markdown.Markdown): ) reg.register(UserMentionPattern(mention.MENTIONS_RE, self), "usermention", 95) reg.register(Tex(TEX_RE, self), "tex", 90) + reg.register( + StreamTopicMessagePattern(get_compiled_stream_topic_message_link_regex(), self), + "stream_topic_message", + 89, + ) reg.register(StreamTopicPattern(get_compiled_stream_topic_link_regex(), self), "topic", 87) reg.register(StreamPattern(get_compiled_stream_link_regex(), self), "stream", 85) reg.register(Timestamp(TIMESTAMP_RE), "timestamp", 75) diff --git a/zerver/tests/test_markdown.py b/zerver/tests/test_markdown.py index 74caf860a3..74684fe75b 100644 --- a/zerver/tests/test_markdown.py +++ b/zerver/tests/test_markdown.py @@ -3108,6 +3108,28 @@ class MarkdownStreamMentionTests(ZulipTestCase): ".

                              ", ) + def test_message_id_multiple(self) -> None: + denmark = get_stream("Denmark", get_realm("zulip")) + sender_user_profile = self.example_user("othello") + msg = Message( + sender=sender_user_profile, + sending_client=get_client("test"), + realm=sender_user_profile.realm, + ) + content = "As mentioned in #**Denmark>danish@123** and #**Denmark>danish@456**." + self.assertEqual( + render_message_markdown(msg, content).rendered_content, + "

                              As mentioned in " + f'' + f"#Denmark > danish @ 💬" + " and " + f'' + f"#Denmark > danish @ 💬" + ".

                              ", + ) + def test_possible_stream_names(self) -> None: content = """#**test here** This mentions #**Denmark** too. From fad7a3f4b419e261920b95bddb049b6bd22348e7 Mon Sep 17 00:00:00 2001 From: Kislay Udbhav Verma Date: Sun, 27 Oct 2024 19:11:28 +0530 Subject: [PATCH 197/276] topic_link_util: Make stream topic link syntax consistent. We pad the `>` with spaces in the generated fallback link to make it consistent with other places where the syntax is used. Fixes part of #31920 --- web/src/topic_link_util.ts | 2 +- web/tests/copy_and_paste.test.js | 10 +++++----- web/tests/topic_link_util.test.js | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/web/src/topic_link_util.ts b/web/src/topic_link_util.ts index 320f36e689..5a83414bbe 100644 --- a/web/src/topic_link_util.ts +++ b/web/src/topic_link_util.ts @@ -43,7 +43,7 @@ export function get_fallback_markdown_link(stream_name: string, topic_name?: str assert(stream_id !== undefined); const escape = html_escape_markdown_syntax_characters; if (topic_name !== undefined) { - return `[#${escape(stream_name)}>${escape(topic_name)}](${internal_url.by_stream_topic_url(stream_id, topic_name, () => stream_name)})`; + return `[#${escape(stream_name)} > ${escape(topic_name)}](${internal_url.by_stream_topic_url(stream_id, topic_name, () => stream_name)})`; } return `[#${escape(stream_name)}](${internal_url.by_stream_url(stream_id, () => stream_name)})`; } diff --git a/web/tests/copy_and_paste.test.js b/web/tests/copy_and_paste.test.js index 98674b0aaf..7252bf735e 100644 --- a/web/tests/copy_and_paste.test.js +++ b/web/tests/copy_and_paste.test.js @@ -49,23 +49,23 @@ run_test("try_stream_topic_syntax_text", () => { // #**stream>topic** urls is pasted, a normal markdown link syntax is produced. [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20profits.60", - "[#Rome>100% profits`](#narrow/channel/4-Rome/topic/100.25.20profits.60)", + "[#Rome > 100% profits`](#narrow/channel/4-Rome/topic/100.25.20profits.60)", ], [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20*profits", - "[#Rome>100% *profits](#narrow/channel/4-Rome/topic/100.25.20*profits)", + "[#Rome > 100% *profits](#narrow/channel/4-Rome/topic/100.25.20*profits)", ], [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/.24.24 100.25.20profits", - "[#Rome>$$ 100% profits](#narrow/channel/4-Rome/topic/.24.24.20100.25.20profits)", + "[#Rome > $$ 100% profits](#narrow/channel/4-Rome/topic/.24.24.20100.25.20profits)", ], [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/>100.25.20profits", - "[#Rome>>100% profits](#narrow/channel/4-Rome/topic/.3E100.25.20profits)", + "[#Rome > >100% profits](#narrow/channel/4-Rome/topic/.3E100.25.20profits)", ], [ "http://zulip.zulipdev.com/#narrow/stream/5-Romeo.60s-lair/topic/normal", - "[#Romeo`s lair>normal](#narrow/channel/5-Romeo.60s-lair/topic/normal)", + "[#Romeo`s lair > normal](#narrow/channel/5-Romeo.60s-lair/topic/normal)", ], ]; diff --git a/web/tests/topic_link_util.test.js b/web/tests/topic_link_util.test.js index d1ba594390..c28df75343 100644 --- a/web/tests/topic_link_util.test.js +++ b/web/tests/topic_link_util.test.js @@ -47,31 +47,31 @@ run_test("stream_topic_link_syntax_test", () => { ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>t", "test `test` test"), - "[#Sweden>test `test` test](#narrow/channel/1-Sweden/topic/test.20.60test.60.20test)", + "[#Sweden > test `test` test](#narrow/channel/1-Sweden/topic/test.20.60test.60.20test)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Denmark>t", "test `test` test`s"), - "[#Denmark>test `test` test`s](#narrow/channel/2-Denmark/topic/test.20.60test.60.20test.60s)", + "[#Denmark > test `test` test`s](#narrow/channel/2-Denmark/topic/test.20.60test.60.20test.60s)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>typeah", "error due to *"), - "[#Sweden>error due to *](#narrow/channel/1-Sweden/topic/error.20due.20to.20*)", + "[#Sweden > error due to *](#narrow/channel/1-Sweden/topic/error.20due.20to.20*)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>t", "*asterisk"), - "[#Sweden>*asterisk](#narrow/channel/1-Sweden/topic/*asterisk)", + "[#Sweden > *asterisk](#narrow/channel/1-Sweden/topic/*asterisk)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>gibberish", "greaterthan>"), - "[#Sweden>greaterthan>](#narrow/channel/1-Sweden/topic/greaterthan.3E)", + "[#Sweden > greaterthan>](#narrow/channel/1-Sweden/topic/greaterthan.3E)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**$$MONEY$$>t", "dollar"), - "[#$$MONEY$$>dollar](#narrow/channel/6-.24.24MONEY.24.24/topic/dollar)", + "[#$$MONEY$$ > dollar](#narrow/channel/6-.24.24MONEY.24.24/topic/dollar)", ); assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>t", "swe$$dish"), - "[#Sweden>swe$$dish](#narrow/channel/1-Sweden/topic/swe.24.24dish)", + "[#Sweden > swe$$dish](#narrow/channel/1-Sweden/topic/swe.24.24dish)", ); assert.equal( topic_link_util.get_fallback_markdown_link("Sweden"), @@ -85,7 +85,7 @@ run_test("stream_topic_link_syntax_test", () => { assert.equal( topic_link_util.get_stream_topic_link_syntax("#**Sweden>&ab", "&ab"), - "[#Sweden>&ab](#narrow/channel/1-Sweden/topic/.26ab)", + "[#Sweden > &ab](#narrow/channel/1-Sweden/topic/.26ab)", ); // Only for full coverage of the module. From 5a24dad5f857f84fad63c175505aa216f15b9ec3 Mon Sep 17 00:00:00 2001 From: Kislay Udbhav Verma Date: Sun, 27 Oct 2024 19:13:14 +0530 Subject: [PATCH 198/276] copy_and_paste: Transform message links to pretty syntax. Similar to #29302, we transform zulip message links to a pretty syntax and in case there are some problematic characters (#30071), we generate a fallback markdown link. Fixes #31920 --- web/src/copy_and_paste.ts | 17 +++++++++++++++-- web/src/hash_util.ts | 13 +++++++++---- web/src/topic_link_util.ts | 16 ++++++++++++++-- web/tests/copy_and_paste.test.js | 11 +++++++++-- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/web/src/copy_and_paste.ts b/web/src/copy_and_paste.ts index 892444a053..14825f8fd2 100644 --- a/web/src/copy_and_paste.ts +++ b/web/src/copy_and_paste.ts @@ -673,20 +673,33 @@ export function try_stream_topic_syntax_text(text: string): string | null { assert(stream !== undefined); const stream_name = stream.name; if (topic_link_util.will_produce_broken_stream_topic_link(stream_name)) { - return topic_link_util.get_fallback_markdown_link(stream_name, stream_topic.topic_name); + return topic_link_util.get_fallback_markdown_link( + stream_name, + stream_topic.topic_name, + stream_topic.message_id, + ); } if ( stream_topic.topic_name !== undefined && topic_link_util.will_produce_broken_stream_topic_link(stream_topic.topic_name) ) { - return topic_link_util.get_fallback_markdown_link(stream_name, stream_topic.topic_name); + return topic_link_util.get_fallback_markdown_link( + stream_name, + stream_topic.topic_name, + stream_topic.message_id, + ); } let syntax_text = "#**" + stream_name; if (stream_topic.topic_name) { syntax_text += ">" + stream_topic.topic_name; } + + if (stream_topic.message_id !== undefined) { + syntax_text += "@" + stream_topic.message_id; + } + syntax_text += "**"; return syntax_text; } diff --git a/web/src/hash_util.ts b/web/src/hash_util.ts index ccf554f4e1..e4e1b3558d 100644 --- a/web/src/hash_util.ts +++ b/web/src/hash_util.ts @@ -293,7 +293,7 @@ export function validate_group_settings_hash(hash: string): string { export function decode_stream_topic_from_url( url_str: string, -): {stream_id: number; topic_name?: string} | null { +): {stream_id: number; topic_name?: string; message_id?: string} | null { try { const url = new URL(url_str); if (url.origin !== window.location.origin || !url.hash.startsWith("#narrow")) { @@ -303,9 +303,8 @@ export function decode_stream_topic_from_url( if (terms === undefined) { return null; } - if (terms.length > 2) { + if (terms.length > 3) { // The link should only contain stream and topic, - // near/ links are not transformed. return null; } // This check is important as a malformed url @@ -324,7 +323,13 @@ export function decode_stream_topic_from_url( if (terms[1]?.operator !== "topic") { return null; } - return {stream_id, topic_name: terms[1].operand}; + if (terms.length === 2) { + return {stream_id, topic_name: terms[1].operand}; + } + if (terms[2]?.operator !== "near") { + return null; + } + return {stream_id, topic_name: terms[1].operand, message_id: terms[2].operand}; } catch { return null; } diff --git a/web/src/topic_link_util.ts b/web/src/topic_link_util.ts index 5a83414bbe..80960ced2c 100644 --- a/web/src/topic_link_util.ts +++ b/web/src/topic_link_util.ts @@ -37,13 +37,25 @@ export function html_escape_markdown_syntax_characters(text: string): string { return text.replaceAll(invalid_stream_topic_regex, escape_invalid_stream_topic_characters); } -export function get_fallback_markdown_link(stream_name: string, topic_name?: string): string { +export function get_fallback_markdown_link( + stream_name: string, + topic_name?: string, + message_id?: string, +): string { const stream = stream_data.get_sub(stream_name); const stream_id = stream?.stream_id; assert(stream_id !== undefined); const escape = html_escape_markdown_syntax_characters; if (topic_name !== undefined) { - return `[#${escape(stream_name)} > ${escape(topic_name)}](${internal_url.by_stream_topic_url(stream_id, topic_name, () => stream_name)})`; + const stream_topic_url = internal_url.by_stream_topic_url( + stream_id, + topic_name, + () => stream_name, + ); + if (message_id !== undefined) { + return `[#${escape(stream_name)} > ${escape(topic_name)} @ 💬](${stream_topic_url}/near/${message_id})`; + } + return `[#${escape(stream_name)} > ${escape(topic_name)}](${stream_topic_url})`; } return `[#${escape(stream_name)}](${internal_url.by_stream_url(stream_id, () => stream_name)})`; } diff --git a/web/tests/copy_and_paste.test.js b/web/tests/copy_and_paste.test.js index 7252bf735e..2b883c8725 100644 --- a/web/tests/copy_and_paste.test.js +++ b/web/tests/copy_and_paste.test.js @@ -33,17 +33,20 @@ run_test("try_stream_topic_syntax_text", () => { ], ["http://different.origin.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT"], - + [ + "http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT/near/100", + "#**Rome>old FAILED EXPORT@100**", + ], // malformed urls ["http://zulip.zulipdev.com/narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT"], ["http://zulip.zulipdev.com/#not_narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT"], ["http://zulip.zulipdev.com/#narrow/not_stream/4-Rome/topic/old.20FAILED.20EXPORT"], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/not_topic/old.20FAILED.20EXPORT"], - ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT/near/100"], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/", "#**Rome**"], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic"], ["http://zulip.zulipdev.com/#narrow/topic/cheese"], ["http://zulip.zulipdev.com/#narrow/topic/pizza/stream/Rome"], + ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT/near/"], // When a url containing characters which are known to produce broken // #**stream>topic** urls is pasted, a normal markdown link syntax is produced. @@ -67,6 +70,10 @@ run_test("try_stream_topic_syntax_text", () => { "http://zulip.zulipdev.com/#narrow/stream/5-Romeo.60s-lair/topic/normal", "[#Romeo`s lair > normal](#narrow/channel/5-Romeo.60s-lair/topic/normal)", ], + [ + "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20profits.60/near/20", + "[#Rome > 100% profits` @ 💬](#narrow/channel/4-Rome/topic/100.25.20profits.60/near/20)", + ], ]; for (const test_case of test_cases) { From c73985da17231e1f6a4d1b1a6cb21838bc37e725 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Sat, 2 Nov 2024 20:04:41 +0530 Subject: [PATCH 199/276] icon: Replace trash icon with archive icon. Updated the existing trash icon used for the archive action with a proper archive icon to improve clarity and match its intended purpose. --- help/archive-a-channel.md | 6 +++--- web/shared/icons/archive.svg | Bin 0 -> 566 bytes web/styles/subscriptions.css | 8 ++++++++ web/templates/stream_settings/stream_settings.hbs | 6 +++++- 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 web/shared/icons/archive.svg diff --git a/help/archive-a-channel.md b/help/archive-a-channel.md index e4b52eed5e..379dc13825 100644 --- a/help/archive-a-channel.md +++ b/help/archive-a-channel.md @@ -23,7 +23,7 @@ archiving them where appropriate. 1. Select a channel. -1. Click the **trash** icon near the top right +1. Click the **archive** icon near the top right corner of the channel settings panel. 1. Approve by clicking **Confirm**. @@ -32,8 +32,8 @@ archiving them where appropriate. You can also hover over a channel in the left sidebar, click on the **ellipsis** (), and - select **Channel settings** to access the **trash** - icon. + select **Channel settings** to access the **archive** + icon. {end_tabs} diff --git a/web/shared/icons/archive.svg b/web/shared/icons/archive.svg new file mode 100644 index 0000000000000000000000000000000000000000..1d77140f04f749a649abfa609bbd119f420c60dc GIT binary patch literal 566 zcmZuvO>cuR488L!tn5rk0tA=Dr!QK*4>;G@^T@!T27Y^W|GJJb{L^A#g;{NP}0v(2f;U>cz(

                              {{#if is_realm_admin}} - + {{/if}}

                              {{/with}} From ccad062f3c0d9abb09d23c2ba36d1b58f1bead5b Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Tue, 29 Oct 2024 10:10:43 -0500 Subject: [PATCH 200/276] left_sidebar: Disallow selection of unread counts. --- web/styles/left_sidebar.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index 1e19cbf620..c73b61267d 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -25,6 +25,10 @@ text-overflow: ellipsis; } +#left-sidebar .unread_count { + user-select: none; +} + .sidebar-topic-check, .topic-markers-and-unreads { cursor: pointer; From b44af63d47c674ae03a90ca2f51138491c1e7b60 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Thu, 10 Oct 2024 11:08:33 -0500 Subject: [PATCH 201/276] left_sidebar: Update DM partners icon color. --- web/styles/app_variables.css | 6 ++++++ web/styles/left_sidebar.css | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index 8ad45feb09..94fec7da14 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -823,6 +823,9 @@ --color-left-sidebar-navigation-icon: hsl(240deg 30% 40%); --color-left-sidebar-heads-up-icon: hsl(240deg 30% 40%); --color-left-sidebar-heads-up-icon-hover: hsl(240deg 100% 15%); + --color-left-sidebar-dm-partners-icon: var( + --color-left-sidebar-navigation-icon + ); --background-color-left-sidebar-heads-up-icon-hover: hsl( 240deg 100% 50% / 7% ); @@ -1355,6 +1358,9 @@ --color-left-sidebar-navigation-icon: hsl(240deg 35% 68%); --color-left-sidebar-heads-up-icon: hsl(240deg 35% 68%); --color-left-sidebar-heads-up-icon-hover: hsl(240deg 100% 90%); + --color-left-sidebar-dm-partners-icon: var( + --color-left-sidebar-navigation-icon + ); --background-color-left-sidebar-heads-up-icon-hover: hsl( 240deg 100% 75% / 20% ); diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index c73b61267d..63987d4b46 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -291,7 +291,7 @@ } .zulip-icon { - opacity: 0.7; + color: var(--color-left-sidebar-dm-partners-icon); } } From 62b17217f5135e4a466237906542ac74a766bff9 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Mon, 4 Nov 2024 13:44:44 -0500 Subject: [PATCH 202/276] unread_counters: Place new colors and set prominent value as default. --- web/styles/app_variables.css | 46 +++++++++++++++++++++++++++--------- web/styles/left_sidebar.css | 7 +++--- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index 94fec7da14..51f99ad541 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -611,16 +611,30 @@ --color-background-active-popover-menu: hsl(220deg 12% 5% / 7%); --color-border-popover-menu: hsl(0deg 0% 0% / 40%); --color-border-personal-menu-avatar: hsl(0deg 0% 0% / 10%); - --color-background-unread-counter: hsl(105deg 2% 50%); + --color-background-unread-counter-prominent: hsl(240deg 10% 50% / 70%); + --color-background-unread-counter-normal: hsl(240deg 10% 50% / 25%); + --color-background-unread-counter-quiet: transparent; + --color-unread-counter-prominent: hsl(0deg 0% 100%); + --color-unread-counter-normal: hsl(0deg 0% 0% / 90%); + --color-unread-counter-quiet: hsl(240deg 15% 50%); + /* Legacy unread-counter color value. */ + --color-background-unread-counter: var( + --color-background-unread-counter-prominent + ); + --color-unread-counter-muted: hsl(240deg 10% 50% / 35%); --color-border-add-subscription-button-focus: hsl(0deg 0% 20%); - /* There's no alpha channel here, but this keeps - the variable names in line. */ - --color-background-unread-counter-no-alpha: var( - --color-background-unread-counter - ); - --color-background-unread-counter-dot: var( - --color-background-unread-counter + /* When unreads are hovered on the condensed + views, they should not have an alpha. + The first color corresponds to + --color-background-unread-counter-prominent. + The second color aligns with light mode's + --color-background. */ + --color-background-unread-counter-no-alpha: color-mix( + in srgb, + hsl(240deg 10% 50%) 70%, + hsl(0deg 0% 94%) ); + --color-background-unread-counter-dot: hsl(240deg 30% 40%); --color-border-unread-counter: var(--color-background-unread-counter); --color-border-unread-counter-popover-menu: inherit; --color-background-tab-picker-container: hsl(0deg 0% 0% / 7%); @@ -1142,7 +1156,6 @@ --color-background-active-popover-menu: hsl(220deg 12% 95% / 7%); --color-border-popover-menu: hsl(0deg 0% 0%); --color-border-personal-menu-avatar: hsl(0deg 0% 100% / 20%); - --color-background-unread-counter: hsl(105deg 2% 50% / 50%); /* When unreads are hovered on the condensed views, they should not have an alpha. @@ -1152,10 +1165,21 @@ an rgb() value by PostCSS Preset Env. */ --color-background-unread-counter-no-alpha: color-mix( in srgb, - hsl(105deg 2% 50%) 50%, + hsl(240deg 10% 50%) 35%, hsl(0deg 0% 11%) ); - --color-background-unread-counter-dot: hsl(105deg 2% 50% / 90%); + --color-background-unread-counter-dot: hsl(240deg 35% 68%); + --color-background-unread-counter-prominent: hsl(240deg 18.37% 34.42%); + --color-background-unread-counter-normal: hsl(240deg 10% 50% / 35%); + --color-background-unread-counter-quiet: transparent; + --color-unread-counter-prominent: hsl(0deg 0% 100%); + --color-unread-counter-normal: hsl(0deg 0% 100% / 85%); + --color-unread-counter-quiet: hsl(240deg 15% 60%); + /* Legacy unread-counter color value. */ + --color-background-unread-counter: var( + --color-background-unread-counter-prominent + ); + --color-unread-counter-muted: hsl(240deg 10% 50% / 35%); --color-border-unread-counter: hsl(105deg 2% 50%); --color-border-unread-counter-popover-menu: var( --color-border-unread-counter diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index 63987d4b46..fa9fd4af19 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -71,7 +71,7 @@ /* Masked unreads display as flex when revealed. */ align-items: center; justify-content: center; - color: var(--color-masked-unread-marker); + color: var(--color-unread-counter-muted); width: var(--left-sidebar-single-digit-unread-width); } @@ -719,9 +719,8 @@ li.active-sub-filter { .top_left_drafts .unread_count, .top_left_scheduled_messages .unread_count, .condensed-views-popover-menu .unread_count { - background-color: unset; - color: inherit; - border: 0.5px solid var(--color-border-unread-counter); + background-color: var(--color-background-unread-counter-quiet); + color: var(--color-unread-counter-quiet); } /* Don't show unread counts on views... */ From 16b8c7562ae6c7bd54ca3e418d5b76f3b3216734 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Thu, 10 Oct 2024 13:19:42 -0500 Subject: [PATCH 203/276] compose: Update colors for draft popover. --- web/styles/compose.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/styles/compose.css b/web/styles/compose.css index dda8132757..f4da4fad59 100644 --- a/web/styles/compose.css +++ b/web/styles/compose.css @@ -1151,9 +1151,8 @@ textarea.new_message_textarea { .unread_count { margin: 1px 0 0 6px; - border: 0.5px solid var(--color-border-unread-counter); - background-color: unset; - color: inherit; + background-color: var(--color-background-unread-counter-quiet); + color: var(--color-unread-counter-quiet); } } From ee63e836f8b87d5716197a2af6a1d1f01e80ef98 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Tue, 15 Oct 2024 11:33:38 -0500 Subject: [PATCH 204/276] marker_icons: Reduce opacity to spec on follow, mention icons. --- web/styles/app_components.css | 2 +- web/styles/left_sidebar.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/styles/app_components.css b/web/styles/app_components.css index c6469d5726..782d9d916b 100644 --- a/web/styles/app_components.css +++ b/web/styles/app_components.css @@ -626,7 +626,7 @@ input.settings_text_input { .unread_mention_info:not(:empty) { margin-right: 5px; margin-left: 2px; - opacity: 0.7; + opacity: 0.5; } /* Implement the web app's default-hidden convention for alert diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index fa9fd4af19..acc568228c 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -496,7 +496,7 @@ ul.filters { } .zulip-icon-follow { - opacity: 0.6; + opacity: 0.5; &:hover { opacity: 1; From 2e1ab1c470a2d3ae93ce20c638d9a37632399b48 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Mon, 4 Nov 2024 16:21:07 -0500 Subject: [PATCH 205/276] unread_counters: Adjust hover styles for inbox, recent views. --- web/styles/inbox.css | 6 ++++-- web/styles/recent_view.css | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/web/styles/inbox.css b/web/styles/inbox.css index 49596c7ab7..e10ec81aaa 100644 --- a/web/styles/inbox.css +++ b/web/styles/inbox.css @@ -19,10 +19,12 @@ } .unread_count { - opacity: 0.7; + opacity: 1; + outline: 0 solid var(--color-background-unread-counter); + transition: outline-width 0.1s ease; &:hover { - opacity: 1; + outline-width: 1.5px; } } diff --git a/web/styles/recent_view.css b/web/styles/recent_view.css index 1e929cdc75..b96b66eeb7 100644 --- a/web/styles/recent_view.css +++ b/web/styles/recent_view.css @@ -45,7 +45,6 @@ align-items: center; .recent-view-table-link, - .recent-view-table-unread-count, & > .zulip-icon { outline: 0; } @@ -191,7 +190,13 @@ margin-right: 1px; margin-left: 1px; align-self: center; - background-color: hsl(105deg 2% 50%); + opacity: 1; + outline: 0 solid var(--color-background-unread-counter); + transition: outline-width 0.1s ease; + + &:hover { + outline-width: 1.5px; + } } .unread_mention_info:not(:empty) { From c08246c68168365848fec5e8fd2f9b196cdf5f27 Mon Sep 17 00:00:00 2001 From: Alya Abbott Date: Mon, 4 Nov 2024 15:48:50 -0800 Subject: [PATCH 206/276] help: Document UI changes in right sidebar. --- help/user-list.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/help/user-list.md b/help/user-list.md index 6cdc613668..a3c3fd0a42 100644 --- a/help/user-list.md +++ b/help/user-list.md @@ -1,17 +1,20 @@ # User list In the Zulip web and desktop app, the right sidebar shows a list of users in -your organization. It shows -[non-deactivated](/help/deactivate-or-reactivate-a-user) users, and does not -include [bots](/help/bots-overview). +your organization. The user list has up to three section: -In organizations with up to 600 users, everyone is shown. In larger organizations, -only users who have been active in the last two weeks are listed. All users are -included when you search for people. +- **In this conversation**: Recent participants in the conversation you're viewing. +- **In this channel** or **Others in this channel**: Subscribers to the channel you're viewing. +- **Others**: Everyone else. -When you view a channel or a topic within a channel, subscribers to that channel -are shown separately from other members of your organization. To avoid -distraction, you can hide the user list any time. +In organizations with up to 600 users, everyone is shown. In larger +organizations, only users who have been active in the last two weeks are shown, +but everyone is included when you [search](#filter-users). +[Deactivated users](/help/deactivate-or-reactivate-a-user) and +[bots](/help/bots-overview) are not listed. + +To avoid distraction, you can [hide](#show-or-hide-the-user-list) the user list +any time. ## Filter users @@ -19,11 +22,11 @@ distraction, you can hide the user list any time. {tab|desktop-web} -1. If the user list in the right sidebar is hidden, click the **user list** ( ) icon in the upper right to -show it. +1. If the user list is hidden, click the **user list** ( ) icon in the upper right to show it. -1. Type the name of the user you are looking for in the search box. +1. Type the name of the user you are looking for in the **Filter users** box at + the top of the right sidebar. !!! keyboard_tip "" From eea2499e0289283093094a17804f76f30e6058b3 Mon Sep 17 00:00:00 2001 From: Alya Abbott Date: Mon, 4 Nov 2024 16:22:04 -0800 Subject: [PATCH 207/276] zulip updates: Add update about sidebars and file size limit. --- zerver/lib/zulip_update_announcements.py | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/zerver/lib/zulip_update_announcements.py b/zerver/lib/zulip_update_announcements.py index 1c9c83dc2c..ab456f62cf 100644 --- a/zerver/lib/zulip_update_announcements.py +++ b/zerver/lib/zulip_update_announcements.py @@ -187,6 +187,40 @@ feature highlights in Zulip Server 9.0, and other Zulip project updates. blog_post_9_0_url="https://blog.zulip.com/zulip-server-9-0", ), ), + ZulipUpdateAnnouncement( + level=9, + message=( + ( + """ +- You can now [upload large files]({file_upload_limits_help_url}) up to + 1 GB in organizations on Zulip Cloud + Standard or Zulip Cloud Plus [plans]({cloud_plans_url}). +""" + if settings.CORPORATE_ENABLED + else """ +- You can now [upload large files]({file_upload_limits_help_url}), up to + the limit configured by your server's administrator (currently {max_file_upload_size} MB). +""" + ) + + """ + +**Web and desktop updates** +- You can now start a new conversation from the left sidebar. Click the `+` +button next to the name of a channel to [start a new +topic]({how_to_start_a_new_topic_help_url}) in that channel, or the `+` next to +DIRECT MESSAGES to [start a DM]({starting_a_new_direct_message_help_url}). +- The [user list]({user_list_help_url}) now shows recent participants in the + conversation you're viewing. +""" + ).format( + how_to_start_a_new_topic_help_url="/help/introduction-to-topics#how-to-start-a-new-topic", + starting_a_new_direct_message_help_url="/help/starting-a-new-direct-message", + user_list_help_url="/help/user-list", + cloud_plans_url="/plans/", + file_upload_limits_help_url="/help/share-and-upload-files#file-upload-limits", + max_file_upload_size=settings.MAX_FILE_UPLOAD_SIZE, + ), + ), ] From 9ac1d641387e746930ea2a95d71732add6cfad39 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Mon, 4 Nov 2024 16:59:06 -0800 Subject: [PATCH 208/276] devtools: Document how to send zulip_updates notices. --- templates/zerver/development/dev_tools.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/zerver/development/dev_tools.html b/templates/zerver/development/dev_tools.html index cd8e00ff73..84381701a5 100644 --- a/templates/zerver/development/dev_tools.html +++ b/templates/zerver/development/dev_tools.html @@ -88,6 +88,8 @@
                            • ./manage.py mark_all_messages_unread: Useful for testing reading messages.
                            • ./manage.py create_realm: Add a new realm. Useful for testing onboarding.
                            • ./manage.py create_user: Add a new user. Useful for testing onboarding.
                            • +
                            • ./manage.py send_zulip_update_announcements: Send Zulip + update notices drafted in `zerver/lib/zulip_update_announcements.py`.
                            • ./manage.py add_mock_conversation: Add test messages, streams, images, emoji, etc. into the dev environment. First edit zilencer/management/commands/add_mock_conversation.py to add the data you're testing. From 5571fb6a3e93c6d19c08064ebc0c1f64714a39b3 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Mon, 4 Nov 2024 22:10:37 -0800 Subject: [PATCH 209/276] migrations: Fix migration 0617 case sensitivity bug. --- zerver/migrations/0617_remove_prefix_from_archived_streams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zerver/migrations/0617_remove_prefix_from_archived_streams.py b/zerver/migrations/0617_remove_prefix_from_archived_streams.py index 9b2aeee0d6..4c7898da0a 100644 --- a/zerver/migrations/0617_remove_prefix_from_archived_streams.py +++ b/zerver/migrations/0617_remove_prefix_from_archived_streams.py @@ -32,7 +32,7 @@ def remove_prefix_from_archived_streams( continue # Check if there's an active stream or another archived stream with the new name - if not Stream.objects.filter(realm=archived_stream.realm, name=new_name).exists(): + if not Stream.objects.filter(realm=archived_stream.realm, name__iexact=new_name).exists(): archived_stream.name = new_name archived_stream.save(update_fields=["name"]) From c073e5adb45a24f1e825ea086e00e46f304ff27e Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Mon, 4 Nov 2024 23:06:44 -0800 Subject: [PATCH 210/276] migrations: Avoid following realm foreign key unnecessarily. I encountered at least one scenario where the previous logic would fail due to migration state confusion, but this could also be a bit faster. --- zerver/migrations/0617_remove_prefix_from_archived_streams.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zerver/migrations/0617_remove_prefix_from_archived_streams.py b/zerver/migrations/0617_remove_prefix_from_archived_streams.py index 4c7898da0a..0083516fbd 100644 --- a/zerver/migrations/0617_remove_prefix_from_archived_streams.py +++ b/zerver/migrations/0617_remove_prefix_from_archived_streams.py @@ -32,7 +32,9 @@ def remove_prefix_from_archived_streams( continue # Check if there's an active stream or another archived stream with the new name - if not Stream.objects.filter(realm=archived_stream.realm, name__iexact=new_name).exists(): + if not Stream.objects.filter( + realm_id=archived_stream.realm_id, name__iexact=new_name + ).exists(): archived_stream.name = new_name archived_stream.save(update_fields=["name"]) From 3d5c72f7627ccd6622a571db9626d87993c965bd Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Tue, 22 Oct 2024 18:01:17 +0200 Subject: [PATCH 211/276] invite-modal: Remove "expires_in" for attribute for custom time input. The "for" attribute on this label meant that clicking the label moved the focus to the select element above the custom time input element. --- web/templates/invite_user_modal.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/invite_user_modal.hbs b/web/templates/invite_user_modal.hbs index 9bd7217603..5d18271bd4 100644 --- a/web/templates/invite_user_modal.hbs +++ b/web/templates/invite_user_modal.hbs @@ -42,7 +42,7 @@

                              - + +

                              From 8d79adceed58ebe49837cfba4831768617ab5108 Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Tue, 5 Nov 2024 15:52:15 +0100 Subject: [PATCH 213/276] invite-modal: Remove unused "custom_time_choice" name and class. In HTML, option elements do not use/have the "name" attribute, so the custom time choice select options do not need it to be set. The "custom_time_choice" class is not referenced anywhere else in the codebase, so it can be removed to make this template more readable. --- web/templates/invite_user_modal.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/invite_user_modal.hbs b/web/templates/invite_user_modal.hbs index d564801abd..00c856c966 100644 --- a/web/templates/invite_user_modal.hbs +++ b/web/templates/invite_user_modal.hbs @@ -46,7 +46,7 @@

                              From 62b365cb3144b3912db0bb59234b7dbefea4e352 Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Tue, 5 Nov 2024 16:05:33 +0100 Subject: [PATCH 214/276] invite-modal: Add name attribute to custom time unit select element. Reorders the select element attributes to be more readable. --- web/templates/invite_user_modal.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/invite_user_modal.hbs b/web/templates/invite_user_modal.hbs index 00c856c966..71c38b65a7 100644 --- a/web/templates/invite_user_modal.hbs +++ b/web/templates/invite_user_modal.hbs @@ -44,7 +44,7 @@
                              - {{#each time_choices}} {{/each}} From 15f010d6c9bc31b2d5e5dc5655bd234e509d5ca6 Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Tue, 22 Oct 2024 18:22:26 +0200 Subject: [PATCH 215/276] invite-modal: Update custom time input element. Adds "custom-expiration-time-input" name to input element and orders attributes in template for readability. --- web/templates/invite_user_modal.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/invite_user_modal.hbs b/web/templates/invite_user_modal.hbs index 71c38b65a7..48e89caa65 100644 --- a/web/templates/invite_user_modal.hbs +++ b/web/templates/invite_user_modal.hbs @@ -43,7 +43,7 @@

                              - + - - +
                              From 9a40319bfc4513db699f5983a9b233a7c5b97a27 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Tue, 5 Nov 2024 20:10:42 +0530 Subject: [PATCH 217/276] management: Add option to reset all the active realms to a level. This commit adds a --reset-level optional argument to send_zulip_update_announcements management command to reset all the active realms to a given level. --- .../commands/send_zulip_update_announcements.py | 13 +++++++++++++ zerver/tests/test_management_commands.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/zerver/management/commands/send_zulip_update_announcements.py b/zerver/management/commands/send_zulip_update_announcements.py index 350883cfd8..fd3b9cdec4 100644 --- a/zerver/management/commands/send_zulip_update_announcements.py +++ b/zerver/management/commands/send_zulip_update_announcements.py @@ -1,10 +1,12 @@ from argparse import ArgumentParser from typing import Any +from django.conf import settings from typing_extensions import override from zerver.lib.management import ZulipBaseCommand, abort_unless_locked from zerver.lib.zulip_update_announcements import send_zulip_update_announcements +from zerver.models import Realm class Command(ZulipBaseCommand): @@ -17,8 +19,19 @@ class Command(ZulipBaseCommand): action="store_true", help="Immediately send updates if 'zulip_update_announcements_stream' is configured.", ) + parser.add_argument( + "--reset-level", + type=int, + help="The level to reset all active realms to.", + ) @override @abort_unless_locked def handle(self, *args: Any, **options: Any) -> None: + if options["reset_level"] is not None: + Realm.objects.filter(deactivated=False).exclude( + string_id=settings.SYSTEM_BOT_REALM + ).update(zulip_update_announcements_level=options["reset_level"]) + return + send_zulip_update_announcements(skip_delay=options["skip_delay"]) diff --git a/zerver/tests/test_management_commands.py b/zerver/tests/test_management_commands.py index c91bbd4133..8e37df70df 100644 --- a/zerver/tests/test_management_commands.py +++ b/zerver/tests/test_management_commands.py @@ -588,3 +588,17 @@ class TestSendCustomEmail(ZulipTestCase): call(" hamlet@zulip.com (zulip)"), ], ) + + +class TestSendZulipUpdateAnnouncements(ZulipTestCase): + COMMAND_NAME = "send_zulip_update_announcements" + + def test_reset_level(self) -> None: + realm = get_realm("zulip") + realm.zulip_update_announcements_level = 9 + realm.save() + + call_command(self.COMMAND_NAME, "--reset-level=5") + + realm.refresh_from_db() + self.assertEqual(realm.zulip_update_announcements_level, 5) From 3ebc507ddba49029f2ee476301b7bfe99677c3a8 Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Mon, 4 Nov 2024 19:00:48 +0100 Subject: [PATCH 218/276] signups: Move logic for realm admin notification to corporate app. As we would like to send similar notifications for other billing state changes (for all BillingSession types), it makes sense to move the logic for creating and sending these admin realm internal messages to the BillingSession framework in the corporate app. In the case that a channel with the specified name does not exist, we now send direct messages to the admin realm administrators with the channel, topic and message so that the information is not lost and so that the channel for these messages can be created. --- corporate/lib/stripe.py | 36 +++++++++++++++++++++++++++++- zerver/actions/create_realm.py | 40 +++++----------------------------- zerver/lib/streams.py | 5 ----- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index c7d91151db..bbf7264a1b 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -54,9 +54,10 @@ from zerver.lib.send_email import ( from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.url_encoding import append_url_query_string from zerver.lib.utils import assert_is_not_none -from zerver.models import Realm, RealmAuditLog, UserProfile +from zerver.models import Realm, RealmAuditLog, Stream, UserProfile from zerver.models.realm_audit_logs import AuditLogEventType from zerver.models.realms import get_org_type_display_name, get_realm +from zerver.models.streams import get_stream from zerver.models.users import get_system_bot from zilencer.lib.remote_counts import MissingDataError from zilencer.models import ( @@ -3843,6 +3844,31 @@ class BillingSession(ABC): return last_ledger + def send_support_admin_realm_internal_message( + self, channel_name: str, topic: str, message: str + ) -> None: + from zerver.actions.message_send import ( + internal_send_private_message, + internal_send_stream_message, + ) + + admin_realm = get_realm(settings.SYSTEM_BOT_REALM) + sender = get_system_bot(settings.NOTIFICATION_BOT, admin_realm.id) + try: + channel = get_stream(channel_name, admin_realm) + internal_send_stream_message( + sender, + channel, + topic, + message, + ) + except Stream.DoesNotExist: # nocoverage + direct_message = ( + f":red_circle: Channel named '{channel_name}' doesn't exist.\n\n{topic}:\n{message}" + ) + for user in admin_realm.get_human_admin_users(): + internal_send_private_message(sender, user, direct_message) + class RealmBillingSession(BillingSession): def __init__( @@ -4212,6 +4238,14 @@ class RealmBillingSession(BillingSession): # anything weird. pass + def send_realm_created_internal_admin_message(self) -> None: + channel = "signups" + topic = "new organizations" + support_url = self.support_url() + organization_type = get_org_type_display_name(self.realm.org_type) + message = f"[{self.realm.name}]({support_url}) ([{self.realm.display_subdomain}]({self.realm.url})). Organization type: {organization_type}" + self.send_support_admin_realm_internal_message(channel, topic, message) + class RemoteRealmBillingSession(BillingSession): def __init__( diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index edd03d6de0..08f69ad4e1 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -9,7 +9,6 @@ from django.utils.translation import gettext as _ from django.utils.translation import override as override_language from confirmation import settings as confirmation_settings -from zerver.actions.message_send import internal_send_stream_message from zerver.actions.realm_settings import ( do_add_deactivated_redirect, do_change_realm_plan_type, @@ -19,7 +18,7 @@ from zerver.lib.bulk_create import create_users from zerver.lib.push_notifications import sends_notifications_directly from zerver.lib.remote_server import maybe_enqueue_audit_log_upload from zerver.lib.server_initialization import create_internal_realm, server_initialized -from zerver.lib.streams import ensure_stream, get_signups_stream +from zerver.lib.streams import ensure_stream from zerver.lib.user_groups import ( create_system_user_groups_for_realm, get_role_based_system_groups_dict, @@ -32,19 +31,12 @@ from zerver.models import ( RealmAuditLog, RealmAuthenticationMethod, RealmUserDefault, - Stream, UserProfile, ) from zerver.models.groups import SystemGroups from zerver.models.presence import PresenceSequence from zerver.models.realm_audit_logs import AuditLogEventType -from zerver.models.realms import ( - CommonPolicyEnum, - InviteToRealmPolicyEnum, - get_org_type_display_name, - get_realm, -) -from zerver.models.users import get_system_bot +from zerver.models.realms import CommonPolicyEnum, InviteToRealmPolicyEnum from zproject.backends import all_default_backend_names @@ -369,32 +361,12 @@ def do_create_realm( prereg_realm.created_realm = realm prereg_realm.save(update_fields=["status", "created_realm"]) - # Send a notification to the admin realm when a new organization registers. if settings.CORPORATE_ENABLED: - from corporate.lib.support import get_realm_support_url + # Send a notification to the admin realm when a new organization registers. + from corporate.lib.stripe import RealmBillingSession - admin_realm = get_realm(settings.SYSTEM_BOT_REALM) - sender = get_system_bot(settings.NOTIFICATION_BOT, admin_realm.id) - - support_url = get_realm_support_url(realm) - organization_type = get_org_type_display_name(realm.org_type) - - message = f"[{realm.name}]({support_url}) ([{realm.display_subdomain}]({realm.url})). Organization type: {organization_type}" - topic_name = "new organizations" - - try: - signups_stream = get_signups_stream(admin_realm) - - internal_send_stream_message( - sender, - signups_stream, - topic_name, - message, - ) - except Stream.DoesNotExist: # nocoverage - # If the signups stream hasn't been created in the admin - # realm, don't auto-create it to send to it; just do nothing. - pass + billing_session = RealmBillingSession(user=None, realm=realm) + billing_session.send_realm_created_internal_admin_message() setup_realm_internal_bots(realm) return realm diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index 9f8ae14c1e..7c9be859d8 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -835,11 +835,6 @@ def get_stream_by_narrow_operand_access_unchecked(operand: str | int, realm: Rea return get_stream_by_id_in_realm(operand, realm) -def get_signups_stream(realm: Realm) -> Stream: - # This one-liner helps us work around a lint rule. - return get_stream("signups", realm) - - def ensure_stream( realm: Realm, stream_name: str, From 7416e9c29c4da821bb4b1e3cf12f3b6f7bbbfb08 Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Mon, 4 Nov 2024 21:01:04 +0100 Subject: [PATCH 219/276] support: Consolidate logic for generating support URLs. The build_support_url is a more generic function that's used in the BillingSession framework and will generate the same URL for these Zulip Cloud support requests. --- corporate/lib/support.py | 14 +------------- corporate/views/support.py | 4 ++-- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/corporate/lib/support.py b/corporate/lib/support.py index 23cc5933a8..c013bc8887 100644 --- a/corporate/lib/support.py +++ b/corporate/lib/support.py @@ -1,11 +1,8 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Optional, TypedDict, Union -from urllib.parse import urlencode, urljoin, urlunsplit -from django.conf import settings from django.db.models import Sum -from django.urls import reverse from django.utils.timezone import now as timezone_now from corporate.lib.stripe import ( @@ -30,7 +27,7 @@ from corporate.models import ( ) from zerver.models import Realm from zerver.models.realm_audit_logs import AuditLogEventType -from zerver.models.realms import get_org_type_display_name, get_realm +from zerver.models.realms import get_org_type_display_name from zilencer.lib.remote_counts import MissingDataError from zilencer.models import ( RemoteCustomerUserCount, @@ -130,15 +127,6 @@ def get_stripe_customer_url(stripe_id: str) -> str: return f"https://dashboard.stripe.com/customers/{stripe_id}" # nocoverage -def get_realm_support_url(realm: Realm) -> str: - support_realm_url = get_realm(settings.STAFF_SUBDOMAIN).url - support_url = urljoin( - support_realm_url, - urlunsplit(("", "", reverse("support"), urlencode({"q": realm.string_id}), "")), - ) - return support_url - - def get_realm_user_data(realm: Realm) -> UserData: non_guests = get_non_guest_user_count(realm) guests = get_guest_user_count(realm) diff --git a/corporate/views/support.py b/corporate/views/support.py index bcac54854a..2b2338bafe 100644 --- a/corporate/views/support.py +++ b/corporate/views/support.py @@ -98,7 +98,7 @@ class DemoRequestForm(forms.Form): @zulip_login_required @typed_endpoint_without_parameters def support_request(request: HttpRequest) -> HttpResponse: - from corporate.lib.support import get_realm_support_url + from corporate.lib.stripe import build_support_url user = request.user assert user.is_authenticated @@ -119,7 +119,7 @@ def support_request(request: HttpRequest) -> HttpResponse: "realm_string_id": user.realm.string_id, "request_subject": form.cleaned_data["request_subject"], "request_message": form.cleaned_data["request_message"], - "support_url": get_realm_support_url(user.realm), + "support_url": build_support_url("support", user.realm.string_id), "user_role": user.get_role_name(), } # Sent to the server's support team, so this email is not user-facing. From 10f03fce111c90fd13b92853dc5e08a1a42194e9 Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Tue, 5 Nov 2024 12:12:11 +0100 Subject: [PATCH 220/276] demo-orgs: Specify email input when rendering add email modal. There are two inputs for the add email modal in demo organizations, one to add the email address of the owner and one to update their full name. We want the email input to be selected when the modal is opened. --- web/src/settings_account.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/src/settings_account.ts b/web/src/settings_account.ts index 8d523e7f0e..a06d0f8eae 100644 --- a/web/src/settings_account.ts +++ b/web/src/settings_account.ts @@ -713,9 +713,7 @@ export function set_up( form_id: "demo_organization_add_email_form", on_click: do_demo_organization_add_email, on_shown() { - ui_util.place_caret_at_end( - util.the($("#demo_organization_add_email_form input")), - ); + ui_util.place_caret_at_end(util.the($("input#demo_organization_add_email"))); }, post_render: demo_organization_add_email_post_render, }); From 7fc9fc32d13bb39d3f4a778e4b1d5b9710977bbd Mon Sep 17 00:00:00 2001 From: PieterCK Date: Mon, 4 Nov 2024 13:15:15 +0700 Subject: [PATCH 221/276] avatars: Split email-based and user ID-based avatar endpoints. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate `avatars//medium?` endpoints into distinct endpoints for email-based and user ID-based access. This change aligns avatar endpoints with Zulip’s existing API path conventions (e.g., the `users/` endpoint). --- zerver/tests/test_upload.py | 14 +++++++ zerver/views/users.py | 75 ++++++++++++++++++++++++------------- zproject/urls.py | 20 ++++++++-- 3 files changed, 79 insertions(+), 30 deletions(-) diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index ae216361a1..c1981ea875 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -1275,6 +1275,14 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase): "Not logged in: API authentication or user session required", status_code=401, ) + # Disallow access by id for spectators with unauthenticated access + # when realm public streams is false. + response = self.client_get(f"/avatar/{cordelia.id}", {"foo": "bar"}) + self.assert_json_error( + response, + "Not logged in: API authentication or user session required", + status_code=401, + ) # Allow unauthenticated/spectator requests by ID. response = self.client_get(f"/avatar/{cordelia.id}", {"foo": "bar"}) @@ -1301,6 +1309,12 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase): redirect_url = response["Location"] self.assertTrue(redirect_url.endswith("images/unknown-user-avatar.png?foo=bar")) + invalid_user_id = 999 + response = self.client_get(f"/avatar/{invalid_user_id}", {"foo": "bar"}) + self.assertEqual(302, response.status_code) + redirect_url = response["Location"] + self.assertTrue(redirect_url.endswith("images/unknown-user-avatar.png?foo=bar")) + def test_get_user_avatar_medium(self) -> None: hamlet = self.example_user("hamlet") self.login_user(hamlet) diff --git a/zerver/views/users.py b/zerver/views/users.py index 6a45fefef4..b0082acbd4 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -336,18 +336,13 @@ def update_user_backend( return json_success(request) -def avatar( +def avatar_by_id( request: HttpRequest, maybe_user_profile: UserProfile | AnonymousUser, - email_or_id: str, + user_id: int, medium: bool = False, ) -> HttpResponse: - """Accepts an email address or user ID and returns the avatar""" - is_email = False - try: - int(email_or_id) - except ValueError: - is_email = True + """Accepts a user ID and returns the avatar""" if not maybe_user_profile.is_authenticated: # Allow anonymous access to avatars only if spectators are @@ -356,27 +351,14 @@ def avatar( if not realm.allow_web_public_streams_access(): raise MissingAuthenticationError - # We only allow the ID format for accessing a user's avatar - # for spectators. This is mainly for defense in depth, since - # email_address_visibility should mean spectators only - # interact with fake email addresses anyway. - if is_email: - raise MissingAuthenticationError - if settings.RATE_LIMITING: - unique_avatar_key = f"{realm.id}/{email_or_id}/{medium}" + unique_avatar_key = f"{realm.id}/{user_id}/{medium}" rate_limit_spectator_attachment_access_by_file(unique_avatar_key) else: realm = maybe_user_profile.realm try: - if is_email: - avatar_user_profile = get_user_including_cross_realm(email_or_id, realm) - else: - avatar_user_profile = get_user_by_id_in_realm_including_cross_realm( - int(email_or_id), realm - ) - + avatar_user_profile = get_user_by_id_in_realm_including_cross_realm(user_id, realm) url: str | None = None if maybe_user_profile.is_authenticated and not check_can_access_user( avatar_user_profile, maybe_user_profile @@ -386,9 +368,43 @@ def avatar( # If there is a valid user account passed in, use its avatar url = avatar_url(avatar_user_profile, medium=medium) assert url is not None + except UserProfile.DoesNotExist: + url = get_avatar_for_inaccessible_user() + + assert url is not None + if request.META["QUERY_STRING"]: + url = append_url_query_string(url, request.META["QUERY_STRING"]) + return redirect(url) + + +def avatar_by_email( + request: HttpRequest, + maybe_user_profile: UserProfile | AnonymousUser, + email: str, + medium: bool = False, +) -> HttpResponse: + """Accepts an email address and returns the avatar""" + + if not maybe_user_profile.is_authenticated: + # We only allow the ID format for accessing a user's avatar + # for spectators. This is mainly for defense in depth, since + # email_address_visibility should mean spectators only + # interact with fake email addresses anyway. + raise MissingAuthenticationError + + realm = maybe_user_profile.realm + + try: + avatar_user_profile = get_user_including_cross_realm(email, realm) + url: str | None = None + if not check_can_access_user(avatar_user_profile, maybe_user_profile): + url = get_avatar_for_inaccessible_user() + else: + # If there is a valid user account passed in, use its avatar + url = avatar_url(avatar_user_profile, medium=medium) + assert url is not None except UserProfile.DoesNotExist: # If there is no such user, treat it as a new gravatar - email = email_or_id avatar_version = 1 url = get_gravatar_url(email, avatar_version, medium) @@ -399,9 +415,16 @@ def avatar( def avatar_medium( - request: HttpRequest, maybe_user_profile: UserProfile | AnonymousUser, email_or_id: str + request: HttpRequest, + maybe_user_profile: UserProfile | AnonymousUser, + email: str | None = None, + user_id: int | None = None, ) -> HttpResponse: - return avatar(request, maybe_user_profile, email_or_id, medium=True) + if email: + return avatar_by_email(request, maybe_user_profile, email, medium=True) + else: + assert user_id is not None + return avatar_by_id(request, maybe_user_profile, user_id, medium=True) def get_stream_name(stream: Stream | None) -> str | None: diff --git a/zproject/urls.py b/zproject/urls.py index 0703e9f104..e68e696a77 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -218,7 +218,8 @@ from zerver.views.user_settings import ( from zerver.views.user_topics import update_muted_topic, update_user_topic from zerver.views.users import ( add_bot_backend, - avatar, + avatar_by_email, + avatar_by_id, avatar_medium, create_user_backend, deactivate_bot_backend, @@ -680,11 +681,22 @@ urls += [ # Avatars have the same constraint because their URLs are included # in API data structures used by both the mobile and web clients. rest_path( - "avatar/", - GET=(avatar, {"override_api_url_scheme", "allow_anonymous_user_web"}), + "avatar/", + GET=(avatar_by_id, {"override_api_url_scheme", "allow_anonymous_user_web"}), ), rest_path( - "avatar//medium", + "avatar/", + GET=(avatar_by_email, {"override_api_url_scheme", "allow_anonymous_user_web"}), + ), + rest_path( + "avatar//medium", + GET=( + avatar_medium, + {"override_api_url_scheme", "allow_anonymous_user_web"}, + ), + ), + rest_path( + "avatar//medium", GET=( avatar_medium, {"override_api_url_scheme", "allow_anonymous_user_web"}, From 62b24c6d92568e9d9583e313508f87145558edbb Mon Sep 17 00:00:00 2001 From: Sayam Samal Date: Tue, 5 Nov 2024 19:30:16 +0530 Subject: [PATCH 222/276] user_card_popover: Fix unknown user_id in unsaved message user mentions. Previously, the event handler was targeting the `event.target` element instead of the `event.currentTarget` element. Due to event bubbling, this lead to incorrect element being used to extract the `data-user-id`, resulting in an error whenever a user mention was clicked in the unsaved message in the message box or the drafts view. --- web/src/user_card_popover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/user_card_popover.js b/web/src/user_card_popover.js index b00ecfb5be..d05bab4398 100644 --- a/web/src/user_card_popover.js +++ b/web/src/user_card_popover.js @@ -458,7 +458,7 @@ function toggle_user_card_popover_for_message( export function unsaved_message_user_mention_event_handler(e) { e.stopPropagation(); - const id_string = $(e.target).attr("data-user-id"); + const id_string = $(e.currentTarget).attr("data-user-id"); // Do not open popover for @all mention if (id_string === "*") { return; From 77064a17260ce888de73a9b7c01e33550e4d05be Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Tue, 5 Nov 2024 09:37:59 -0800 Subject: [PATCH 223/276] api_docs: Document new message-link Markdown syntax. --- api_docs/changelog.md | 6 ++++++ api_docs/message-formatting.md | 36 ++++++++++++++++++++++++++++++++++ version.py | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index d09f7c47c7..7c72e426f5 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 319** + +* [Markdown message + formatting](/api/message-formatting#links-to-channels-topics-and-messages): Added + new `message-link` format for special direct links to messages. + **Feature level 318** * [`POST /register`](/api/register-queue): Updated diff --git a/api_docs/message-formatting.md b/api_docs/message-formatting.md index 2f0cbad79e..bb5290435e 100644 --- a/api_docs/message-formatting.md +++ b/api_docs/message-formatting.md @@ -23,6 +23,42 @@ for syntax highlighting. This field is used in the mentions][help-global-time] to supported Markdown message formatting features. +## Links to channels, topics, and messages + +Zulip's markup supports special readable Markdown syntax for [linking +to channels, topics, and messages](/help/link-to-a-message-or-conversation). + +Sample HTML formats are as follows: +``` html + + + #announce + + + + + #announce > Zulip updates + + + + + #announce > Zulip updates @ 💬 + +``` + +The older stream/topic elements include a `data-stream-id`, which +historically was used in order to display the current channel name if +the channel had been renamed. That field is **deprecated**, because +displaying an updated value for the most common forms of this syntax +requires parsing the URL to get the topic to use anyway. + +**Changes**: In Zulip 10.0 (feature level 319), added Markdown syntax +for linking to a specific message in a conversation. Declared the +`data-stream-id` field to be deprecated as detailed above. + ## Image previews When a Zulip message is sent linking to an uploaded image, Zulip will diff --git a/version.py b/version.py index 7825a2ee4b..185898b379 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 318 # Last bumped for `WebhookConfigOption` configuration update in realm_incoming_webhook_bots. +API_FEATURE_LEVEL = 319 # Last bumped for message-link class # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump From c39ae45d954a982cbc7fa1a216a048877d18c419 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Thu, 24 Oct 2024 14:30:13 -0700 Subject: [PATCH 224/276] user_circles: Use background color instead of transparency. This sets us up to layer the status bubbles over avatars. --- web/styles/user_circles.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/styles/user_circles.css b/web/styles/user_circles.css index aaa20d9590..74f81fb4ab 100644 --- a/web/styles/user_circles.css +++ b/web/styles/user_circles.css @@ -15,13 +15,13 @@ border-color: var(--color-user-circle-idle); background: linear-gradient( to bottom, - hsl(0deg 0% 100% / 0%) 50%, + var(--color-background) 50%, var(--color-user-circle-idle) 50% ); } .user_circle_empty { - background-color: transparent; + background-color: var(--color-background); border-color: hsl(0deg 0% 50%); } From 774f230074207f4acc3bc8319231259aafe6a6b8 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Fri, 25 Oct 2024 12:29:14 -0700 Subject: [PATCH 225/276] people: Cache gravatar url on person object. --- web/src/people.ts | 21 ++++++++++---------- web/tests/people.test.js | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/web/src/people.ts b/web/src/people.ts index 3ee4e65ec2..f388c13852 100644 --- a/web/src/people.ts +++ b/web/src/people.ts @@ -809,27 +809,28 @@ export function small_avatar_url_for_person(person: User): string { } if (person.avatar_url === null) { - return gravatar_url_for_email(person.email); + person.avatar_url = gravatar_url_for_email(person.email); + return person.avatar_url; } return `/avatar/${person.user_id}`; } -function medium_gravatar_url_for_email(email: string): string { - const hash = md5(email.toLowerCase()); - const avatar_url = "https://secure.gravatar.com/avatar/" + hash + "?d=identicon"; - const url = new URL(avatar_url, window.location.origin); - url.search += (url.search ? "&" : "") + "s=500"; - return url.href; -} - export function medium_avatar_url_for_person(person: User): string { /* Unlike the small avatar URL case, we don't generally have a * medium avatar URL included in person objects. So only have the * gravatar and server endpoints here. */ if (person.avatar_url === null) { - return medium_gravatar_url_for_email(person.email); + person.avatar_url = gravatar_url_for_email(person.email); + } + + if (person.avatar_url !== undefined) { + const url = new URL(person.avatar_url, window.location.origin); + if (url.origin === "https://secure.gravatar.com") { + url.search += (url.search ? "&" : "") + "s=500"; + return url.href; + } } // We need to attach a version to the URL as a cache-breaker so that the browser diff --git a/web/tests/people.test.js b/web/tests/people.test.js index e7e5d69bbb..1c9c3e669c 100644 --- a/web/tests/people.test.js +++ b/web/tests/people.test.js @@ -214,6 +214,22 @@ const maria = { avatar_url: null, }; +const cedar = { + email: "Cedar@example.com", + user_id: 305, + full_name: "Cedar Athens", + // With client_gravatar enabled, requests that client compute gravatar + avatar_url: null, +}; + +const leo = { + email: "Leo@example.com", + user_id: 306, + full_name: "Leo Athens", + // With client_gravatar enabled, requests that client compute gravatar + avatar_url: null, +}; + const ashton = { email: "ashton@example.com", user_id: 303, @@ -889,6 +905,8 @@ test_people("concat_direct_message_group", () => { test_people("message_methods", () => { people.add_active_user(charles); people.add_active_user(maria); + people.add_active_user(cedar); + people.add_active_user(leo); people.add_active_user(ashton); // We don't rely on Maria to have all flags set explicitly-- @@ -899,10 +917,24 @@ test_people("message_methods", () => { people.small_avatar_url_for_person(maria), "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", ); + assert.equal( + maria.avatar_url, + "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", + ); + // This will use the cached gravatar url assert.equal( people.medium_avatar_url_for_person(maria), "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon&s=500", ); + // This will create a new gravatar url + assert.equal( + people.medium_avatar_url_for_person(cedar), + "https://secure.gravatar.com/avatar/2e6ed9fc1de54b7b5bc98ced46fe7a14?d=identicon&s=500", + ); + assert.equal( + cedar.avatar_url, + "https://secure.gravatar.com/avatar/2e6ed9fc1de54b7b5bc98ced46fe7a14?d=identicon", + ); assert.equal(people.medium_avatar_url_for_person(charles), "/avatar/301/medium?version=0"); assert.equal(people.medium_avatar_url_for_person(ashton), "/avatar/303/medium?version=0"); @@ -952,6 +984,16 @@ test_people("message_methods", () => { "https://secure.gravatar.com/avatar/6dbdd7946b58d8b11351fcb27e5cdd55?d=identicon", ); + // No gravatar url cached yet + message = { + avatar_url: undefined, + sender_id: leo.user_id, + }; + assert.equal( + people.small_avatar_url(message), + "https://secure.gravatar.com/avatar/ce9581fbf1beefbad43a4233aa65954c?d=identicon", + ); + blueslip.expect("error", "Unknown user_id in maybe_get_user_by_id"); message = { avatar_url: undefined, From 94368dcf2b1d94d7adff50618ac7e66299350c4d Mon Sep 17 00:00:00 2001 From: evykassirer Date: Wed, 23 Oct 2024 23:10:56 -0700 Subject: [PATCH 226/276] buddy_list: Add option to view avatar in buddy list. Fixes #19999. This change adds an option to show users' avatar and status in the buddy list. Previous options are still available in the settings menu. --- web/src/admin.js | 2 + web/src/buddy_data.ts | 2 + web/src/settings.js | 3 ++ web/src/settings_config.ts | 9 ++--- web/styles/app_variables.css | 3 ++ web/styles/right_sidebar.css | 40 +++++++++++++++++++ web/styles/user_circles.css | 4 ++ web/styles/zulip.css | 15 ++++++- web/templates/presence_row.hbs | 16 +++++++- .../settings/preferences_information.hbs | 14 +++++++ web/tests/activity.test.js | 1 + web/tests/buddy_data.test.js | 3 ++ 12 files changed, 103 insertions(+), 9 deletions(-) diff --git a/web/src/admin.js b/web/src/admin.js index 6eb939a6e6..3f74031b5c 100644 --- a/web/src/admin.js +++ b/web/src/admin.js @@ -8,6 +8,7 @@ import * as bot_data from "./bot_data"; import * as demo_organizations_ui from "./demo_organizations_ui"; import {$t, get_language_name, language_list} from "./i18n"; import {page_params} from "./page_params"; +import * as people from "./people"; import {realm_user_settings_defaults} from "./realm_user_settings_defaults"; import * as settings from "./settings"; import * as settings_bots from "./settings_bots"; @@ -109,6 +110,7 @@ export function build_page() { const options = { custom_profile_field_types: realm.custom_profile_field_types, full_name: current_user.full_name, + profile_picture: people.small_avatar_url_for_person(current_user), realm_name: realm.realm_name, realm_org_type: realm.realm_org_type, realm_available_video_chat_providers: realm.realm_available_video_chat_providers, diff --git a/web/src/buddy_data.ts b/web/src/buddy_data.ts index e356d7c473..8163c9e741 100644 --- a/web/src/buddy_data.ts +++ b/web/src/buddy_data.ts @@ -172,6 +172,7 @@ export type BuddyUserInfo = { href: string; name: string; user_id: number; + profile_picture: string; status_emoji_info: user_status.UserStatusEmojiInfo | undefined; is_current_user: boolean; num_unread: number; @@ -204,6 +205,7 @@ export function info_for(user_id: number): BuddyUserInfo { name: person.full_name, user_id, status_emoji_info, + profile_picture: people.small_avatar_url_for_person(person), is_current_user: people.is_my_user_id(user_id), num_unread: get_num_unread(user_id), user_circle_class, diff --git a/web/src/settings.js b/web/src/settings.js index e45c4cc16e..de0275eee2 100644 --- a/web/src/settings.js +++ b/web/src/settings.js @@ -93,6 +93,9 @@ export function build_page() { const rendered_settings_tab = render_settings_tab({ full_name: people.my_full_name(), + profile_picture: people.small_avatar_url_for_person( + people.get_by_user_id(people.my_current_user_id()), + ), date_joined_text: get_parsed_date_of_joining(), current_user, page_params, diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts index 609f7dbae7..ea89c8bb5b 100644 --- a/web/src/settings_config.ts +++ b/web/src/settings_config.ts @@ -77,11 +77,10 @@ export const user_list_style_values = { code: 2, description: $t({defaultMessage: "Show status text"}), }, - // The `with_avatar` design in still in discussion. - // with_avatar: { - // code: 3, - // description: $t({defaultMessage: "Show status text and avatar"}), - // }, + with_avatar: { + code: 3, + description: $t({defaultMessage: "Show avatar"}), + }, }; export const web_animate_image_previews_values = { diff --git a/web/styles/app_variables.css b/web/styles/app_variables.css index 51f99ad541..b3b5a36d1e 100644 --- a/web/styles/app_variables.css +++ b/web/styles/app_variables.css @@ -276,6 +276,7 @@ the smaller line-height. */ --line-height-sidebar-row-prominent: 1.7142em; /* 24px / 14px em */ --line-height-sidebar-row: 1.5714em; /* 22px / 14px em */ + --line-height-sidebar-row-with-avatars: 1.2857em; /* 18px / 14px em */ /* Right sidebar */ --right-sidebar-padding-right: 8px; @@ -283,6 +284,8 @@ space created by the padding-left on .right-sidebar, which separates the right sidebar from the message area. */ --right-sidebar-padding-left: 2px; + --right-sidebar-avatar-width: 2em; + --right-sidebar-avatar-height: var(--right-sidebar-avatar-width); /* Tippy popover related values */ --navbar-popover-menu-min-width: 230px; diff --git a/web/styles/right_sidebar.css b/web/styles/right_sidebar.css index f8e6e03998..b0762dc092 100644 --- a/web/styles/right_sidebar.css +++ b/web/styles/right_sidebar.css @@ -143,6 +143,10 @@ $user_status_emoji_width: 24px; &.highlighted_user { background-color: var(--color-buddy-list-highlighted-user); box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover); + + .user_circle { + outline: 1px solid var(--color-buddy-list-highlighted-user); + } } } @@ -161,6 +165,15 @@ $user_status_emoji_width: 24px; } } + .user_sidebar_entry.with_avatar .user_circle { + display: inline-block; + position: absolute; + width: 0.4em; + height: 0.4em; + top: 1.7em; + left: 1.7em; + } + .empty-list-message { font-style: italic; color: var(--color-text-empty-list-message); @@ -241,6 +254,13 @@ $user_status_emoji_width: 24px; align-items: baseline; } +.user_sidebar_entry.with_avatar .selectable_sidebar_block { + grid-template: + "row-content markers-and-controls" var(--line-height-sidebar-row-with-avatars) + "row-content ." auto / minmax(0, 1fr) + minmax(0, auto); +} + .user-presence-link { grid-area: row-content; @@ -251,6 +271,26 @@ $user_status_emoji_width: 24px; } } +.information-settings .profile-with-avatar, +.user_sidebar_entry.with_avatar .user-presence-link { + line-height: var(--line-height-sidebar-row-with-avatars); + display: grid; + grid-template: "avatar row-content" var(--right-sidebar-avatar-width) / auto minmax( + 0, + 1fr + ); + justify-content: flex-start; + align-items: center; +} + +.information-settings .profile-with-avatar { + margin: 5px 0; +} + +.user_sidebar_entry.with_avatar .user-presence-link { + margin: 3px; +} + .my_user_status { opacity: 0.5; white-space: nowrap; diff --git a/web/styles/user_circles.css b/web/styles/user_circles.css index 74f81fb4ab..d06c10362b 100644 --- a/web/styles/user_circles.css +++ b/web/styles/user_circles.css @@ -6,6 +6,10 @@ border: 1px solid; } +.user_sidebar_entry.with_avatar .user_circle { + outline: 1px solid var(--color-background); +} + .user_circle_green { background-color: var(--color-user-circle-active); border-color: var(--color-user-circle-active); diff --git a/web/styles/zulip.css b/web/styles/zulip.css index 7b9886e5b9..bd51c98dc7 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -999,10 +999,10 @@ div.focused-message-list { background-color: transparent; } +.information-settings .user-profile-picture, +.user_sidebar_entry.with_avatar .user-profile-picture, .inline_profile_picture { display: inline-block; - width: var(--message-box-avatar-width); - height: var(--message-box-avatar-height); /* Don't inherit the line-height from message-avatar; this preserves the dimensions and rounded corners on the image itself. */ @@ -1017,6 +1017,17 @@ div.focused-message-list { } } +.inline_profile_picture { + width: var(--message-box-avatar-width); + height: var(--message-box-avatar-height); +} + +.information-settings .user-profile-picture, +.user_sidebar_entry.with_avatar .user-profile-picture { + width: var(--right-sidebar-avatar-width); + height: var(--right-sidebar-avatar-height); +} + .home-error-bar { margin-top: 5px; display: none; diff --git a/web/templates/presence_row.hbs b/web/templates/presence_row.hbs index 40c3b5abbd..b1cce8cdd4 100644 --- a/web/templates/presence_row.hbs +++ b/web/templates/presence_row.hbs @@ -1,6 +1,6 @@ -
                            • +
                            • - + {{#unless user_list_style.WITH_AVATAR}}{{/unless}} {{status_text}}
                              + {{else if user_list_style.WITH_AVATAR}} + +
                              +
                              + {{> user_full_name}} + {{> status_emoji status_emoji_info}} +
                              + {{status_text}} +
                              {{else}}
                              {{> user_full_name}} diff --git a/web/templates/settings/preferences_information.hbs b/web/templates/settings/preferences_information.hbs index 8cbefa54e2..cf1b2d6a29 100644 --- a/web/templates/settings/preferences_information.hbs +++ b/web/templates/settings/preferences_information.hbs @@ -29,6 +29,20 @@ {{t "Working remotely" }}
                              {{/if}} + {{#if (eq this.code 3)}} +
                              + +
                              +
                              + {{../full_name}} + {{> ../status_emoji emoji_code="1f3e0"}} +
                              + {{t "Working remotely" }} +
                              +
                              + {{/if}} {{/each}} diff --git a/web/tests/activity.test.js b/web/tests/activity.test.js index ce92e36413..7bc01d38b6 100644 --- a/web/tests/activity.test.js +++ b/web/tests/activity.test.js @@ -501,6 +501,7 @@ test("insert_one_user_into_empty_list", ({override, mock_template}) => { user_id: 1, is_current_user: false, num_unread: 0, + profile_picture: "/avatar/1", user_circle_class: "user_circle_green", status_emoji_info: undefined, status_text: undefined, diff --git a/web/tests/buddy_data.test.js b/web/tests/buddy_data.test.js index 982911494c..99a847f24e 100644 --- a/web/tests/buddy_data.test.js +++ b/web/tests/buddy_data.test.js @@ -604,6 +604,7 @@ test("get_items_for_users", ({override}) => { is_current_user: true, name: "Human Myself", num_unread: 0, + profile_picture: "/avatar/1001", status_emoji_info, status_text: undefined, user_circle_class: "user_circle_green", @@ -616,6 +617,7 @@ test("get_items_for_users", ({override}) => { is_current_user: false, name: "Alice Smith", num_unread: 0, + profile_picture: "/avatar/1002", status_emoji_info, status_text: undefined, user_circle_class: "user_circle_empty", @@ -628,6 +630,7 @@ test("get_items_for_users", ({override}) => { is_current_user: false, name: "Fred Flintstone", num_unread: 0, + profile_picture: "/avatar/1003", status_emoji_info, status_text: undefined, user_circle_class: "user_circle_empty", From 08e7c5feea5329550529673989bd1aabc32ba641 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Fri, 25 Oct 2024 12:50:03 -0700 Subject: [PATCH 227/276] buddy_list: Refactor click handlers to not rely on placement of user-presence-link. --- web/src/click_handlers.js | 25 ++++++++++++------------- web/src/user_card_popover.js | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/web/src/click_handlers.js b/web/src/click_handlers.js index 3530238a7c..75fe846133 100644 --- a/web/src/click_handlers.js +++ b/web/src/click_handlers.js @@ -515,8 +515,9 @@ export function initialize() { // BUDDY LIST TOOLTIPS (not displayed on touch devices) $(".buddy-list-section").on("mouseenter", ".selectable_sidebar_block", (e) => { e.stopPropagation(); - const $elem = $(e.currentTarget).closest(".user_sidebar_entry").find(".user-presence-link"); - const user_id_string = $elem.attr("data-user-id"); + const user_id_string = $(e.currentTarget) + .closest(".user_sidebar_entry") + .attr("data-user-id"); const title_data = buddy_data.get_title_data(user_id_string, false); // `target_node` is the `ul` element since it stays in DOM even after updates. @@ -531,28 +532,26 @@ export function initialize() { ); } - do_render_buddy_list_tooltip( - $elem.parent(), - title_data, - get_target_node, - check_reference_removed, - ); + const $elem = $(e.currentTarget) + .closest(".user_sidebar_entry") + .find(".selectable_sidebar_block"); + do_render_buddy_list_tooltip($elem, title_data, get_target_node, check_reference_removed); /* The following implements a little tooltip giving the name for status emoji when hovering them in the right sidebar. This requires special logic, to avoid conflicting with the main tooltip or showing duplicate tooltips. */ - $(".user-presence-link .status-emoji-name").off("mouseenter").off("mouseleave"); - $(".user-presence-link .status-emoji-name").on("mouseenter", () => { - const instance = $elem.parent()[0]._tippy; + $(".user_sidebar_entry .status-emoji-name").off("mouseenter").off("mouseleave"); + $(".user_sidebar_entry .status-emoji-name").on("mouseenter", () => { + const instance = $elem[0]._tippy; if (instance && instance.state.isVisible) { instance.destroy(); } }); - $(".user-presence-link .status-emoji-name").on("mouseleave", () => { + $(".user_sidebar_entry .status-emoji-name").on("mouseleave", () => { do_render_buddy_list_tooltip( - $elem.parent(), + $elem, title_data, get_target_node, check_reference_removed, diff --git a/web/src/user_card_popover.js b/web/src/user_card_popover.js index d05bab4398..270d8f68c5 100644 --- a/web/src/user_card_popover.js +++ b/web/src/user_card_popover.js @@ -526,7 +526,7 @@ function get_user_card_popover_for_message_items() { // Functions related to the user card popover in the user sidebar. function toggle_sidebar_user_card_popover($target) { - const user_id = elem_to_user_id($target.find("a")); + const user_id = elem_to_user_id($target); const user = people.get_by_user_id(user_id); // Hiding popovers may mutate current_user_sidebar_user_id. From ce7c3705f70ff93d55790e202fe9743183d0cd12 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Thu, 24 Oct 2024 12:13:44 -0700 Subject: [PATCH 228/276] buddy_list: Don't go to DM narrow on avatar click. This commit also moves styles from user-presence-link onto selectable_sidebar_block, since the link is no longer wrapping the whole block contents. The `data-name` was also moved to the top level `
                            • ` because it's used in pupeteer tests. --- web/e2e-tests/message-basics.test.ts | 8 +++---- web/src/click_handlers.js | 3 +++ web/styles/right_sidebar.css | 7 ++---- web/templates/presence_row.hbs | 32 +++++++++++++++------------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/web/e2e-tests/message-basics.test.ts b/web/e2e-tests/message-basics.test.ts index eceed790ba..0102867957 100644 --- a/web/e2e-tests/message-basics.test.ts +++ b/web/e2e-tests/message-basics.test.ts @@ -404,21 +404,21 @@ async function test_stream_search_filters_stream_list(page: Page): Promise async function test_users_search(page: Page): Promise { console.log("Search users using right sidebar"); async function assert_in_list(page: Page, name: string): Promise { - await page.waitForSelector(`#buddy-list-other-users li [data-name="${CSS.escape(name)}"]`, { + await page.waitForSelector(`#buddy-list-other-users li[data-name="${CSS.escape(name)}"]`, { visible: true, }); } async function assert_selected(page: Page, name: string): Promise { await page.waitForSelector( - `#buddy-list-other-users li.highlighted_user [data-name="${CSS.escape(name)}"]`, + `#buddy-list-other-users li.highlighted_user[data-name="${CSS.escape(name)}"]`, {visible: true}, ); } async function assert_not_selected(page: Page, name: string): Promise { await page.waitForSelector( - `#buddy-list-other-users li.highlighted_user [data-name="${CSS.escape(name)}"]`, + `#buddy-list-other-users li.highlighted_user[data-name="${CSS.escape(name)}"]`, {hidden: true}, ); } @@ -452,7 +452,7 @@ async function test_users_search(page: Page): Promise { await arrow(page, "Down"); // Now Iago must be highlighted - await page.waitForSelector('#buddy-list-other-users li.highlighted_user [data-name="Iago"]', { + await page.waitForSelector('#buddy-list-other-users li.highlighted_user[data-name="Iago"]', { visible: true, }); await assert_not_selected(page, "King Hamlet"); diff --git a/web/src/click_handlers.js b/web/src/click_handlers.js index 75fe846133..5ceb2472e5 100644 --- a/web/src/click_handlers.js +++ b/web/src/click_handlers.js @@ -440,6 +440,9 @@ export function initialize() { if (e.metaKey || e.ctrlKey || e.shiftKey) { return; } + if ($(e.target).parents(".user-profile-picture").length === 1) { + return; + } const $li = $(e.target).parents("li"); diff --git a/web/styles/right_sidebar.css b/web/styles/right_sidebar.css index b0762dc092..7ac8c507dc 100644 --- a/web/styles/right_sidebar.css +++ b/web/styles/right_sidebar.css @@ -255,6 +255,7 @@ $user_status_emoji_width: 24px; } .user_sidebar_entry.with_avatar .selectable_sidebar_block { + margin: 3px; grid-template: "row-content markers-and-controls" var(--line-height-sidebar-row-with-avatars) "row-content ." auto / minmax(0, 1fr) @@ -272,7 +273,7 @@ $user_status_emoji_width: 24px; } .information-settings .profile-with-avatar, -.user_sidebar_entry.with_avatar .user-presence-link { +.user_sidebar_entry.with_avatar .selectable_sidebar_block { line-height: var(--line-height-sidebar-row-with-avatars); display: grid; grid-template: "avatar row-content" var(--right-sidebar-avatar-width) / auto minmax( @@ -287,10 +288,6 @@ $user_status_emoji_width: 24px; margin: 5px 0; } -.user_sidebar_entry.with_avatar .user-presence-link { - margin: 3px; -} - .my_user_status { opacity: 0.5; white-space: nowrap; diff --git a/web/templates/presence_row.hbs b/web/templates/presence_row.hbs index b1cce8cdd4..abb5f893df 100644 --- a/web/templates/presence_row.hbs +++ b/web/templates/presence_row.hbs @@ -1,11 +1,8 @@ -
                            • +
                            • From 99856327f03c525b878cb453f27a4e96ed2a415d Mon Sep 17 00:00:00 2001 From: evykassirer Date: Thu, 24 Oct 2024 13:15:46 -0700 Subject: [PATCH 229/276] buddy_list: Clicking avatar opens menu instead of using ... icon. --- web/src/user_card_popover.js | 7 +++++++ web/styles/right_sidebar.css | 25 +++++++++++++++++-------- web/templates/presence_row.hbs | 2 ++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/web/src/user_card_popover.js b/web/src/user_card_popover.js index 270d8f68c5..01f6630a37 100644 --- a/web/src/user_card_popover.js +++ b/web/src/user_card_popover.js @@ -783,6 +783,13 @@ function register_click_handlers() { toggle_sidebar_user_card_popover($target); }); + $(".buddy-list-section").on("click", ".user-profile-picture", (e) => { + e.stopPropagation(); + const $target = $(e.currentTarget).closest("li"); + + toggle_sidebar_user_card_popover($target); + }); + $("body").on("click", ".sidebar-popover-mute-user", (e) => { const user_id = elem_to_user_id($(e.target).parents("ul")); hide_all_user_card_popovers(); diff --git a/web/styles/right_sidebar.css b/web/styles/right_sidebar.css index 7ac8c507dc..8e30af3435 100644 --- a/web/styles/right_sidebar.css +++ b/web/styles/right_sidebar.css @@ -170,8 +170,8 @@ $user_status_emoji_width: 24px; position: absolute; width: 0.4em; height: 0.4em; - top: 1.7em; - left: 1.7em; + top: 1.6em; + left: 1.6em; } .empty-list-message { @@ -254,12 +254,21 @@ $user_status_emoji_width: 24px; align-items: baseline; } -.user_sidebar_entry.with_avatar .selectable_sidebar_block { - margin: 3px; - grid-template: - "row-content markers-and-controls" var(--line-height-sidebar-row-with-avatars) - "row-content ." auto / minmax(0, 1fr) - minmax(0, auto); +.user_sidebar_entry.with_avatar { + grid-template: "row-content" var(--line-height-sidebar-row-with-avatars) "row-content" auto / minmax( + 0, + 1fr + ); + + .selectable_sidebar_block { + margin: 2px; + grid-template: + "row-content markers-and-controls" var( + --line-height-sidebar-row-with-avatars + ) + "row-content ." auto / minmax(0, 1fr) + minmax(0, auto); + } } .user-presence-link { diff --git a/web/templates/presence_row.hbs b/web/templates/presence_row.hbs index abb5f893df..dea8aec996 100644 --- a/web/templates/presence_row.hbs +++ b/web/templates/presence_row.hbs @@ -36,5 +36,7 @@ {{/if}} {{#if num_unread}}{{num_unread}}{{/if}}
                            • + {{#unless user_list_style.WITH_AVATAR}} + {{/unless}} From 2b636e64ecadccb76cd02957891236b8b04fa77a Mon Sep 17 00:00:00 2001 From: evykassirer Date: Thu, 31 Oct 2024 16:20:46 -0700 Subject: [PATCH 230/276] buddy_list: Make avatar display option only visible in dev. --- web/src/settings_config.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts index ea89c8bb5b..77bac3e0bf 100644 --- a/web/src/settings_config.ts +++ b/web/src/settings_config.ts @@ -68,7 +68,20 @@ export const web_channel_default_view_values = { }, }; -export const user_list_style_values = { +export const user_list_style_values: { + compact: { + code: number; + description: string; + }; + with_status: { + code: number; + description: string; + }; + with_avatar?: { + code: number; + description: string; + }; +} = { compact: { code: 1, description: $t({defaultMessage: "Compact"}), @@ -77,11 +90,14 @@ export const user_list_style_values = { code: 2, description: $t({defaultMessage: "Show status text"}), }, - with_avatar: { +}; + +if (page_params.development_environment) { + user_list_style_values.with_avatar = { code: 3, description: $t({defaultMessage: "Show avatar"}), - }, -}; + }; +} export const web_animate_image_previews_values = { always: { From d63ae115511ce89e929887543337d8f70a047a48 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Sat, 2 Nov 2024 08:45:32 +0530 Subject: [PATCH 231/276] settings: Make separate section for organization group settings. Fixes #32214. --- .../organization_permissions_admin.hbs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/web/templates/settings/organization_permissions_admin.hbs b/web/templates/settings/organization_permissions_admin.hbs index 6da7fc12f1..8cc312528e 100644 --- a/web/templates/settings/organization_permissions_admin.hbs +++ b/web/templates/settings/organization_permissions_admin.hbs @@ -106,6 +106,24 @@
                              +
                              +
                              +

                              {{t "Group permissions" }} + {{> ../help_link_widget link="/help/manage-user-groups" }} +

                              + {{> settings_save_discard_widget section_name="group-permissions" }} +
                              +
                              + {{> group_setting_value_pill_input + setting_name="realm_can_manage_all_groups" + label=(t 'Who can administer all user groups')}} + + {{> group_setting_value_pill_input + setting_name="realm_can_create_groups" + label=(t 'Who can create user groups')}} +
                              +
                              +

                              {{t "Direct message permissions" }} @@ -326,14 +344,6 @@

                              - {{> group_setting_value_pill_input - setting_name="realm_can_create_groups" - label=(t 'Who can create user groups')}} - - {{> group_setting_value_pill_input - setting_name="realm_can_manage_all_groups" - label=(t 'Who can administer all user groups')}} - {{> group_setting_value_pill_input setting_name="realm_can_add_custom_emoji_group" label=(t 'Who can add custom emoji')}} From 9b6cb5b9dc491e4e8dbc03140dbd0877ffba0a1c Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Sat, 2 Nov 2024 09:22:31 +0530 Subject: [PATCH 232/276] group-settings: Rename "Who can manage this group" setting label. This makes the naming consistent with the realm setting for managing all groups. --- web/templates/user_group_settings/group_permissions.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/user_group_settings/group_permissions.hbs b/web/templates/user_group_settings/group_permissions.hbs index bc61fdaa2f..a1167e510a 100644 --- a/web/templates/user_group_settings/group_permissions.hbs +++ b/web/templates/user_group_settings/group_permissions.hbs @@ -1,5 +1,5 @@
                              - +
                              From 9a50fae25ad4769ee72b6021d10f452f0c4220cc Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Sat, 2 Nov 2024 08:53:29 +0530 Subject: [PATCH 233/276] group-settings: Update right pannel for user without creation permission. This commit updates the right panel for groups to be same as we have it for stream settings when the user does not have permission to create groups. Fixes part of #32212. --- .../user_group_settings/user_group_settings_overlay.hbs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/templates/user_group_settings/user_group_settings_overlay.hbs b/web/templates/user_group_settings/user_group_settings_overlay.hbs index cdc1409d6e..42f56e1f95 100644 --- a/web/templates/user_group_settings/user_group_settings_overlay.hbs +++ b/web/templates/user_group_settings/user_group_settings_overlay.hbs @@ -50,14 +50,18 @@
                              {{t 'User group settings' }}
                              + {{#if can_create_user_groups}} - {{#tr}} First time? Read our guidelines for creating user groups. {{#*inline "z-link"}}{{> @partial-block}}{{/inline}} {{/tr}} + {{else}} + + {{t 'You do not have permission to create user groups.' }} + {{/if}}
                              From 067e5a46c3d6d7960ae44d8f6420f903cdef17e5 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Sat, 2 Nov 2024 10:21:21 +0530 Subject: [PATCH 234/276] settings: Show stream and group info in a banner at the top. Instead of having information about streams and groups below the create button, we show that in a banner at the top irrespective of whether user has permission to create them or not. Fixes part of #32212. --- web/src/stream_settings_ui.js | 13 +++++++ web/src/user_group_edit.js | 14 +++++++ web/styles/subscriptions.css | 37 +++++++++++++++---- web/templates/modal_banner/modal_banner.hbs | 6 +++ .../modal_banner/stream_info_banner.hbs | 5 +++ .../modal_banner/user_group_info_banner.hbs | 5 +++ .../stream_settings_overlay.hbs | 22 +++++------ .../user_group_settings_overlay.hbs | 22 +++++------ 8 files changed, 90 insertions(+), 34 deletions(-) create mode 100644 web/templates/modal_banner/stream_info_banner.hbs create mode 100644 web/templates/modal_banner/user_group_info_banner.hbs diff --git a/web/src/stream_settings_ui.js b/web/src/stream_settings_ui.js index 1d4ff231ab..c2c6a25c48 100644 --- a/web/src/stream_settings_ui.js +++ b/web/src/stream_settings_ui.js @@ -2,6 +2,7 @@ import $ from "jquery"; import _ from "lodash"; 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_browse_streams_list from "../templates/stream_settings/browse_streams_list.hbs"; import render_browse_streams_list_item from "../templates/stream_settings/browse_streams_list_item.hbs"; import render_stream_settings from "../templates/stream_settings/stream_settings.hbs"; @@ -705,6 +706,18 @@ export function setup_page(callback) { throttled_redraw_left_panel(); }); + const context = { + banner_type: compose_banner.INFO, + classname: "stream_info", + hide_close_button: true, + button_text: $t({defaultMessage: "Learn more"}), + button_link: "/help/introduction-to-channels", + }; + + $("#channels_overlay_container .nothing-selected .stream-info-banner").html( + render_stream_info_banner(context), + ); + // When hitting Enter in the stream creation box, we open the // "create stream" UI with the stream name prepopulated. This // is only useful if the user has permission to create diff --git a/web/src/user_group_edit.js b/web/src/user_group_edit.js index cc7439e9b4..adcd642ab8 100644 --- a/web/src/user_group_edit.js +++ b/web/src/user_group_edit.js @@ -1,6 +1,7 @@ import $ from "jquery"; import render_confirm_delete_user from "../templates/confirm_dialog/confirm_delete_user.hbs"; +import render_group_info_banner from "../templates/modal_banner/user_group_info_banner.hbs"; import render_browse_user_groups_list_item from "../templates/user_group_settings/browse_user_groups_list_item.hbs"; import render_cannot_deactivate_group_banner from "../templates/user_group_settings/cannot_deactivate_group_banner.hbs"; import render_change_user_group_info_modal from "../templates/user_group_settings/change_user_group_info_modal.hbs"; @@ -11,6 +12,7 @@ import * as blueslip from "./blueslip"; import * as browser_history from "./browser_history"; import * as channel from "./channel"; import * as components from "./components"; +import * as compose_banner from "./compose_banner"; import * as confirm_dialog from "./confirm_dialog"; import * as dialog_widget from "./dialog_widget"; import * as hash_util from "./hash_util"; @@ -866,6 +868,18 @@ export function setup_page(callback) { ); $groups_overlay_container.html(groups_overlay_html); + const context = { + banner_type: compose_banner.INFO, + classname: "group_info", + hide_close_button: true, + button_text: $t({defaultMessage: "Learn more"}), + button_link: "/help/user-groups", + }; + + $("#groups_overlay_container .nothing-selected .group-info-banner").html( + render_group_info_banner(context), + ); + // Initially as the overlay is build with empty right panel, // active_group_id is undefined. user_group_components.reset_active_group_id(); diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 0757a6f5a7..cfa53783b5 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -365,9 +365,9 @@ h4.user_group_setting_subsection_title { .left .no-streams-to-show, .left .no-groups-to-show, - .right .nothing-selected { + .right .nothing-selected .create-stream-button-container, + .right .nothing-selected .create-group-button-container { display: block; - margin-top: calc(45vh - 75px); text-align: center; font-size: 1em; margin-left: 2em; @@ -378,6 +378,28 @@ h4.user_group_setting_subsection_title { } } + .left .no-streams-to-show, + .left .no-groups-to-show { + margin-top: calc(45vh - 75px); + } + + .right .nothing-selected { + padding: 5px 5px 0; + + .stream-info-banner a, + .group-info-banner a { + color: inherit; + } + + .create-stream-button-container { + margin-top: calc(45vh - 128px); + } + + .create-group-button-container { + margin-top: calc(45vh - 134px); + } + } + .left { border-right: 1px solid var(--color-border-modal-bar); @@ -398,12 +420,11 @@ h4.user_group_setting_subsection_title { .right { width: calc(50% + 1px); - .nothing-selected { - & button { - padding: 6px 10px 8px; - display: block; - margin: 0 auto 10px; - } + .nothing-selected .create_stream_button, + .nothing-selected .create_user_group_button { + padding: 6px 10px 8px; + display: block; + margin: 0 auto 10px; } .display-type { diff --git a/web/templates/modal_banner/modal_banner.hbs b/web/templates/modal_banner/modal_banner.hbs index 577d582573..e2fac4f432 100644 --- a/web/templates/modal_banner/modal_banner.hbs +++ b/web/templates/modal_banner/modal_banner.hbs @@ -6,8 +6,14 @@ {{/if}} {{#if button_text}} + {{#if button_link}} + + + + {{else}} {{/if}} + {{/if}}
                              {{#unless hide_close_button}} diff --git a/web/templates/modal_banner/stream_info_banner.hbs b/web/templates/modal_banner/stream_info_banner.hbs new file mode 100644 index 0000000000..a2b3739e1f --- /dev/null +++ b/web/templates/modal_banner/stream_info_banner.hbs @@ -0,0 +1,5 @@ +{{#> modal_banner }} + +{{/modal_banner}} diff --git a/web/templates/modal_banner/user_group_info_banner.hbs b/web/templates/modal_banner/user_group_info_banner.hbs new file mode 100644 index 0000000000..417adc794c --- /dev/null +++ b/web/templates/modal_banner/user_group_info_banner.hbs @@ -0,0 +1,5 @@ +{{#> modal_banner }} + +{{/modal_banner}} diff --git a/web/templates/stream_settings/stream_settings_overlay.hbs b/web/templates/stream_settings/stream_settings_overlay.hbs index 675929eb41..b6976fec79 100644 --- a/web/templates/stream_settings/stream_settings_overlay.hbs +++ b/web/templates/stream_settings/stream_settings_overlay.hbs @@ -63,19 +63,15 @@
                              {{t 'Channel settings' }}
                              - - {{#if can_create_streams}} - - {{#tr}} - First time? Read our guidelines for creating and naming channels. - {{#*inline "z-link"}}{{> @partial-block}}{{/inline}} - {{/tr}} - - {{else}} - - {{t 'You do not have permission to create channels in this organization.' }} - - {{/if}} +
                              +
                              + + {{#unless can_create_streams}} + + {{t 'You do not have permission to create channels in this organization.' }} + + {{/unless}} +
                              {{!-- edit stream here --}} diff --git a/web/templates/user_group_settings/user_group_settings_overlay.hbs b/web/templates/user_group_settings/user_group_settings_overlay.hbs index 42f56e1f95..6141c10923 100644 --- a/web/templates/user_group_settings/user_group_settings_overlay.hbs +++ b/web/templates/user_group_settings/user_group_settings_overlay.hbs @@ -50,19 +50,15 @@
                              {{t 'User group settings' }}
                              - - {{#if can_create_user_groups}} - - {{#tr}} - First time? Read our guidelines for creating user groups. - {{#*inline "z-link"}}{{> @partial-block}}{{/inline}} - {{/tr}} - - {{else}} - - {{t 'You do not have permission to create user groups.' }} - - {{/if}} +
                              +
                              + + {{#unless can_create_user_groups}} + + {{t 'You do not have permission to create user groups.' }} + + {{/unless}} +
                              {{!-- edit user group here --}} From c7ede831aaa22f104e12fc8f3de8a887274b1d3d Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Sat, 2 Nov 2024 14:43:55 +0530 Subject: [PATCH 235/276] stream_settings: Update channel creation permission text. This commit updates text shown in the right panel when user does not have permission to create streams. --- web/templates/stream_settings/stream_settings_overlay.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/stream_settings/stream_settings_overlay.hbs b/web/templates/stream_settings/stream_settings_overlay.hbs index b6976fec79..924a6e6668 100644 --- a/web/templates/stream_settings/stream_settings_overlay.hbs +++ b/web/templates/stream_settings/stream_settings_overlay.hbs @@ -68,7 +68,7 @@ {{#unless can_create_streams}} - {{t 'You do not have permission to create channels in this organization.' }} + {{t 'You do not have permission to create channels.' }} {{/unless}}
                              From 1984333454d1c13640e47e35796ef784fb79e492 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Tue, 5 Nov 2024 10:36:53 -0500 Subject: [PATCH 236/276] left_sidebar: Scope line-height adjustment to low-res screens. --- web/styles/left_sidebar.css | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index acc568228c..32e622ab9a 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -691,10 +691,6 @@ li.active-sub-filter { .left-sidebar-navigation-label-container { .left-sidebar-navigation-label { - /* Again, for the sake of low-resolution screens, - we'll let the actual label take 1 as a line-height - value, and allow grid to handle the alignment. */ - line-height: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -775,6 +771,13 @@ li.top_left_scheduled_messages { .left-sidebar-navigation-label { grid-area: row-content; padding-right: var(--left-sidebar-before-unread-count-padding); + + @media screen and (resolution <= 1x) { + /* For the sake of low-resolution screens, + we'll let the actual label take 1 as a line-height + value, and allow grid to handle the alignment. */ + line-height: 1; + } } .unread_count { From bcaea0a6af776bd206ba8907a54ff19e2b0cbe34 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Tue, 5 Nov 2024 10:49:00 -0500 Subject: [PATCH 237/276] left_sidebar: Better align low-attention unreads in nav area. --- web/styles/left_sidebar.css | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index 32e622ab9a..204c53e422 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -791,6 +791,30 @@ li.top_left_scheduled_messages { } } +/* Low-attention unreads have no bounding box, + so their counters should be aligned on the + same baseline as the navigation label. */ +.top_left_starred_messages, +.top_left_drafts, +.top_left_scheduled_messages { + .left-sidebar-navigation-label-container { + align-items: baseline; + } + + .left-sidebar-navigation-label { + @media screen and (resolution <= 1x) { + /* Owing to the baseline alignment in this + area, we don't need the low-res line-height + adjustment. */ + line-height: inherit; + } + } + + .filter-icon { + align-self: center; + } +} + .top_left_starred_messages { &.hide_starred_message_count { .masked_unread_count { From 331004f0259d07a09bb323c63298c634ae30e4fb Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Tue, 5 Nov 2024 12:00:33 -0500 Subject: [PATCH 238/276] popovers: Add em conversion and missing annotation. --- web/styles/popovers.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/styles/popovers.css b/web/styles/popovers.css index 63bb2670cf..9126c6e760 100644 --- a/web/styles/popovers.css +++ b/web/styles/popovers.css @@ -1186,12 +1186,16 @@ ul.popover-group-menu-member-list { flex-flow: row nowrap; align-items: flex-start; gap: 5px; + /* 3px at 15px/1em, 10px at 15px/1em */ padding: 0.2em 0.6666em; /* 15px at 15px/1em */ font-size: 1em; /* 16px at 15px/1em */ line-height: 1.0667em; - min-height: 26px; + /* 26px at 16px/1em - this height was carried forward + despite the information density change in 15px > 16px, + so we calculate its height to the 16px em in use. */ + min-height: 1.625em; .popover-menu-icon { /* 16px at 15px/1em */ From 96b62a5372279d59a038d7caee1b7f7a2a2c52cc Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Tue, 5 Nov 2024 14:09:12 -0500 Subject: [PATCH 239/276] left_sidebar: Correct unread alignment in condensed popover. --- web/styles/left_sidebar.css | 8 ----- web/styles/popovers.css | 32 +++++++++++++++++++ .../left_sidebar_condensed_views_popover.hbs | 12 ++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index 204c53e422..ceaa75c93e 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -1058,14 +1058,6 @@ li.top_left_scheduled_messages { } } -.condensed-views-popover-menu { - .unread_count { - margin: 1px 0 0 6px; - border-color: var(--color-border-unread-counter-popover-menu); - width: max-content; - } -} - .subscription_block { grid-template-columns: var(--left-sidebar-toggle-width-offset) var( diff --git a/web/styles/popovers.css b/web/styles/popovers.css index 9126c6e760..3fc646e95e 100644 --- a/web/styles/popovers.css +++ b/web/styles/popovers.css @@ -1476,3 +1476,35 @@ ul.popover-menu-list { border: 1px solid var(--color-border-popover-hotkey-hint); } } + +.condensed-views-popover-menu { + .popover-menu-link:has(.label-and-unread-wrapper) { + align-items: center; + + .popover-menu-icon { + margin-top: 1px; + } + + .label-and-unread-wrapper { + /* Occupy the maximum width of the + parent flex container. */ + flex: 1 0 max-content; + display: flex; + gap: 5px; + align-items: baseline; + } + + .popover-menu-label { + margin-top: 0; + } + + .unread_count { + margin: 0 0 0 6px; + border-color: var(--color-border-unread-counter-popover-menu); + width: max-content; + height: auto; + line-height: 1.2445em; + align-self: baseline; + } + } +} diff --git a/web/templates/popovers/left_sidebar/left_sidebar_condensed_views_popover.hbs b/web/templates/popovers/left_sidebar/left_sidebar_condensed_views_popover.hbs index 9fadfe7229..ded91280b4 100644 --- a/web/templates/popovers/left_sidebar/left_sidebar_condensed_views_popover.hbs +++ b/web/templates/popovers/left_sidebar/left_sidebar_condensed_views_popover.hbs @@ -9,16 +9,20 @@ {{#if has_scheduled_messages }} {{/if}} From 06a50eaa78a3c17cbf0965420962f29eae015257 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Fri, 1 Nov 2024 16:10:46 +0530 Subject: [PATCH 240/276] echo: Update cached message data for locally echoed messages. Since we only pass non echo messages to message_events.insert_new_messages, where message lists get updated, it is important to update locally echoed messages here. --- web/src/echo.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/echo.ts b/web/src/echo.ts index bcf4c0a2f4..695b1712dd 100644 --- a/web/src/echo.ts +++ b/web/src/echo.ts @@ -13,6 +13,7 @@ import * as echo_state from "./echo_state"; import * as local_message from "./local_message"; import * as markdown from "./markdown"; import * as message_events_util from "./message_events_util"; +import * as message_list_data_cache from "./message_list_data_cache"; import * as message_lists from "./message_lists"; import * as message_live_update from "./message_live_update"; import * as message_store from "./message_store"; @@ -560,6 +561,16 @@ export function process_from_server(messages: ServerMessage[]): ServerMessage[] msg_list.add_messages(msgs_to_rerender_or_add_to_narrow, {}); } } + + for (const msg_list_data of message_lists.non_rendered_data()) { + if (!msg_list_data.filter.can_apply_locally()) { + // Ideally we would ask server to if messages matches filter + // but it is not worth doing so for every new message. + message_list_data_cache.remove(msg_list_data.filter); + } else { + msg_list_data.add_messages(msgs_to_rerender_or_add_to_narrow); + } + } } return non_echo_messages; From 1f1165027568de4ffbb287a66b2cf6941b36a9c0 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Fri, 1 Nov 2024 16:55:24 +0530 Subject: [PATCH 241/276] filter: Fix search narrow not live updated on topic move. Since stream / topic change involves change in topic name, it could involve new messages being included and some being discarded. Also, messages could have been moved from an inaccessible stream to now accessible stream for the user. --- web/src/filter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/filter.ts b/web/src/filter.ts index bab337c349..8f145b1ae0 100644 --- a/web/src/filter.ts +++ b/web/src/filter.ts @@ -1772,6 +1772,7 @@ export class Filter { "not-in-home", "in-all", "not-in-all", + "search", ]); for (const term of term_types) { From 662f364283120df1441304488ad64be9957cbd9b Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Fri, 1 Nov 2024 23:30:45 +0530 Subject: [PATCH 242/276] fetch_status: Add function to copy current status of msg list data. This will be used to copy over status of superset data to the newly populated message list data. --- web/src/fetch_status.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/fetch_status.ts b/web/src/fetch_status.ts index f60f4b4c4e..4ae7298cc7 100644 --- a/web/src/fetch_status.ts +++ b/web/src/fetch_status.ts @@ -141,4 +141,15 @@ export class FetchStatus { max_id_for_messages(messages), ); } + + copy_status(fetch_status: FetchStatus): void { + this._found_newest = fetch_status.has_found_newest(); + this._found_oldest = fetch_status.has_found_oldest(); + this._expected_max_message_id = fetch_status._expected_max_message_id; + this._history_limited = fetch_status._history_limited; + // We don't want to copy over the loading state of the message list + // data since the same data object is not used for two messages lists + // and hence when the fetch is finished, only the original message list + // data will be updated. + } } From 517fabd123afb71efe038e819cdbf42959d232e5 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Fri, 1 Nov 2024 23:38:23 +0530 Subject: [PATCH 243/276] message_events: Extract function to check if filter has term type. Not added to `Filter` library since this function is a bit localized to message_events us case in the sense that it checks for existence of `not-` prefix of the term type too. --- web/src/message_events.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/src/message_events.js b/web/src/message_events.js index e8550acb7a..21c83ec49c 100644 --- a/web/src/message_events.js +++ b/web/src/message_events.js @@ -39,17 +39,21 @@ import * as unread from "./unread"; import * as unread_ui from "./unread_ui"; import * as util from "./util"; +function filter_has_term_type(filter, term_type) { + return ( + filter !== undefined && + (filter.sorted_term_types().includes(term_type) || + filter.sorted_term_types().includes(`not-${term_type}`)) + ); +} + export function update_current_view_for_topic_visibility() { // If we have rendered message list / cached data based on topic // visibility policy, we need to rerender it to reflect the changes. It // is easier to just load the narrow from scratch, instead of asking server // for relevant messages in the updated topic. const filter = message_lists.current?.data.filter; - if ( - filter !== undefined && - (filter.sorted_term_types().includes("is-followed") || - filter.sorted_term_types().includes("not-is-followed")) - ) { + if (filter_has_term_type(filter, "is-followed")) { // Use `set_timeout to call after we update the topic // visibility policy locally. // Calling this outside `user_topics_ui` to avoid circular imports. From e6113b0dd4cc748bfdd59915fd94c8afa24ce2e7 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Sun, 27 Oct 2024 12:52:53 +0530 Subject: [PATCH 244/276] message_events: Discard message list that cannot be live updated. Since we cannot cheaply live update `is-followed` narrow, we discard any message list or data with that narrow if we received a message event that changes topic visibility from or to `FOLLOWED`. --- web/src/message_events.js | 28 ++++++++++++++++++++++++++++ web/src/message_lists.ts | 2 +- web/src/server_events_dispatch.js | 15 ++++++++++++++- web/tests/dispatch.test.js | 14 ++++++++++++++ web/tests/lib/events.js | 8 ++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/web/src/message_events.js b/web/src/message_events.js index 21c83ec49c..2c7b7e9e2d 100644 --- a/web/src/message_events.js +++ b/web/src/message_events.js @@ -47,6 +47,34 @@ function filter_has_term_type(filter, term_type) { ); } +export function discard_cached_lists_with_term_type(term_type) { + // Discards cached MessageList and MessageListData which have + // `term_type` and `not-term_type`. + assert(!term_type.includes("not-")); + + // We loop over rendered message lists and cached message data separately since + // they are separately maintained and can have different items. + for (const msg_list of message_lists.all_rendered_message_lists()) { + // We never want to discard the current message list. + if (msg_list === message_lists.current) { + continue; + } + + const filter = msg_list.data.filter; + if (filter_has_term_type(filter, term_type)) { + message_lists.delete_message_list(msg_list); + message_list_data_cache.remove(filter); + } + } + + for (const msg_list_data of message_lists.non_rendered_data()) { + const filter = msg_list_data.filter; + if (filter_has_term_type(filter, term_type)) { + message_list_data_cache.remove(filter); + } + } +} + export function update_current_view_for_topic_visibility() { // If we have rendered message list / cached data based on topic // visibility policy, we need to rerender it to reflect the changes. It diff --git a/web/src/message_lists.ts b/web/src/message_lists.ts index 6e06ea5bd5..a8c8f8c69e 100644 --- a/web/src/message_lists.ts +++ b/web/src/message_lists.ts @@ -15,7 +15,7 @@ export function set_current(msg_list: MessageList | undefined): void { current = msg_list; } -function delete_message_list(message_list: MessageList): void { +export function delete_message_list(message_list: MessageList): void { message_list.view.$list.remove(); rendered_message_lists.delete(message_list.id); message_list.data.set_rendered_message_list_id(undefined); diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 920581967d..7f99f99a1d 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -90,6 +90,7 @@ import * as user_group_edit from "./user_group_edit"; import * as user_groups from "./user_groups"; import {user_settings} from "./user_settings"; import * as user_status from "./user_status"; +import * as user_topics from "./user_topics"; import * as user_topics_ui from "./user_topics_ui"; export function dispatch_normal_event(event) { @@ -1017,11 +1018,23 @@ export function dispatch_normal_event(event) { } break; - case "user_topic": + case "user_topic": { + const previous_topic_visibility = user_topics.get_topic_visibility_policy( + event.stream_id, + event.topic_name, + ); user_topics_ui.handle_topic_updates( event, message_events.update_current_view_for_topic_visibility(), ); + // Discard cached message lists if `event` topic was / is followed. + if ( + event.visibility_policy === user_topics.all_visibility_policies.FOLLOWED || + previous_topic_visibility === user_topics.all_visibility_policies.FOLLOWED + ) { + message_events.discard_cached_lists_with_term_type("is-followed"); + } break; + } } } diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index e22dd1f73a..3b2d8b9cc0 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -379,6 +379,20 @@ run_test("muted_topics", ({override}) => { assert_same(args.user_topic, event); }); +run_test("followed_topic", ({override}) => { + const event = event_fixtures.user_topic_with_followed_policy_change; + + const stub = make_stub(); + const discard_msg_list_stub = make_stub(); + override(user_topics_ui, "handle_topic_updates", stub.f); + override(message_events, "discard_cached_lists_with_term_type", discard_msg_list_stub.f); + dispatch(event); + assert.equal(stub.num_calls, 1); + assert.equal(discard_msg_list_stub.num_calls, 1); + const args = stub.get_args("user_topic"); + assert_same(args.user_topic, event); +}); + run_test("muted_users", ({override}) => { const event = event_fixtures.muted_users; diff --git a/web/tests/lib/events.js b/web/tests/lib/events.js index a395dc89ab..2b47162242 100644 --- a/web/tests/lib/events.js +++ b/web/tests/lib/events.js @@ -1164,6 +1164,14 @@ exports.fixtures = { visibility_policy: 1, }, + user_topic_with_followed_policy_change: { + type: "user_topic", + stream_id: 101, + topic_name: "js", + last_updated: fake_now, + visibility_policy: 3, + }, + web_reload_client: { type: "web_reload_client", immediate: true, From ca20a4df38727b3cbcbb62b479dc91bbd7b52533 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Sat, 2 Nov 2024 00:15:55 +0530 Subject: [PATCH 245/276] narrow_active: Remove broken tests. These tests have been broken since long ago, we don't see any errors since they are wrapped in `Sentry`. Also, We have a great puppeteer test suite for the verification of loading current narrow. --- web/tests/narrow_activate.test.js | 292 ------------------------------ 1 file changed, 292 deletions(-) delete mode 100644 web/tests/narrow_activate.test.js diff --git a/web/tests/narrow_activate.test.js b/web/tests/narrow_activate.test.js deleted file mode 100644 index df0731e537..0000000000 --- a/web/tests/narrow_activate.test.js +++ /dev/null @@ -1,292 +0,0 @@ -"use strict"; - -const assert = require("node:assert/strict"); - -const {mock_esm, set_global, zrequire} = require("./lib/namespace"); -const {run_test, noop} = require("./lib/test"); -const $ = require("./lib/zjquery"); - -set_global("history", {}); -mock_esm("../src/resize", { - resize_stream_filters_container() {}, -}); -const {Filter} = zrequire("../src/filter"); -const all_messages_data = mock_esm("../src/all_messages_data"); -const browser_history = mock_esm("../src/browser_history", { - state: {changing_hash: false}, - get_current_state_show_more_topics: () => undefined, -}); -const compose_actions = mock_esm("../src/compose_actions"); -const compose_banner = mock_esm("../src/compose_banner"); -const compose_closed_ui = mock_esm("../src/compose_closed_ui"); -const compose_recipient = mock_esm("../src/compose_recipient"); -const compose_notifications = mock_esm("../src/compose_notifications"); -const message_fetch = mock_esm("../src/message_fetch"); -const message_list = mock_esm("../src/message_list"); -const message_lists = mock_esm("../src/message_lists", { - current: { - view: { - $list: { - remove: noop, - removeClass: noop, - addClass: noop, - }, - update_sticky_recipient_headers: noop, - }, - data: { - filter: new Filter([{operator: "in", operand: "all"}]), - }, - }, - update_current_message_list(msg_list) { - message_lists.current = msg_list; - }, - all_rendered_message_lists() { - return [message_lists.current]; - }, -}); -const message_feed_top_notices = mock_esm("../src/message_feed_top_notices"); -const message_feed_loading = mock_esm("../src/message_feed_loading"); -const message_view_header = mock_esm("../src/message_view_header"); -const message_viewport = mock_esm("../src/message_viewport"); -const narrow_history = mock_esm("../src/narrow_history"); -const narrow_title = mock_esm("../src/narrow_title"); -const stream_list = mock_esm("../src/stream_list", {is_zoomed_in: () => false}); -const left_sidebar_navigation_area = mock_esm("../src/left_sidebar_navigation_area"); -const typing_events = mock_esm("../src/typing_events"); -const unread_ops = mock_esm("../src/unread_ops"); -mock_esm("../src/pm_list", { - handle_narrow_activated() {}, -}); -mock_esm("../src/unread_ui", { - reset_unread_banner() {}, - update_unread_banner() {}, -}); - -// -// We have strange hacks in message_view.show to sleep 0 -// seconds. -set_global("setTimeout", (f, t) => { - assert.equal(t, 0); - f(); -}); - -mock_esm("../src/user_topics", { - is_topic_muted: () => false, -}); - -const {buddy_list} = zrequire("buddy_list"); -const activity_ui = zrequire("activity_ui"); -const narrow_state = zrequire("narrow_state"); -const stream_data = zrequire("stream_data"); -const message_view = zrequire("message_view"); -const people = zrequire("people"); -const {set_realm} = zrequire("state_data"); - -set_realm({}); - -const denmark = { - subscribed: false, - color: "blue", - name: "Denmark", - stream_id: 1, - is_muted: true, -}; -stream_data.add_sub(denmark); - -function test_helper({override}) { - const events = []; - - function stub(module, func_name) { - override(module, func_name, () => { - events.push([module, func_name]); - }); - } - - stub(browser_history, "set_hash"); - stub(compose_banner, "clear_message_sent_banners"); - stub(compose_actions, "on_narrow"); - stub(compose_closed_ui, "update_reply_recipient_label"); - stub(compose_recipient, "handle_middle_pane_transition"); - stub(narrow_history, "save_narrow_state_and_flush"); - stub(message_feed_loading, "hide_indicators"); - stub(message_feed_top_notices, "hide_top_of_narrow_notices"); - stub(narrow_title, "update_narrow_title"); - stub(stream_list, "handle_narrow_activated"); - stub(message_view_header, "render_title_area"); - stub(message_viewport, "stop_auto_scrolling"); - stub(left_sidebar_navigation_area, "handle_narrow_activated"); - stub(typing_events, "render_notifications_for_narrow"); - stub(unread_ops, "process_visible"); - stub(compose_closed_ui, "update_buttons_for_stream_views"); - stub(compose_closed_ui, "update_buttons_for_private"); - // We don't test the css calls; we just skip over them. - $("#mark_read_on_scroll_state_banner").toggleClass = noop; - - return { - assert_events(expected_events) { - assert.deepEqual(events, expected_events); - }, - }; -} - -function stub_message_list() { - message_list.MessageList = class MessageList { - constructor(opts) { - this.data = opts.data; - } - - view = { - set_message_offset(offset) { - this.offset = offset; - }, - - $list: { - remove: noop, - removeClass: noop, - addClass: noop, - }, - update_sticky_recipient_headers: noop, - }; - - get(msg_id) { - return this.data.get(msg_id); - } - - visibly_empty() { - return this.data.visibly_empty(); - } - - select_id(msg_id) { - this.selected_id = msg_id; - } - }; -} - -run_test("basics", ({override, override_rewire}) => { - stub_message_list(); - activity_ui.set_cursor_and_filter(); - - const me = { - email: "me@zulip.com", - user_id: 999, - full_name: "Me Myself", - }; - people.add_active_user(me); - people.initialize_current_user(me.user_id); - override(buddy_list, "populate", noop); - override_rewire(message_view, "try_rendering_locally_for_same_narrow", noop); - - const helper = test_helper({override}); - const terms = [{operator: "stream", operand: denmark.stream_id.toString()}]; - - const selected_id = 1000; - - const selected_message = { - id: selected_id, - type: "stream", - stream_id: denmark.stream_id, - topic: "whatever", - }; - - const messages = [selected_message]; - - const row = { - length: 1, - get_offset_to_window: () => ({top: 25}), - }; - - message_lists.current.selected_id = () => -1; - message_lists.current.get_row = () => row; - - all_messages_data.all_messages_data = { - all_messages: () => messages, - visibly_empty: () => false, - first: () => ({id: 900}), - last: () => ({id: 1100}), - filter: { - equals: () => false, - }, - }; - - $("#navbar-fixed-container").set_height(40); - $("#compose").get_offset_to_window = () => ({top: 200}); - - message_fetch.load_messages_for_narrow = (opts) => { - // Only validates the anchor and set of fields - assert.deepEqual(opts, { - cont: opts.cont, - msg_list: opts.msg_list, - anchor: 1000, - validate_filter_topic_post_fetch: false, - }); - - opts.cont(); - }; - - override( - compose_notifications, - "maybe_show_one_time_interleaved_view_messages_fading_banner", - noop, - ); - - message_view.show(terms, { - then_select_id: selected_id, - }); - - assert.equal(message_lists.current.selected_id, selected_id); - // 25 was the offset of the selected message but it is low for the - // message top to be visible, so we use set offset to navbar height + header height. - assert.equal(message_lists.current.view.offset, 80); - assert.equal(narrow_state.narrowed_to_pms(), false); - - helper.assert_events([ - [message_feed_top_notices, "hide_top_of_narrow_notices"], - [message_feed_loading, "hide_indicators"], - [compose_banner, "clear_message_sent_banners"], - [message_viewport, "stop_auto_scrolling"], - [browser_history, "set_hash"], - [compose_actions, "on_narrow"], - [unread_ops, "process_visible"], - [narrow_history, "save_narrow_state_and_flush"], - [typing_events, "render_notifications_for_narrow"], - [compose_closed_ui, "update_buttons_for_stream_views"], - [compose_closed_ui, "update_reply_recipient_label"], - [message_view_header, "render_title_area"], - [narrow_title, "update_narrow_title"], - [left_sidebar_navigation_area, "handle_narrow_activated"], - [stream_list, "handle_narrow_activated"], - [compose_recipient, "handle_middle_pane_transition"], - ]); - - message_lists.current.selected_id = () => -1; - message_lists.current.get_row = () => row; - - message_view.show([{operator: "is", operand: "private"}], { - then_select_id: selected_id, - }); - - assert.equal(narrow_state.narrowed_to_pms(), true); - - message_lists.current.selected_id = () => -1; - // Row offset is between navbar and compose, so we keep it in the same position. - row.get_offset_to_window = () => ({top: 100, bottom: 150}); - message_lists.current.get_row = () => row; - - message_view.show(terms, { - then_select_id: selected_id, - }); - - assert.equal(message_lists.current.view.offset, 100); - - message_lists.current.selected_id = () => -1; - // Row is below navbar and row bottom is below compose but since the message is - // visible enough, we don't scroll the message to a new position. - row.get_offset_to_window = () => ({top: 150, bottom: 250}); - message_lists.current.get_row = () => row; - - message_view.show(terms, { - then_select_id: selected_id, - }); - - assert.equal(message_lists.current.view.offset, 150); -}); From e9e61f303b9e07ad2b1f21e41daf7f7bf23afb22 Mon Sep 17 00:00:00 2001 From: Gunnar Samuelsson Date: Tue, 5 Nov 2024 17:30:35 +0100 Subject: [PATCH 246/276] docs: Fix typo in contributing guide. Corrected a minor typo in the "Contributing guide" to improve readability. This change ensures the document aligns with Zulip's guidelines for documentation. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d4d292ec6..92fe3d59c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -284,7 +284,7 @@ labels. ### Common questions -- **What if somebody is already working on the issue I want do claim?** There +- **What if somebody is already working on the issue I want to claim?** There are lots of issue to work on! If somebody else is actively working on the issue, you can find a different one, or help with reviewing their work. From b9314562a7bdf3afb234604468435d70a4375109 Mon Sep 17 00:00:00 2001 From: Alya Abbott Date: Tue, 5 Nov 2024 10:04:48 -0800 Subject: [PATCH 247/276] help: Document pretty message links. --- help/include/links-examples.md | 3 ++- help/include/links-intro.md | 5 ++--- help/link-to-a-message-or-conversation.md | 26 +++++++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/help/include/links-examples.md b/help/include/links-examples.md index d21c56e327..25b97d0ec5 100644 --- a/help/include/links-examples.md +++ b/help/include/links-examples.md @@ -4,7 +4,8 @@ Named link: [Zulip homepage](zulip.com) A URL (links automatically): zulip.com Channel link: #**channel name** -Channel and topic link: #**channel name>topic name** +Topic link: #**channel name>topic name** +Message link: #**channel name>topic name@123** Custom linkifier: For example, #2468 can automatically link to an issue in your tracker. ``` diff --git a/help/include/links-intro.md b/help/include/links-intro.md index d6398c90c8..2bb497b5d0 100644 --- a/help/include/links-intro.md +++ b/help/include/links-intro.md @@ -2,7 +2,6 @@ In Zulip, you can insert a named link using Markdown formatting. In addition, Zu automatically creates links for you when you enter: - A URL -- An appropriately formatted channel name, or a channel name followed by a topic - (see also [Link to a message or - conversation](/help/link-to-a-message-or-conversation)) +- A reference to a channel, topic, or specific message (see also [Link to a + message or conversation](/help/link-to-a-message-or-conversation)) - Text that matches a [custom linkifier](/help/add-a-custom-linkifier) set up by your organization diff --git a/help/link-to-a-message-or-conversation.md b/help/link-to-a-message-or-conversation.md index 86dafe762c..6957508681 100644 --- a/help/link-to-a-message-or-conversation.md +++ b/help/link-to-a-message-or-conversation.md @@ -50,13 +50,21 @@ This copies to your clipboard a permanent link to the message, displayed in its thread (i.e. topic view for messages in a channel). Viewing a topic via a message link will never mark messages as read. -These links will still work even when the message is -[moved to another topic](/help/move-content-to-another-topic) -or [channel](/help/move-content-to-another-channel) or -if its [topic is resolved](/help/resolve-a-topic). +These links will still work even when the message is [moved to another +topic](/help/move-content-to-another-topic) or +[channel](/help/move-content-to-another-channel), or if its [topic is +resolved](/help/resolve-a-topic). Zulip uses the same permanent link syntax when +[quoting a message](/help/quote-and-reply). -Zulip uses the same permanent link syntax when [quoting a -message](/help/quote-and-reply). +When you paste a message link into the compose box, it gets automatically +formatted to be easy to read: + +``` +#**channel name>topic name@message ID** +``` + +When you send your message, the link will appear as **#channel name>topic +name@💬**. {start_tabs} @@ -71,6 +79,12 @@ message](/help/quote-and-reply). If using Zulip in a browser, you can also click on the timestamp of a message, and copy the URL from your browser's address bar. +!!! tip "" + + When you paste a message link into Zulip, it is automatically + formatted for you. You can paste as plain text if you prefer with + Ctrl + Shift + V. + {end_tabs} ### Get a link to a specific topic From 782fe316ca3390b5dd4d5bd8459429d3535e94d4 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Tue, 5 Nov 2024 13:45:51 -0800 Subject: [PATCH 248/276] buddy_list: Support unread marker for avatar mode. --- web/src/buddy_data.ts | 2 ++ web/styles/right_sidebar.css | 23 +++++++++++++---------- web/templates/presence_row.hbs | 2 +- web/tests/activity.test.js | 1 + web/tests/buddy_data.test.js | 3 +++ 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/web/src/buddy_data.ts b/web/src/buddy_data.ts index 8163c9e741..02b2475096 100644 --- a/web/src/buddy_data.ts +++ b/web/src/buddy_data.ts @@ -178,6 +178,7 @@ export type BuddyUserInfo = { num_unread: number; user_circle_class: string; status_text: string | undefined; + has_status_text: boolean; user_list_style: { COMPACT: boolean; WITH_STATUS: boolean; @@ -210,6 +211,7 @@ export function info_for(user_id: number): BuddyUserInfo { num_unread: get_num_unread(user_id), user_circle_class, status_text, + has_status_text: Boolean(status_text), user_list_style, should_add_guest_user_indicator: people.should_add_guest_user_indicator(user_id), }; diff --git a/web/styles/right_sidebar.css b/web/styles/right_sidebar.css index 8e30af3435..bb78bafe2b 100644 --- a/web/styles/right_sidebar.css +++ b/web/styles/right_sidebar.css @@ -262,12 +262,14 @@ $user_status_emoji_width: 24px; .selectable_sidebar_block { margin: 2px; - grid-template: - "row-content markers-and-controls" var( - --line-height-sidebar-row-with-avatars - ) - "row-content ." auto / minmax(0, 1fr) - minmax(0, auto); + } + + .unread_count:not(.hide) { + margin-right: 2px; + } + + &.with_status .unread_count { + align-self: baseline; } } @@ -285,10 +287,11 @@ $user_status_emoji_width: 24px; .user_sidebar_entry.with_avatar .selectable_sidebar_block { line-height: var(--line-height-sidebar-row-with-avatars); display: grid; - grid-template: "avatar row-content" var(--right-sidebar-avatar-width) / auto minmax( - 0, - 1fr - ); + grid-template: + "avatar row-content markers-and-controls" var( + --right-sidebar-avatar-width + ) + "avatar row-content ." auto / auto minmax(0, 1fr) minmax(0, auto); justify-content: flex-start; align-items: center; } diff --git a/web/templates/presence_row.hbs b/web/templates/presence_row.hbs index dea8aec996..3d2f4d5a69 100644 --- a/web/templates/presence_row.hbs +++ b/web/templates/presence_row.hbs @@ -1,4 +1,4 @@ -
                            • +
                            • {{#if user_list_style.WITH_STATUS}} diff --git a/web/tests/activity.test.js b/web/tests/activity.test.js index 7bc01d38b6..128cb96246 100644 --- a/web/tests/activity.test.js +++ b/web/tests/activity.test.js @@ -505,6 +505,7 @@ test("insert_one_user_into_empty_list", ({override, mock_template}) => { user_circle_class: "user_circle_green", status_emoji_info: undefined, status_text: undefined, + has_status_text: false, user_list_style: { COMPACT: false, WITH_STATUS: true, diff --git a/web/tests/buddy_data.test.js b/web/tests/buddy_data.test.js index 99a847f24e..6a4ad2c81f 100644 --- a/web/tests/buddy_data.test.js +++ b/web/tests/buddy_data.test.js @@ -607,6 +607,7 @@ test("get_items_for_users", ({override}) => { profile_picture: "/avatar/1001", status_emoji_info, status_text: undefined, + has_status_text: false, user_circle_class: "user_circle_green", user_id: 1001, user_list_style, @@ -620,6 +621,7 @@ test("get_items_for_users", ({override}) => { profile_picture: "/avatar/1002", status_emoji_info, status_text: undefined, + has_status_text: false, user_circle_class: "user_circle_empty", user_id: 1002, user_list_style, @@ -633,6 +635,7 @@ test("get_items_for_users", ({override}) => { profile_picture: "/avatar/1003", status_emoji_info, status_text: undefined, + has_status_text: false, user_circle_class: "user_circle_empty", user_id: 1003, user_list_style, From 75c143a05ea6c44995c8d97139619f766d8340a6 Mon Sep 17 00:00:00 2001 From: Alya Abbott Date: Tue, 5 Nov 2024 15:07:12 -0800 Subject: [PATCH 249/276] help: Document how archived channels work now. Initial bullets closely match in-app confirmation modal. --- help/archive-a-channel.md | 44 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/help/archive-a-channel.md b/help/archive-a-channel.md index 379dc13825..66094ff4d2 100644 --- a/help/archive-a-channel.md +++ b/help/archive-a-channel.md @@ -2,19 +2,29 @@ {!admin-only.md!} -Archiving a channel will hide the channel from most views in the UI. -Archiving a channel does not delete a channel's messages, and its -messages will remain accessible via search and the like, unless you -first change the channel's access permissions. +You can archive channels you no longer plan to use. Archiving a channel: -Archiving a channel does not remove subscribers or change its access -permissions. +- Removes it from the left sidebar for all users. +- Prevents new messages from being sent to the channel. +- Prevents messages in the channel from being edited, deleted, or moved. -Consider [renaming channels](/help/rename-a-channel) rather than -archiving them where appropriate. +Archiving a channel does not remove subscribers, or change who can access it. +Messages in archived channels still appear in [search +results](/help/search-for-messages), the [combined feed](/help/combined-feed), +and [recent conversations](/help/recent-conversations). + +To hide the content in a channel, you can make it +[private](/help/change-the-privacy-of-a-channel) and [unsubscribe +users](/help/manage-user-channel-subscriptions) from it prior to archiving. ## Archive a channel +!!! warn "" + + Channels can be [unarchived](#unarchiving-archived-channels) only by + [contacting support](/help/contact-support) for organizations hosted + on Zulip Cloud, or by your self-hosted server's administrator. + {start_tabs} {tab|desktop-web} @@ -23,8 +33,8 @@ archiving them where appropriate. 1. Select a channel. -1. Click the **archive** icon near the top right - corner of the channel settings panel. +1. Click the **archive** () icon + in the upper right corner of the channel settings panel. 1. Approve by clicking **Confirm**. @@ -32,22 +42,19 @@ archiving them where appropriate. You can also hover over a channel in the left sidebar, click on the **ellipsis** (), and - select **Channel settings** to access the **archive** - icon. + select **Channel settings** to access settings for the channel. {end_tabs} -!!! warn "" - - Archiving a channel is currently irreversible via the UI. - ## Unarchiving archived channels +Zulip Cloud organizations that need to unarchive a channel can [contact Zulip +support](/help/contact-support). + If you are self-hosting, you can unarchive an archived channel using the `unarchive_channel` [management command][management-command]. This will restore it as a private channel with shared history, and subscribe all organization -owners to it. If you are using Zulip Cloud, you can [contact us](/help/contact-support) -for help. +owners to it. [management-command]: https://zulip.readthedocs.io/en/latest/production/management-commands.html#other-useful-manage-py-commands @@ -59,4 +66,3 @@ https://zulip.readthedocs.io/en/latest/production/management-commands.html#other * [Delete a topic](/help/delete-a-topic) * [Message retention policy](/help/message-retention-policy) * [Channel permissions](/help/channel-permissions) -* [Zulip Cloud or self-hosting?](/help/zulip-cloud-or-self-hosting) From 0bd9f4f67419ed33f60b974b6e3e7e8b84e510ac Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 5 Nov 2024 12:20:11 -0800 Subject: [PATCH 250/276] install-node: Upgrade Node.js from 20.18.0 to 22.11.0. Signed-off-by: Anders Kaseorg --- scripts/lib/install-node | 6 +++--- version.py | 2 +- web/tests/lib/index.js | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/lib/install-node b/scripts/lib/install-node index 8c8b824013..ac746e7882 100755 --- a/scripts/lib/install-node +++ b/scripts/lib/install-node @@ -1,18 +1,18 @@ #!/usr/bin/env bash set -euo pipefail -version=20.18.0 +version=22.11.0 arch="$(uname -m)" case $arch in x86_64) tarball="node-v$version-linux-x64.tar.xz" - sha256=4543670b589593f8fa5f106111fd5139081da42bb165a9239f05195e405f240a + sha256=83bf07dd343002a26211cf1fcd46a9d9534219aad42ee02847816940bf610a72 ;; aarch64) tarball="node-v$version-linux-arm64.tar.xz" - sha256=a9ce85675ba33f00527f6234d90000946c0936fb4fca605f1891bb5f4fe6fb0a + sha256=6031d04b98f59ff0f7cb98566f65b115ecd893d3b7870821171708cdbaf7ae6e ;; esac diff --git a/version.py b/version.py index 185898b379..297c369b02 100644 --- a/version.py +++ b/version.py @@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 319 # Last bumped for message-link class # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = (296, 0) # bumped 2024-10-31 to upgrade Python requirements +PROVISION_VERSION = (297, 0) # bumped 2024-11-05 to upgrade Node.js diff --git a/web/tests/lib/index.js b/web/tests/lib/index.js index 94896a22f3..5a85014eac 100644 --- a/web/tests/lib/index.js +++ b/web/tests/lib/index.js @@ -25,9 +25,12 @@ global.DOMParser = dom.window.DOMParser; global.HTMLAnchorElement = dom.window.HTMLAnchorElement; global.HTMLElement = dom.window.HTMLElement; global.Window = dom.window.Window; -global.navigator = { - userAgent: "node.js", -}; +Object.defineProperty(global, "navigator", { + value: { + userAgent: "node.js", + }, + writable: true, +}); require("@babel/register")({ extensions: [".es6", ".es", ".jsx", ".js", ".mjs", ".ts"], From 03c3bb31dc6b42f303e33ac56614852bae75530e Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 5 Nov 2024 16:26:56 -0800 Subject: [PATCH 251/276] settings_linkifiers: Remove dead code. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s been dead since d7e1e4a2c0b11e6fc4a3fa396aa7be9f75da8943 (#2261) introduced it. Signed-off-by: Anders Kaseorg --- web/src/settings_linkifiers.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/web/src/settings_linkifiers.ts b/web/src/settings_linkifiers.ts index e733d1d0e6..74e1a8831b 100644 --- a/web/src/settings_linkifiers.ts +++ b/web/src/settings_linkifiers.ts @@ -21,10 +21,6 @@ import * as util from "./util"; type RealmLinkifiers = typeof realm.realm_linkifiers; -const configure_linkifier_api_response_schema = z.object({ - id: z.number(), -}); - const meta = { loaded: false, }; @@ -258,21 +254,14 @@ export function build_page(): void { $linkifier_status.hide(); $pattern_status.hide(); $template_status.hide(); - const linkifier: {[key in string]?: string} & {id?: number} = {}; - - for (const obj of $(this).serializeArray()) { - linkifier[obj.name] = obj.value; - } void channel.post({ url: "/json/realm/filters", data: $(this).serialize(), - success(raw_data) { - const data = configure_linkifier_api_response_schema.parse(raw_data); + success() { $("#linkifier_pattern").val(""); $("#linkifier_template").val(""); $add_linkifier_button.prop("disabled", false); - linkifier.id = data.id; ui_report.success( $t_html({defaultMessage: "Custom linkifier added!"}), $linkifier_status, From 6037230ea8e5935c3afb4530fc54da0b7da69d36 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 5 Nov 2024 12:54:33 -0800 Subject: [PATCH 252/276] eslint: Fix @typescript-eslint/consistent-indexed-object-style. Signed-off-by: Anders Kaseorg --- web/src/portico/team.ts | 4 +--- web/src/scheduled_messages.ts | 2 +- web/src/settings_config.ts | 9 +++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/web/src/portico/team.ts b/web/src/portico/team.ts index 55e6e512cd..61795f29ab 100644 --- a/web/src/portico/team.ts +++ b/web/src/portico/team.ts @@ -74,9 +74,7 @@ export type Contributor = { email?: string | undefined; github_username?: string | undefined; name?: string | undefined; -} & { - [K in RepositoryName]?: number; -}; +} & Partial>; type ContributorData = { avatar: string; email?: string | undefined; diff --git a/web/src/scheduled_messages.ts b/web/src/scheduled_messages.ts index 8c7c9f1bcd..4af844552e 100644 --- a/web/src/scheduled_messages.ts +++ b/web/src/scheduled_messages.ts @@ -14,7 +14,7 @@ type TimeKey = | "tomorrow_four_pm" | "monday_nine_am"; -type SendOption = {[key in TimeKey]?: {text: string; stamp: number}}; +type SendOption = Partial>; export const MINIMUM_SCHEDULED_MESSAGE_DELAY_SECONDS = 5 * 60; diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts index 77bac3e0bf..c2bdfc552b 100644 --- a/web/src/settings_config.ts +++ b/web/src/settings_config.ts @@ -145,12 +145,13 @@ export const web_home_view_values = { }; type ColorScheme = "automatic" | "dark" | "light"; -export type ColorSchemeValues = { - [key in ColorScheme]: { +export type ColorSchemeValues = Record< + ColorScheme, + { code: number; description: string; - }; -}; + } +>; export const color_scheme_values = { automatic: { From 799e59bb8f32dc52abbb030b1eafa304224f6d56 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 5 Nov 2024 12:46:07 -0800 Subject: [PATCH 253/276] dependencies: Upgrade JavaScript dependencies. Signed-off-by: Anders Kaseorg --- package.json | 7 +- patches/puppeteer-core.patch | 56 - pnpm-lock.yaml | 4055 +++++++++++++++++----------------- version.py | 2 +- web/babel.config.js | 2 +- 5 files changed, 1973 insertions(+), 2149 deletions(-) delete mode 100644 patches/puppeteer-core.patch diff --git a/package.json b/package.json index 771066b128..b9edc1988b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "packageManager": "pnpm@9.12.0+sha512.4abf725084d7bcbafbd728bfc7bee61f2f791f977fd87542b3579dcb23504d170d46337945e4c66485cd12d588a0c0e570ed9c477e7ccdd8507cf05f3f92eaca", + "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee", "dependencies": { "@babel/core": "^7.5.5", "@babel/preset-env": "^7.5.5", @@ -109,7 +109,7 @@ "@types/lodash": "^4.14.172", "@types/micromodal": "^0.3.3", "@types/minimalistic-assert": "^1.0.1", - "@types/node": "^20.11.20", + "@types/node": "^22.9.0", "@types/plotly.js": "^2.12.20", "@types/sortablejs": "^1.15.1", "@types/spectrum": "^1.8.4", @@ -152,7 +152,7 @@ "swagger-parser": "^10.0.0", "ts-node": "^10.0.0", "typescript": "^5.0.2", - "vnu-jar": "^23.4.11", + "vnu-jar": "^24.10.17", "webpack-dev-server": "^5.0.2", "xvfb": "^0.4.0", "yaml": "^2.0.0-8", @@ -164,7 +164,6 @@ "source-map@^0.6": "npm:source-map-js@^1.2.1" }, "patchedDependencies": { - "puppeteer-core": "patches/puppeteer-core.patch", "source-sans@3.46.0": "patches/source-sans@3.46.0.patch", "tippy.js@6.3.7": "patches/tippy.js@6.3.7.patch" } diff --git a/patches/puppeteer-core.patch b/patches/puppeteer-core.patch deleted file mode 100644 index 87a2a14634..0000000000 --- a/patches/puppeteer-core.patch +++ /dev/null @@ -1,56 +0,0 @@ -diff --git a/lib/cjs/puppeteer/common/QueryHandler.js b/lib/cjs/puppeteer/common/QueryHandler.js -index db0ff2ee001ac452b6de5804ec8810e566992299..13437d905fe4fca94ad69e4e2fd95ba6b2cce89a 100644 ---- a/lib/cjs/puppeteer/common/QueryHandler.js -+++ b/lib/cjs/puppeteer/common/QueryHandler.js -@@ -163,8 +163,7 @@ class QueryHandler { - return await frame.isolatedRealm().adoptHandle(elementOrFrame); - })(), false); - const { visible = false, hidden = false, timeout, signal } = options; -- const polling = options.polling ?? -- (visible || hidden ? "raf" /* PollingOptions.RAF */ : "mutation" /* PollingOptions.MUTATION */); -+ const polling = visible || hidden ? "raf" /* PollingOptions.RAF */ : options.polling; - try { - const env_4 = { stack: [], error: void 0, hasError: false }; - try { -diff --git a/lib/es5-iife/puppeteer-core-browser.js b/lib/es5-iife/puppeteer-core-browser.js -index 3262ea9f5fed2d0e3d7a0ece16ad31c7e5bc4cf6..f73deab795a92d80c913adafb6c3eff7eda5e5e0 100644 ---- a/lib/es5-iife/puppeteer-core-browser.js -+++ b/lib/es5-iife/puppeteer-core-browser.js -@@ -4594,7 +4594,7 @@ var Puppeteer = function (exports, _PuppeteerURL, _LazyArg, _ARIAQueryHandler, _ - timeout, - signal - } = options; -- const polling = options.polling ?? (visible || hidden ? "raf" /* PollingOptions.RAF */ : "mutation" /* PollingOptions.MUTATION */); -+ const polling = visible || hidden ? "raf" /* PollingOptions.RAF */ : options.polling; - try { - const env_4 = { - stack: [], -diff --git a/lib/esm/puppeteer/common/QueryHandler.js b/lib/esm/puppeteer/common/QueryHandler.js -index b07ddf54fb1830c8ddc42dc6d0452b288696caa4..cb0203c7c1f98f03ea6658b33c8da3710ab71b29 100644 ---- a/lib/esm/puppeteer/common/QueryHandler.js -+++ b/lib/esm/puppeteer/common/QueryHandler.js -@@ -160,8 +160,7 @@ export class QueryHandler { - return await frame.isolatedRealm().adoptHandle(elementOrFrame); - })(), false); - const { visible = false, hidden = false, timeout, signal } = options; -- const polling = options.polling ?? -- (visible || hidden ? "raf" /* PollingOptions.RAF */ : "mutation" /* PollingOptions.MUTATION */); -+ const polling = visible || hidden ? "raf" /* PollingOptions.RAF */ : options.polling; - try { - const env_4 = { stack: [], error: void 0, hasError: false }; - try { -diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts -index f377771f22a193164760e703c2f3af123588ab16..db10e1cf94ada080c710f33d4dda6f4a116400fa 100644 ---- a/src/common/QueryHandler.ts -+++ b/src/common/QueryHandler.ts -@@ -162,9 +162,7 @@ export class QueryHandler { - })(); - - const {visible = false, hidden = false, timeout, signal} = options; -- const polling = -- options.polling ?? -- (visible || hidden ? PollingOptions.RAF : PollingOptions.MUTATION); -+ const polling = visible || hidden ? PollingOptions.RAF : options.polling; - - try { - signal?.throwIfAborted(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76288eb64a..55c1df8655 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ overrides: source-map@^0.6: npm:source-map-js@^1.2.1 patchedDependencies: - puppeteer-core: - hash: arlsztxuq6xlcowulqo36wnkoa - path: patches/puppeteer-core.patch source-sans@3.46.0: hash: 4n7ij66tzyhzaqnxsenbilrxr4 path: patches/source-sans@3.46.0.patch @@ -24,28 +21,28 @@ importers: dependencies: '@babel/core': specifier: ^7.5.5 - version: 7.25.8 + version: 7.26.0 '@babel/preset-env': specifier: ^7.5.5 - version: 7.25.8(@babel/core@7.25.8) + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': specifier: ^7.15.0 - version: 7.25.7(@babel/core@7.25.8) + version: 7.26.0(@babel/core@7.26.0) '@babel/register': specifier: ^7.6.2 - version: 7.25.7(@babel/core@7.25.8) + version: 7.25.9(@babel/core@7.26.0) '@fontsource-variable/open-sans': specifier: ^5.0.9 version: 5.1.0 '@formatjs/intl': specifier: ^2.0.0 - version: 2.10.8(typescript@5.6.3) + version: 2.10.14(typescript@5.6.3) '@gfx/zopfli': specifier: ^1.0.15 version: 1.0.15 '@giphy/js-components': specifier: ^5.13.0 - version: 5.13.0(@babel/core@7.25.8) + version: 5.13.0(@babel/core@7.26.0) '@giphy/js-fetch-api': specifier: ^5.6.0 version: 5.6.0 @@ -54,22 +51,22 @@ importers: version: 5.1.1(koa@2.15.3) '@sentry/browser': specifier: ^8.33.1 - version: 8.34.0 + version: 8.37.1 '@sentry/core': specifier: ^8.33.1 - version: 8.34.0 + version: 8.37.1 '@uppy/core': specifier: ^4.2.0 - version: 4.2.2 + version: 4.2.3 '@uppy/drag-drop': specifier: ^4.0.2 - version: 4.0.3(@uppy/core@4.2.2) + version: 4.0.4(@uppy/core@4.2.3) '@uppy/progress-bar': specifier: ^4.0.0 - version: 4.0.0(@uppy/core@4.2.2) + version: 4.0.1(@uppy/core@4.2.3) '@uppy/tus': specifier: ^4.1.0 - version: 4.1.2(@uppy/core@4.2.2) + version: 4.1.3(@uppy/core@4.2.3) '@zxcvbn-ts/core': specifier: ^3.0.1 version: 3.0.4 @@ -84,10 +81,10 @@ importers: version: 5.0.2 babel-loader: specifier: ^9.1.0 - version: 9.2.1(@babel/core@7.25.8)(webpack@5.95.0) + version: 9.2.1(@babel/core@7.26.0)(webpack@5.96.1) babel-plugin-formatjs: specifier: ^10.2.6 - version: 10.5.18 + version: 10.5.24 blueimp-md5: specifier: ^2.10.0 version: 2.19.0 @@ -102,16 +99,16 @@ importers: version: 2.9.3 compression-webpack-plugin: specifier: ^11.1.0 - version: 11.1.0(webpack@5.95.0) + version: 11.1.0(webpack@5.96.1) core-js: specifier: ^3.37.0 - version: 3.38.1 + version: 3.39.0 css-loader: specifier: ^7.1.1 - version: 7.1.2(webpack@5.95.0) + version: 7.1.2(webpack@5.96.1) css-minimizer-webpack-plugin: specifier: ^7.0.0 - version: 7.0.0(clean-css@5.3.3)(webpack@5.95.0) + version: 7.0.0(clean-css@5.3.3)(webpack@5.96.1) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -132,7 +129,7 @@ importers: version: 2.1.4 expose-loader: specifier: ^5.0.0 - version: 5.0.0(webpack@5.95.0) + version: 5.0.0(webpack@5.96.1) flatpickr: specifier: ^4.5.7 version: 4.6.13 @@ -153,10 +150,10 @@ importers: version: 1.7.3(handlebars@4.7.8) html-webpack-plugin: specifier: ^5.3.2 - version: 5.6.0(webpack@5.95.0) + version: 5.6.3(webpack@5.96.1) intl-messageformat: specifier: ^10.3.0 - version: 10.7.0 + version: 10.7.6 is-url: specifier: ^1.2.4 version: 1.2.4 @@ -186,7 +183,7 @@ importers: version: 0.4.10 mini-css-extract-plugin: specifier: ^2.2.2 - version: 2.9.1(webpack@5.95.0) + version: 2.9.2(webpack@5.96.1) minimalistic-assert: specifier: ^1.0.1 version: 1.0.1 @@ -195,7 +192,7 @@ importers: version: 9.4.3 plotly.js: specifier: ^2.0.0 - version: 2.35.2(mapbox-gl@1.13.3)(webpack@5.95.0) + version: 2.35.2(mapbox-gl@1.13.3)(webpack@5.96.1) postcss: specifier: ^8.0.3 version: 8.4.47 @@ -207,13 +204,13 @@ importers: version: 16.1.0(postcss@8.4.47) postcss-loader: specifier: ^8.0.0 - version: 8.1.1(postcss@8.4.47)(typescript@5.6.3)(webpack@5.95.0) + version: 8.1.1(postcss@8.4.47)(typescript@5.6.3)(webpack@5.96.1) postcss-prefixwrap: specifier: ^1.24.0 version: 1.52.0(postcss@8.4.47) postcss-preset-env: specifier: ^10.0.2 - version: 10.0.7(postcss@8.4.47) + version: 10.0.9(postcss@8.4.47) postcss-simple-vars: specifier: ^7.0.0 version: 7.0.1(postcss@8.4.47) @@ -252,7 +249,7 @@ importers: version: 3.1.2 style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.95.0) + version: 4.0.0(webpack@5.96.1) stylelint-high-performance-animation: specifier: ^1.10.0 version: 1.10.0(stylelint@16.10.0(typescript@5.6.3)) @@ -270,7 +267,7 @@ importers: version: 7.2.0 url-loader: specifier: ^4.1.1 - version: 4.1.1(webpack@5.95.0) + version: 4.1.1(webpack@5.96.1) url-template: specifier: ^2.0.8 version: 2.0.8 @@ -279,13 +276,13 @@ importers: version: 8.0.1 webpack: specifier: ^5.61.0 - version: 5.95.0(webpack-cli@5.1.4) + version: 5.96.1(webpack-cli@5.1.4) webpack-bundle-tracker: specifier: ^3.0.1 version: 3.1.1 webpack-cli: specifier: ^5.0.1 - version: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.95.0) + version: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.96.1) winchan: specifier: ^0.2.1 version: 0.2.2 @@ -295,13 +292,13 @@ importers: devDependencies: '@babel/eslint-parser': specifier: ^7.11.3 - version: 7.25.8(@babel/core@7.25.8)(eslint@8.57.1) + version: 7.25.9(@babel/core@7.26.0)(eslint@8.57.1) '@babel/plugin-transform-modules-commonjs': specifier: ^7.19.6 - version: 7.25.7(@babel/core@7.25.8) + version: 7.25.9(@babel/core@7.26.0) '@formatjs/cli': specifier: ^6.0.0 - version: 6.2.15 + version: 6.3.8 '@types/autosize': specifier: ^4.0.1 version: 4.0.3 @@ -316,7 +313,7 @@ importers: version: 1.2.32 '@types/jquery': specifier: ^3.3.31 - version: 3.5.31 + version: 3.5.32 '@types/jquery.validation': specifier: ^1.16.7 version: 1.17.0 @@ -331,7 +328,7 @@ importers: version: 2.15.0 '@types/lodash': specifier: ^4.14.172 - version: 4.17.10 + version: 4.17.13 '@types/micromodal': specifier: ^0.3.3 version: 0.3.5 @@ -339,11 +336,11 @@ importers: specifier: ^1.0.1 version: 1.0.3 '@types/node': - specifier: ^20.11.20 - version: 20.16.11 + specifier: ^22.9.0 + version: 22.9.0 '@types/plotly.js': specifier: ^2.12.20 - version: 2.33.4 + version: 2.33.5 '@types/sortablejs': specifier: ^1.15.1 version: 1.15.8 @@ -361,10 +358,10 @@ importers: version: 5.0.5 '@typescript-eslint/eslint-plugin': specifier: ^8.2.0 - version: 8.9.0(@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/parser': specifier: ^8.2.0 - version: 8.9.0(eslint@8.57.1)(typescript@5.6.3) + version: 8.13.0(eslint@8.57.1)(typescript@5.6.3) babel-plugin-istanbul: specifier: ^7.0.0 version: 7.0.0 @@ -409,13 +406,13 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-import-resolver-webpack: specifier: ^0.13.4 - version: 0.13.9(eslint-plugin-import@2.31.0)(webpack@5.95.0) + version: 0.13.9(eslint-plugin-import@2.31.0)(webpack@5.96.1) eslint-plugin-formatjs: specifier: ^5.0.0 - version: 5.1.0(eslint@8.57.1) + version: 5.2.2(eslint@8.57.1)(typescript@5.6.3) eslint-plugin-import: specifier: ^2.22.0 - version: 2.31.0(@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1) eslint-plugin-no-jquery: specifier: ^3.0.2 version: 3.0.2(eslint@8.57.1) @@ -445,7 +442,7 @@ importers: version: 3.3.3 puppeteer: specifier: ^23.5.0 - version: 23.5.3(typescript@5.6.3) + version: 23.7.0(typescript@5.6.3) source-map: specifier: npm:source-map-js@^1.2.1 version: source-map-js@1.2.1 @@ -463,16 +460,16 @@ importers: version: 10.0.3(openapi-types@12.1.3) ts-node: specifier: ^10.0.0 - version: 10.9.2(@types/node@20.16.11)(typescript@5.6.3) + version: 10.9.2(@types/node@22.9.0)(typescript@5.6.3) typescript: specifier: ^5.0.2 version: 5.6.3 vnu-jar: - specifier: ^23.4.11 - version: 23.4.11 + specifier: ^24.10.17 + version: 24.10.17 webpack-dev-server: specifier: ^5.0.2 - version: 5.1.0(webpack-cli@5.1.4)(webpack@5.95.0) + version: 5.1.0(webpack-cli@5.1.4)(webpack@5.96.1) xvfb: specifier: ^0.4.0 version: 0.4.0 @@ -484,7 +481,7 @@ importers: version: 17.7.2 zulip-js: specifier: ^2.0.8 - version: 2.0.9(encoding@0.1.13) + version: 2.1.0(encoding@0.1.13) help-beta: dependencies: @@ -493,10 +490,10 @@ importers: version: 0.9.4(prettier@3.3.3)(typescript@5.6.3) '@astrojs/starlight': specifier: ^0.28.2 - version: 0.28.3(astro@4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)) + version: 0.28.6(astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3)) astro: specifier: ^4.10.2 - version: 4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3) + version: 4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3) sharp: specifier: ^0.33.5 version: 0.33.5 @@ -540,8 +537,8 @@ packages: '@astrojs/internal-helpers@0.4.1': resolution: {integrity: sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g==} - '@astrojs/language-server@2.15.0': - resolution: {integrity: sha512-wJHSjGApm5X8Rg1GvkevoatZBfvaFizY4kCPvuSYgs3jGCobuY3KstJGKC1yNLsRJlDweHruP+J54iKn9vEKoA==} + '@astrojs/language-server@2.15.4': + resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} hasBin: true peerDependencies: prettier: ^3.0.0 @@ -555,8 +552,8 @@ packages: '@astrojs/markdown-remark@5.3.0': resolution: {integrity: sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg==} - '@astrojs/mdx@3.1.8': - resolution: {integrity: sha512-4o/+pvgoLFG0eG96cFs4t3NzZAIAOYu57fKAprWHXJrnq/qdBV0av6BYDjoESxvxNILUYoj8sdZVWtlPWVDLog==} + '@astrojs/mdx@3.1.9': + resolution: {integrity: sha512-3jPD4Bff6lIA20RQoonnZkRtZ9T3i0HFm6fcDF7BMsKIZ+xBP2KXzQWiuGu62lrVCmU612N+SQVGl5e0fI+zWg==} engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} peerDependencies: astro: ^4.8.0 @@ -568,8 +565,8 @@ packages: '@astrojs/sitemap@3.2.1': resolution: {integrity: sha512-uxMfO8f7pALq0ADL6Lk68UV6dNYjJ2xGUzyjjVj60JLBs5a6smtlkBYv3tQ0DzoqwS7c9n4FUx5lgv0yPo/fgA==} - '@astrojs/starlight@0.28.3': - resolution: {integrity: sha512-GXXIPKSu5d50mLVtgI4jf6pb3FPQm8n4MI6ZXuQQqqnA0xg7PJQ76WFSVyrICeqM5fKABSqcBksp/glyEJes/A==} + '@astrojs/starlight@0.28.6': + resolution: {integrity: sha512-lY+rbRMIVxDGiXhS4lBuVrU2jTUezEt4QeTxUTHxfj2tuKBwquG7Jg+alON6l+uaV+anbOkFb001MMXZF8X85w==} peerDependencies: astro: ^4.14.0 @@ -577,52 +574,52 @@ packages: resolution: {integrity: sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==} engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} - '@astrojs/yaml2ts@0.2.1': - resolution: {integrity: sha512-CBaNwDQJz20E5WxzQh4thLVfhB3JEEGz72wRA+oJp6fQR37QLAqXZJU0mHC+yqMOQ6oj0GfRPJrz6hjf+zm6zA==} + '@astrojs/yaml2ts@0.2.2': + resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} - '@babel/code-frame@7.25.7': - resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.8': - resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} engines: {node: '>=6.9.0'} - '@babel/core@7.25.8': - resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} - '@babel/eslint-parser@7.25.8': - resolution: {integrity: sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg==} + '@babel/eslint-parser@7.25.9': + resolution: {integrity: sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: '@babel/core': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - '@babel/generator@7.25.7': - resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.25.7': - resolution: {integrity: sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==} + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} - '@babel/helper-builder-binary-assignment-operator-visitor@7.25.7': - resolution: {integrity: sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==} + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': + resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.7': - resolution: {integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==} + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.25.7': - resolution: {integrity: sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==} + '@babel/helper-create-class-features-plugin@7.25.9': + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.25.7': - resolution: {integrity: sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==} + '@babel/helper-create-regexp-features-plugin@7.25.9': + resolution: {integrity: sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -632,103 +629,99 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - '@babel/helper-member-expression-to-functions@7.25.7': - resolution: {integrity: sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==} + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.25.7': - resolution: {integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.25.7': - resolution: {integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==} + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.25.7': - resolution: {integrity: sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==} + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.25.7': - resolution: {integrity: sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==} + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} engines: {node: '>=6.9.0'} - '@babel/helper-remap-async-to-generator@7.25.7': - resolution: {integrity: sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==} + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-replace-supers@7.25.7': - resolution: {integrity: sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==} + '@babel/helper-replace-supers@7.25.9': + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-simple-access@7.25.7': - resolution: {integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==} + '@babel/helper-simple-access@7.25.9': + resolution: {integrity: sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==} engines: {node: '>=6.9.0'} - '@babel/helper-skip-transparent-expression-wrappers@7.25.7': - resolution: {integrity: sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==} + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.25.7': - resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.7': - resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.25.7': - resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.25.7': - resolution: {integrity: sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==} + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.25.7': - resolution: {integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==} + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.25.7': - resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.25.8': - resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==} + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.7': - resolution: {integrity: sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==} + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.7': - resolution: {integrity: sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==} + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.7': - resolution: {integrity: sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==} + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.7': - resolution: {integrity: sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==} + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.7': - resolution: {integrity: sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -739,26 +732,26 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.25.7': - resolution: {integrity: sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==} + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.25.7': - resolution: {integrity: sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==} + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.25.7': - resolution: {integrity: sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==} + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.25.7': - resolution: {integrity: sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==} + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -769,314 +762,320 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-arrow-functions@7.25.7': - resolution: {integrity: sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==} + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.25.8': - resolution: {integrity: sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA==} + '@babel/plugin-transform-async-generator-functions@7.25.9': + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.25.7': - resolution: {integrity: sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg==} + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.25.7': - resolution: {integrity: sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==} + '@babel/plugin-transform-block-scoped-functions@7.25.9': + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.25.7': - resolution: {integrity: sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==} + '@babel/plugin-transform-block-scoping@7.25.9': + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.25.7': - resolution: {integrity: sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==} + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.25.8': - resolution: {integrity: sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==} + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.25.7': - resolution: {integrity: sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==} + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.25.7': - resolution: {integrity: sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==} + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.25.7': - resolution: {integrity: sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==} + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-dotall-regex@7.25.7': - resolution: {integrity: sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==} + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-keys@7.25.7': - resolution: {integrity: sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==} + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.7': - resolution: {integrity: sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-dynamic-import@7.25.8': - resolution: {integrity: sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==} + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.25.7': - resolution: {integrity: sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==} + '@babel/plugin-transform-exponentiation-operator@7.25.9': + resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-export-namespace-from@7.25.8': - resolution: {integrity: sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==} + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.25.7': - resolution: {integrity: sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==} + '@babel/plugin-transform-for-of@7.25.9': + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.25.7': - resolution: {integrity: sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==} + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-json-strings@7.25.8': - resolution: {integrity: sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==} + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-literals@7.25.7': - resolution: {integrity: sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==} + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.25.8': - resolution: {integrity: sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==} + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-member-expression-literals@7.25.7': - resolution: {integrity: sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==} + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-amd@7.25.7': - resolution: {integrity: sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==} + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-commonjs@7.25.7': - resolution: {integrity: sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==} + '@babel/plugin-transform-modules-commonjs@7.25.9': + resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.25.7': - resolution: {integrity: sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==} + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-umd@7.25.7': - resolution: {integrity: sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==} + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-named-capturing-groups-regex@7.25.7': - resolution: {integrity: sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==} + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-new-target@7.25.7': - resolution: {integrity: sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==} + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.25.8': - resolution: {integrity: sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==} + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9': + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-numeric-separator@7.25.8': - resolution: {integrity: sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==} + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.25.8': - resolution: {integrity: sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==} + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-super@7.25.7': - resolution: {integrity: sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==} + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-catch-binding@7.25.8': - resolution: {integrity: sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==} + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.25.8': - resolution: {integrity: sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==} + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-parameters@7.25.7': - resolution: {integrity: sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==} + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.25.7': - resolution: {integrity: sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==} + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.25.8': - resolution: {integrity: sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==} + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-property-literals@7.25.7': - resolution: {integrity: sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==} + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx@7.25.7': - resolution: {integrity: sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q==} + '@babel/plugin-transform-react-jsx@7.25.9': + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.25.7': - resolution: {integrity: sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==} + '@babel/plugin-transform-regenerator@7.25.9': + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-reserved-words@7.25.7': - resolution: {integrity: sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.25.7': - resolution: {integrity: sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.25.7': - resolution: {integrity: sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.25.7': - resolution: {integrity: sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.25.7': - resolution: {integrity: sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.25.7': - resolution: {integrity: sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.25.7': - resolution: {integrity: sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.25.7': - resolution: {integrity: sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-property-regex@7.25.7': - resolution: {integrity: sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.25.7': - resolution: {integrity: sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-sets-regex@7.25.7': - resolution: {integrity: sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==} + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.25.8': - resolution: {integrity: sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==} + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.25.9': + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.25.9': + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.25.9': + resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.26.0': + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1086,32 +1085,32 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - '@babel/preset-typescript@7.25.7': - resolution: {integrity: sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==} + '@babel/preset-typescript@7.26.0': + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/register@7.25.7': - resolution: {integrity: sha512-qHTd2Rhn/rKhSUwdY6+n98FmwXN+N+zxSVx3zWqRe9INyvTpv+aQ5gDV2+43ACd3VtMBzPPljbb0gZb8u5ma6Q==} + '@babel/register@7.25.9': + resolution: {integrity: sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.25.7': - resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/template@7.25.7': - resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.7': - resolution: {integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==} + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.8': - resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} '@choojs/findup@0.2.1': @@ -1126,39 +1125,39 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/cascade-layer-name-parser@2.0.2': - resolution: {integrity: sha512-rRWNJ8n16okpQT+8RWEbPfSl8D9WVoDZGBfHkjYnMYWcC20RiMpu/iGeKqUl1hR+SQIKg6p/QJap5rZJaHtVOg==} + '@csstools/cascade-layer-name-parser@2.0.4': + resolution: {integrity: sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.2 - '@csstools/css-tokenizer': ^3.0.2 + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 '@csstools/color-helpers@5.0.1': resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} engines: {node: '>=18'} - '@csstools/css-calc@2.0.2': - resolution: {integrity: sha512-N70YZw+R6WDP9EEd5xAT3xd+SgZFZsllXR6kclq6U8e2thlakNpWCKhuOiWfCKU8HpeWOyL+2ArSX8uDszMytA==} + '@csstools/css-calc@2.0.4': + resolution: {integrity: sha512-8/iCd8lH10gKNsq5detnbGWiFd6PXK2wB8wjE6fHNNhtqvshyMrIJgffwRcw6yl/gzGTH+N1i+KRhjqHxqYTmg==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.2 - '@csstools/css-tokenizer': ^3.0.2 + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 - '@csstools/css-color-parser@3.0.3': - resolution: {integrity: sha512-mnOTQ6KbQ6GHfdVHVTNXffroW0r5P5531h73bIyEzWAScGjMPQi+1XYgAydYVaZiKeXlQ4GHG9dnBWq9h7xFIQ==} + '@csstools/css-color-parser@3.0.5': + resolution: {integrity: sha512-4Wo8raj9YF3PnZ5iGrAl+BSsk2MYBOEUS/X4k1HL9mInhyCVftEG02MywdvelXlwZGUF2XTQ0qj9Jd398mhqrw==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.2 - '@csstools/css-tokenizer': ^3.0.2 + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 - '@csstools/css-parser-algorithms@3.0.2': - resolution: {integrity: sha512-6tC/MnlEvs5suR4Ahef4YlBccJDHZuxGsAlxXmybWjZ5jPxlzLSMlRZ9mVHSRvlD+CmtE7+hJ+UQbfXrws/rUQ==} + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.2 + '@csstools/css-tokenizer': ^3.0.3 - '@csstools/css-tokenizer@3.0.2': - resolution: {integrity: sha512-IuTRcD53WHsXPCZ6W7ubfGqReTJ9Ra0yRRFmXYP/Re8hFYYfoIYIK4080X5luslVLWimhIeFq0hj09urVMQzTw==} + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} engines: {node: '>=18'} '@csstools/media-query-list-parser@3.0.1': @@ -1168,39 +1167,39 @@ packages: '@csstools/css-parser-algorithms': ^3.0.1 '@csstools/css-tokenizer': ^3.0.1 - '@csstools/media-query-list-parser@4.0.0': - resolution: {integrity: sha512-nUfbCGeqCju55Po8ujRNQ8DjuKYth5UcsDj5HsVzSfqnaFdpOwYCUAhRJ2grfwrXhb9+KuRXHQ6JHzaI0Qhu8Q==} + '@csstools/media-query-list-parser@4.0.2': + resolution: {integrity: sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.2 - '@csstools/css-tokenizer': ^3.0.2 + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 - '@csstools/postcss-cascade-layers@5.0.0': - resolution: {integrity: sha512-h+VunB3KXaoWTWEPBcdVk8Kz1eZ/CtDD+HXgKw5JLdbsViLEQdKUtFYH73VIQigdodng8s5DCrrwNQY7pnuWBA==} + '@csstools/postcss-cascade-layers@5.0.1': + resolution: {integrity: sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-color-function@4.0.3': - resolution: {integrity: sha512-dziWTvbyBsXze7Li+BemXyYX9yCf8udlGKB78evZismrBf7SNN6K5S0qE4sRQELKEkttugcGz0hwqyXilEhoUA==} + '@csstools/postcss-color-function@4.0.5': + resolution: {integrity: sha512-6dHr2NDsBMiZCPkGDi2qMfIbzV2kWV8Dh7SVb1FZGnN/r2TI4TSAkVF8rCG5L70yQZHMcQGB84yp8Zm+RGhoHA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-color-mix-function@3.0.3': - resolution: {integrity: sha512-L7v0pQlLC3VejShxn5bjrdo3GhhHExSVGB8CgZqIcED/W/eK9pKGxubyGivNcJQYl+iznBtTU3mFPMmOrLccBQ==} + '@csstools/postcss-color-mix-function@3.0.5': + resolution: {integrity: sha512-jgq0oGbit7TxWYP8y2hWWfV64xzcAgJk54PBYZ2fDrRgEDy1l5YMCrFawnn+5JETh/E1jjXPDFhFEYhwr3vA3g==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-content-alt-text@2.0.2': - resolution: {integrity: sha512-GzMdDJrNPAOq4XxGac5xv5Ae2pB3JjvYWIJhJPcE6g87Q38gXG1Daaqq55QUU8DnC+iVm8lrO/JGvSC2T4YBOA==} + '@csstools/postcss-content-alt-text@2.0.4': + resolution: {integrity: sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-exponential-functions@2.0.2': - resolution: {integrity: sha512-gSGeXEKse3U3lDzSXh9XE1DgdicMWolo+eyXN8nH96Vr5mWPd6jUwk6W+x8yRNwM5dDkoAE/HkYK6/WzSo9Jsw==} + '@csstools/postcss-exponential-functions@2.0.4': + resolution: {integrity: sha512-xmzFCGTkkLDs7q9vVaRGlnu8s51lRRJzHsaJ/nXmkQuyg0q7gh7rTbJ0bY5sSVet+KB7MTIxRXRUCl2tm7RODA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -1211,20 +1210,20 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-gamut-mapping@2.0.3': - resolution: {integrity: sha512-1mbYE41F3fluEdjExw70b339NVU62O6sz43mya5O+LultfZQdmY68qRsWT+rw85Imya9aeLCDgBHaxwgXf1Z/g==} + '@csstools/postcss-gamut-mapping@2.0.5': + resolution: {integrity: sha512-VQDayRhC/Mg1fuo8/4F43La5aROgvVyqtCqdNyGvRKi6L1+zXfwQ583nImi7k/gn2GNJH82Bf9mutTuT1GtXzA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-gradients-interpolation-method@5.0.3': - resolution: {integrity: sha512-TW+utpEOOn2HLlRZTEVNS8XBlG5bOcSNBanIKjPWnkmdgkFjcj1eIaEtWezpGX2hKJpkiwZeIEyP/UItWk6c3g==} + '@csstools/postcss-gradients-interpolation-method@5.0.5': + resolution: {integrity: sha512-l3ShDdAt/szbyBh3Jz27MRFt5WPAbnVCMsU7Vs7EbBxJQNgVDrcu1APBB2nPagDJOyhI6/IahuW7nb6grWVTpA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-hwb-function@4.0.3': - resolution: {integrity: sha512-HBeApQzk6UlqAAWbuXSiWmF0Xtc/hfMTESSbkRUpolXshuPkUaBWXflfQuoo6exv3MvID6iTmv11GZT1ZfADDQ==} + '@csstools/postcss-hwb-function@4.0.5': + resolution: {integrity: sha512-bPn/SQyiiYjWkwK2ykc7O9LliMR50YfUGukd6jQI2okHzB7NxNt/IS45tS1Muk7Hhf3B9Lbmg1Ofq36tBmM92Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -1241,14 +1240,14 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-is-pseudo-class@5.0.0': - resolution: {integrity: sha512-E/CjrT03BL06WmrjupnrT0VUBTvxJdoW1hRVeXFa9qatWtvcLLw0j8hP372G4A9PpSGEMXi3/AoHzPf7DNryCQ==} + '@csstools/postcss-is-pseudo-class@5.0.1': + resolution: {integrity: sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-light-dark-function@2.0.5': - resolution: {integrity: sha512-mSqqxuwlBg10YyErq2YYB71KtvWDueBYE9WAnC6B7GHU+z0ECcGf+sR9zxpvePGzesuBNDB+cp15cW2CvOyszA==} + '@csstools/postcss-light-dark-function@2.0.7': + resolution: {integrity: sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -1277,20 +1276,20 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-logical-viewport-units@3.0.2': - resolution: {integrity: sha512-oog7VobKvrS34oyUKslI6wCphtJxx0ldiA8RToPQ0HXPWNiXXSM7IbgwOTImJKTIUjo3eL7o5uuPxeu5MsnkvA==} + '@csstools/postcss-logical-viewport-units@3.0.3': + resolution: {integrity: sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-media-minmax@2.0.2': - resolution: {integrity: sha512-zodxyIwRNuro/SIjN+zrYeZCQJvMd1obPtsvmNxLRvk3FOM3KwuuX8GEev9if19OGlNVvJZIe9wH77c+jIbXzA==} + '@csstools/postcss-media-minmax@2.0.4': + resolution: {integrity: sha512-zgdBOCI9aKoy5GK9tb/3ve0pl7vH0HJg7rfQEWT3TZiIKh7XEWucDSTSwnwgdgtgz50UxrOfbK+C59M+u2fE2Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.2': - resolution: {integrity: sha512-9bEvSC8hIkdqHwehYIADcwC7/TvuJeb1hAw0STI7BMRVE57nFxHyXY+WzfLPXtmhpdFqGcKJIyQkDcenQI3Sow==} + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.4': + resolution: {integrity: sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -1307,8 +1306,8 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-oklab-function@4.0.3': - resolution: {integrity: sha512-BrhnL98OSpWt5EOMk5Hm+kL0kjA8BhBc9DGG0jYgww1GhWItn+L/McQ4WgHE2cm9+jSUE2OMy/31WvSRKhWpnQ==} + '@csstools/postcss-oklab-function@4.0.5': + resolution: {integrity: sha512-19bsJQFyJNSEhpaVq0Mq1E0HDXfx8qMHa/bR1MaHr1UD4DWvM2/J6YXb9OVGS7eFl92Y3c84Yggn9uFv13vsiQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -1319,20 +1318,20 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-relative-color-syntax@3.0.3': - resolution: {integrity: sha512-1VYBTdGiFSOFrlczaYcUNybCU3XZRL9DDY3ooYRkvweWJZas8dQVHi6vy9SUmxnk0vfGbMbrISXLOIHw4LjKDg==} + '@csstools/postcss-relative-color-syntax@3.0.5': + resolution: {integrity: sha512-5VrE4hAwv/ZpuL1Yo0ZGGFi1QPpIikp/rzz7LnpQ31ACQVRIA5/M9qZmJbRlZVsJ4bUFSQ3dq6kHSHrCt2uM6Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-scope-pseudo-class@4.0.0': - resolution: {integrity: sha512-+ZUOBtVMDcmHZcZqsP/jcNRriEILfWQflTI3tCTA+/RheXAg57VkFGyPDAilpQSqlCpxWLWG8VUFKFtZJPwuOg==} + '@csstools/postcss-scope-pseudo-class@4.0.1': + resolution: {integrity: sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - '@csstools/postcss-stepped-value-functions@4.0.2': - resolution: {integrity: sha512-AxLKGIV0zYIAkeN02fo4o/vcG39WEZjT9dXs78ajy87dM94OFNIu5huxqBgkFGKLiXhQIKBRxAF/MtJmuIWi8w==} + '@csstools/postcss-stepped-value-functions@4.0.4': + resolution: {integrity: sha512-JjShuWZkmIOT8EfI7lYjl7V5qM29LNDdnnSo5O7v/InJJHfeiQjtxyAaZzKGXzpkghPrCAcgLfJ+IyqTdXo7IA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -1343,8 +1342,8 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/postcss-trigonometric-functions@4.0.2': - resolution: {integrity: sha512-hQzJkTWNvHKGYa5ySpdex2K/ODX6bI3z8Pmdl3W/opRlaXMA7Xvq7Nagp31BTkr1ngzfnqTY9XNKEI2FqaO3fg==} + '@csstools/postcss-trigonometric-functions@4.0.4': + resolution: {integrity: sha512-nn+gWTZZlSnwbyUtGQCnvBXIx1TX+HVStvIm3221dWGQvp47bB5giMBbuAK4a/UJGBbfDQhGKEbYq++WWM1i1A==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -1355,11 +1354,11 @@ packages: peerDependencies: postcss: ^8.4 - '@csstools/selector-resolve-nested@2.0.0': - resolution: {integrity: sha512-oklSrRvOxNeeOW1yARd4WNCs/D09cQjunGZUgSq6vM8GpzFswN+8rBZyJA29YFZhOTQ6GFzxgLDNtVbt9wPZMA==} + '@csstools/selector-resolve-nested@3.0.0': + resolution: {integrity: sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==} engines: {node: '>=18'} peerDependencies: - postcss-selector-parser: ^6.1.0 + postcss-selector-parser: ^7.0.0 '@csstools/selector-specificity@2.2.0': resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} @@ -1373,6 +1372,12 @@ packages: peerDependencies: postcss-selector-parser: ^6.1.0 + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + '@csstools/utilities@2.0.0': resolution: {integrity: sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==} engines: {node: '>=18'} @@ -1617,14 +1622,14 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.1': - resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/eslintrc@2.1.4': @@ -1650,8 +1655,8 @@ packages: '@fontsource-variable/open-sans@5.1.0': resolution: {integrity: sha512-NuJdoORzM6qp+bf5j7dyo6MO8rBEEiSw8wiZv9t7sga0RIODgLLbBAMaZMaZiLwNbXcGPxhFcHcuDaQr1xj9kA==} - '@formatjs/cli@6.2.15': - resolution: {integrity: sha512-s31YblAseSVqgFvY2EoIZaaEycifR/CadvMj1WcNvFvHK+2Xn02OuSX1jiKM/Nx29hX2x8k0raFJ6PtnXZgjtQ==} + '@formatjs/cli@6.3.8': + resolution: {integrity: sha512-zXUkIdPE+L+nu5A6mgEsIuB5tX369C7FnqR1aRHj33zvj/G5fwuyJZiavZCdR/XAvDWflKza6/pMVC91fNlR/w==} engines: {node: '>= 16'} hasBin: true peerDependencies: @@ -1681,37 +1686,37 @@ packages: vue: optional: true - '@formatjs/ecma402-abstract@2.2.0': - resolution: {integrity: sha512-IpM+ev1E4QLtstniOE29W1rqH9eTdx5hQdNL8pzrflMj/gogfaoONZqL83LUeQScHAvyMbpqP5C9MzNf+fFwhQ==} + '@formatjs/ecma402-abstract@2.2.3': + resolution: {integrity: sha512-aElGmleuReGnk2wtYOzYFmNWYoiWWmf1pPPCYg0oiIQSJj0mjc4eUfzUXaSOJ4S8WzI/cLqnCTWjqz904FT2OQ==} - '@formatjs/fast-memoize@2.2.1': - resolution: {integrity: sha512-XS2RcOSyWxmUB7BUjj3mlPH0exsUzlf6QfhhijgI941WaJhVxXQ6mEWkdUFIdnKi3TuTYxRdelsgv3mjieIGIA==} + '@formatjs/fast-memoize@2.2.3': + resolution: {integrity: sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==} - '@formatjs/icu-messageformat-parser@2.7.10': - resolution: {integrity: sha512-wlQfqCZ7PURkUNL2+8VTEFavPovtADU/isSKLFvDbdFmV7QPZIYqFMkhklaDYgMyLSBJa/h2MVQ2aFvoEJhxgg==} + '@formatjs/icu-messageformat-parser@2.9.3': + resolution: {integrity: sha512-9L99QsH14XjOCIp4TmbT8wxuffJxGK8uLNO1zNhLtcZaVXvv626N0s4A2qgRCKG3dfYWx9psvGlFmvyVBa6u/w==} - '@formatjs/icu-skeleton-parser@1.8.4': - resolution: {integrity: sha512-LMQ1+Wk1QSzU4zpd5aSu7+w5oeYhupRwZnMQckLPRYhSjf2/8JWQ882BauY9NyHxs5igpuQIXZDgfkaH3PoATg==} + '@formatjs/icu-skeleton-parser@1.8.7': + resolution: {integrity: sha512-fI+6SmS2g7h3srfAKSWa5dwreU5zNEfon2uFo99OToiLF6yxGE+WikvFSbsvMAYkscucvVmTYNlWlaDPp0n5HA==} - '@formatjs/intl-displaynames@6.6.10': - resolution: {integrity: sha512-tUz5qT61og1WwMM0K1/p46J69gLl1YJbty8xhtbigDA9LhbBmW2ShDg4ld+aqJTwCq4WK3Sj0VlFCKvFYeY3rQ==} + '@formatjs/intl-displaynames@6.8.4': + resolution: {integrity: sha512-HDVNBspDAOW0yTWluWTPHX2fk/9iBO4oST4R96f/IUaPGsFtjsHrpakwc+XDRPa3U5RniSEU2z34ZY0W78+E6Q==} - '@formatjs/intl-listformat@7.5.9': - resolution: {integrity: sha512-HqtGkxUh2Uz0oGVTxHAvPZ3EGxc8+ol5+Bx7S9xB97d4PEJJd9oOgHrerIghHA0gtIjsNKBFUae3P0My+F6YUA==} + '@formatjs/intl-listformat@7.7.4': + resolution: {integrity: sha512-lipFspH2MZcoeXxR6WSR/Jy9unzJ/iT0w+gbL8vgv25Ap0S9cUtcDVAce4ECEKI1bDtAvEU3b6+9Dha27gAikA==} - '@formatjs/intl-localematcher@0.5.5': - resolution: {integrity: sha512-t5tOGMgZ/i5+ALl2/offNqAQq/lfUnKLEw0mXQI4N4bqpedhrSE+fyKLpwnd22sK0dif6AV+ufQcTsKShB9J1g==} + '@formatjs/intl-localematcher@0.5.7': + resolution: {integrity: sha512-GGFtfHGQVFe/niOZp24Kal5b2i36eE2bNL0xi9Sg/yd0TR8aLjcteApZdHmismP5QQax1cMnZM9yWySUUjJteA==} - '@formatjs/intl@2.10.8': - resolution: {integrity: sha512-eY8r8RMmrRTTkLdbNBOZLFGXN3OnrEmInaNt8s4msIVfo+xuLqAqvB3W1jevj0I9QjU6ueIP7tEk+1rj6Xbv5A==} + '@formatjs/intl@2.10.14': + resolution: {integrity: sha512-4CA1EO75i/mSMHdjwfpgRj3Rsdsm6WjALeu/nlzYhBmAPxGu/Ha5GIRHAet5SO05TnpmqxmEGOsskWqFm0IeoA==} peerDependencies: typescript: ^4.7 || 5 peerDependenciesMeta: typescript: optional: true - '@formatjs/ts-transformer@3.13.16': - resolution: {integrity: sha512-ZIV7KB2EQ5w9k7yrwSsdGdoOgqlXNd2sfG317pbJPHDgIo04sxoRzZPayCiNo7VWaRyqkVYUpME94rd54FDvuw==} + '@formatjs/ts-transformer@3.13.22': + resolution: {integrity: sha512-+Zfz0wZ6wkdQE2zePiIQWIf4dVWeJGFXjkZxoCwzqxXdDrRhyAsQm91kbdFRIcVrjILA6KV0gOz8X7OBbLP4fQ==} peerDependencies: ts-jest: '>=27' peerDependenciesMeta: @@ -1740,8 +1745,8 @@ packages: '@giphy/js-types@5.1.0': resolution: {integrity: sha512-BZYCDtYNRR7cUWkbDLB4wmm3qmWMsVCQdUiBNOfmZ3yAazCgygKJoDI/5Rq4CK5MBaOc5LVdF8viC2WtoBdaPA==} - '@giphy/js-util@5.1.0': - resolution: {integrity: sha512-CvmeJi9H6tj0jscL7xpRXBhNuTgkQkXo7PQZK+UbWMR3NmJ07JibYxrw02bLmBZHxw+4F0cGeZL3zf8u5d+o5A==} + '@giphy/js-util@5.2.0': + resolution: {integrity: sha512-Qt7pGh2cqiNmXLeWAgb459wK8+BuMLtIxTfg4ZksnPHPsLthiHT9hhzs2QhqUh7Pp/HOq+Cbv2etGDfnq+xiKA==} '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} @@ -1972,12 +1977,12 @@ packages: resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} engines: {node: '>=6.0.0'} - '@maplibre/maplibre-gl-style-spec@20.3.1': - resolution: {integrity: sha512-5ueL4UDitzVtceQ8J4kY+Px3WK+eZTsmGwha3MBKHKqiHvKrjWWwBCIl1K8BuJSc5OFh83uI8IFNoFvQxX2uUw==} + '@maplibre/maplibre-gl-style-spec@20.4.0': + resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==} hasBin: true - '@mdx-js/mdx@3.0.1': - resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} + '@mdx-js/mdx@3.1.0': + resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -2063,13 +2068,13 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@puppeteer/browsers@2.4.0': - resolution: {integrity: sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==} + '@puppeteer/browsers@2.4.1': + resolution: {integrity: sha512-0kdAbmic3J09I6dT8e9vE2JOCSt13wHCW5x/ly8TSt2bDtuIWe2TgLZZDHdcziw9AVCzflMAXCrVyRIhIs44Ng==} engines: {node: '>=18'} hasBin: true - '@rollup/pluginutils@5.1.2': - resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==} + '@rollup/pluginutils@5.1.3': + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -2077,132 +2082,142 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.24.0': - resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} + '@rollup/rollup-android-arm-eabi@4.24.4': + resolution: {integrity: sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.24.0': - resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==} + '@rollup/rollup-android-arm64@4.24.4': + resolution: {integrity: sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.24.0': - resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==} + '@rollup/rollup-darwin-arm64@4.24.4': + resolution: {integrity: sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.24.0': - resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==} + '@rollup/rollup-darwin-x64@4.24.4': + resolution: {integrity: sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': - resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} + '@rollup/rollup-freebsd-arm64@4.24.4': + resolution: {integrity: sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.24.4': + resolution: {integrity: sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.24.4': + resolution: {integrity: sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.24.0': - resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} + '@rollup/rollup-linux-arm-musleabihf@4.24.4': + resolution: {integrity: sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.24.0': - resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} + '@rollup/rollup-linux-arm64-gnu@4.24.4': + resolution: {integrity: sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.24.0': - resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} + '@rollup/rollup-linux-arm64-musl@4.24.4': + resolution: {integrity: sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': - resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.24.4': + resolution: {integrity: sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.24.0': - resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} + '@rollup/rollup-linux-riscv64-gnu@4.24.4': + resolution: {integrity: sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.24.0': - resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} + '@rollup/rollup-linux-s390x-gnu@4.24.4': + resolution: {integrity: sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.24.0': - resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} + '@rollup/rollup-linux-x64-gnu@4.24.4': + resolution: {integrity: sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.24.0': - resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} + '@rollup/rollup-linux-x64-musl@4.24.4': + resolution: {integrity: sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.24.0': - resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} + '@rollup/rollup-win32-arm64-msvc@4.24.4': + resolution: {integrity: sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.24.0': - resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==} + '@rollup/rollup-win32-ia32-msvc@4.24.4': + resolution: {integrity: sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.24.0': - resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==} + '@rollup/rollup-win32-x64-msvc@4.24.4': + resolution: {integrity: sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==} cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sentry-internal/browser-utils@8.34.0': - resolution: {integrity: sha512-4AcYOzPzD1tL5eSRQ/GpKv5enquZf4dMVUez99/Bh3va8qiJrNP55AcM7UzZ7WZLTqKygIYruJTU5Zu2SpEAPQ==} + '@sentry-internal/browser-utils@8.37.1': + resolution: {integrity: sha512-OSR/V5GCsSCG7iapWtXCT/y22uo3HlawdEgfM1NIKk1mkP15UyGQtGEzZDdih2H+SNuX1mp9jQLTjr5FFp1A5w==} engines: {node: '>=14.18'} - '@sentry-internal/feedback@8.34.0': - resolution: {integrity: sha512-aYSM2KPUs0FLPxxbJCFSwCYG70VMzlT04xepD1Y/tTlPPOja/02tSv2tyOdZbv8Uw7xslZs3/8Lhj74oYcTBxw==} + '@sentry-internal/feedback@8.37.1': + resolution: {integrity: sha512-Se25NXbSapgS2S+JssR5YZ48b3OY4UGmAuBOafgnMW91LXMxRNWRbehZuNUmjjHwuywABMxjgu+Yp5uJDATX+g==} engines: {node: '>=14.18'} - '@sentry-internal/replay-canvas@8.34.0': - resolution: {integrity: sha512-x8KhZcCDpbKHqFOykYXiamX6x0LRxv6N1OJHoH+XCrMtiDBZr4Yo30d/MaS6rjmKGMtSRij30v+Uq+YWIgxUrg==} + '@sentry-internal/replay-canvas@8.37.1': + resolution: {integrity: sha512-1JLAaPtn1VL5vblB0BMELFV0D+KUm/iMGsrl4/JpRm0Ws5ESzQl33DhXVv1IX/ZAbx9i14EjR7MG9+Hj70tieQ==} engines: {node: '>=14.18'} - '@sentry-internal/replay@8.34.0': - resolution: {integrity: sha512-EoMh9NYljNewZK1quY23YILgtNdGgrkzJ9TPsj6jXUG0LZ0Q7N7eFWd0xOEDBvFxrmI3cSXF1i4d1sBb+eyKRw==} + '@sentry-internal/replay@8.37.1': + resolution: {integrity: sha512-E/Plhisk/pXJjOdOU12sg8m/APTXTA21iEniidP6jW3/+O0tD/H/UovEqa4odNTqxPMa798xHQSQNt5loYiaLA==} engines: {node: '>=14.18'} - '@sentry/browser@8.34.0': - resolution: {integrity: sha512-3HHG2NXxzHq1lVmDy2uRjYjGNf9NsJsTPlOC70vbQdOb+S49EdH/XMPy+J3ruIoyv6Cu0LwvA6bMOM6rHZOgNQ==} + '@sentry/browser@8.37.1': + resolution: {integrity: sha512-5ym+iGiIpjIKKpMWi9S3/tXh9xneS+jqxwRTJqed3cb8i4ydfMAAP8sM3U8xMCWWABpWyIUW+fpewC0tkhE1aQ==} engines: {node: '>=14.18'} - '@sentry/core@8.34.0': - resolution: {integrity: sha512-adrXCTK/zsg5pJ67lgtZqdqHvyx6etMjQW3P82NgWdj83c8fb+zH+K79Z47pD4zQjX0ou2Ws5nwwi4wJbz4bfA==} + '@sentry/core@8.37.1': + resolution: {integrity: sha512-82csXby589iDupM3VgCHJeWZagUyEEaDnbFcoZ/Z91QX2Sjq8FcF5OsforoXjw09i0XTFqlkFAnQVpDBmMXcpQ==} engines: {node: '>=14.18'} - '@sentry/types@8.34.0': - resolution: {integrity: sha512-zLRc60CzohGCo6zNsNeQ9JF3SiEeRE4aDCP9fDDdIVCOKovS+mn1rtSip0qd0Vp2fidOu0+2yY0ALCz1A3PJSQ==} + '@sentry/types@8.37.1': + resolution: {integrity: sha512-ryMOTROLSLINKFEbHWvi7GigNrsQhsaScw2NddybJGztJQ5UhxIGESnxGxWCufBmWFDwd7+5u0jDPCVUJybp7w==} engines: {node: '>=14.18'} - '@sentry/utils@8.34.0': - resolution: {integrity: sha512-W1KoRlFUjprlh3t86DZPFxLfM6mzjRzshVfMY7vRlJFymBelJsnJ3A1lPeBZM9nCraOSiw6GtOWu6k5BAkiGIg==} + '@sentry/utils@8.37.1': + resolution: {integrity: sha512-Qtn2IfpII12K17txG/ZtTci35XYjYi4CxbQ3j7nXY7toGv/+MqPXwV5q2i9g94XaSXlE5Wy9/hoCZoZpZs/djA==} engines: {node: '>=14.18'} - '@shikijs/core@1.22.0': - resolution: {integrity: sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==} + '@shikijs/core@1.22.2': + resolution: {integrity: sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==} - '@shikijs/engine-javascript@1.22.0': - resolution: {integrity: sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==} + '@shikijs/engine-javascript@1.22.2': + resolution: {integrity: sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==} - '@shikijs/engine-oniguruma@1.22.0': - resolution: {integrity: sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==} + '@shikijs/engine-oniguruma@1.22.2': + resolution: {integrity: sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==} - '@shikijs/types@1.22.0': - resolution: {integrity: sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==} + '@shikijs/types@1.22.2': + resolution: {integrity: sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==} '@shikijs/vscode-textmate@9.3.0': resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} @@ -2305,6 +2320,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} @@ -2317,8 +2335,8 @@ packages: '@types/express-serve-static-core@4.19.6': resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} - '@types/express-serve-static-core@5.0.0': - resolution: {integrity: sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==} + '@types/express-serve-static-core@5.0.1': + resolution: {integrity: sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==} '@types/express@4.17.21': resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} @@ -2338,8 +2356,8 @@ packages: '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - '@types/http-assert@1.5.5': - resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==} + '@types/http-assert@1.5.6': + resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} '@types/http-errors@2.0.4': resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} @@ -2362,8 +2380,8 @@ packages: '@types/jquery.validation@1.17.0': resolution: {integrity: sha512-4cbCiPm6T4VXHsAaoYZ+HbqIIIbd6h+dyEAht9qIhjVrI7yxgkvFLCdbmYGgV6wtYg16dlRtxnfZIMhuVCB9gQ==} - '@types/jquery@3.5.31': - resolution: {integrity: sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==} + '@types/jquery@3.5.32': + resolution: {integrity: sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==} '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} @@ -2371,8 +2389,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json-stable-stringify@1.0.36': - resolution: {integrity: sha512-b7bq23s4fgBB76n34m2b3RBf6M369B0Z9uRR8aHTMd8kZISRkmDEpPD8hhpYvDFzr3bJCPES96cm3Q6qRNDbQw==} + '@types/json-stable-stringify@1.1.0': + resolution: {integrity: sha512-ESTsHWB72QQq+pjUFIbEz9uSCZppD31YrVkbt2rnUciTYEvcwN6uZIhX5JZeBHqRlFJ41x/7MewCs7E2Qux6Cg==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -2392,8 +2410,8 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.10': - resolution: {integrity: sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==} + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} '@types/mapbox__point-geometry@0.1.4': resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} @@ -2431,11 +2449,8 @@ packages: '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@18.19.55': - resolution: {integrity: sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==} - - '@types/node@20.16.11': - resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} + '@types/node@22.9.0': + resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2446,14 +2461,14 @@ packages: '@types/pbf@3.0.5': resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} - '@types/picomatch@2.3.4': - resolution: {integrity: sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==} + '@types/picomatch@3.0.1': + resolution: {integrity: sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==} - '@types/plotly.js@2.33.4': - resolution: {integrity: sha512-BzAbsJTiUQyALkkYx1D31YZ9YvcU2ag3LlE/iePMo19eDPvM30cbM2EFNIcu31n39EhXj/9G7800XLA8/rfApA==} + '@types/plotly.js@2.33.5': + resolution: {integrity: sha512-TSXtrlc/4Zz7FP8HyDjmhsFFZ9JlzRk0KdHxXieDno4yZB4Jm5ET873QH+qPm5iZMaRZAEJMOrs1AGgN7r4e4g==} - '@types/qs@6.9.16': - resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + '@types/qs@6.9.17': + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} @@ -2473,8 +2488,8 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/sizzle@2.3.8': - resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==} + '@types/sizzle@2.3.9': + resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} @@ -2506,8 +2521,8 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/ws@8.5.12': - resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -2518,8 +2533,8 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.9.0': - resolution: {integrity: sha512-Y1n621OCy4m7/vTXNlCbMVp87zSd7NH0L9cXD8aIpOaNlzeWxIK4+Q19A68gSmTNRZn92UjocVUWDthGxtqHFg==} + '@typescript-eslint/eslint-plugin@8.13.0': + resolution: {integrity: sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -2529,8 +2544,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.9.0': - resolution: {integrity: sha512-U+BLn2rqTTHnc4FL3FJjxaXptTxmf9sNftJK62XLz4+GxG3hLHm/SUNaaXP5Y4uTiuYoL5YLy4JBCJe3+t8awQ==} + '@typescript-eslint/parser@8.13.0': + resolution: {integrity: sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2539,16 +2554,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.5.0': - resolution: {integrity: sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==} + '@typescript-eslint/scope-manager@8.13.0': + resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.9.0': - resolution: {integrity: sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/type-utils@8.9.0': - resolution: {integrity: sha512-JD+/pCqlKqAk5961vxCluK+clkppHY07IbV3vett97KOV+8C6l+CPEPwpUuiMwgbOz/qrN3Ke4zzjqbT+ls+1Q==} + '@typescript-eslint/type-utils@8.13.0': + resolution: {integrity: sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -2556,16 +2567,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.5.0': - resolution: {integrity: sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==} + '@typescript-eslint/types@8.13.0': + resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.9.0': - resolution: {integrity: sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.5.0': - resolution: {integrity: sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==} + '@typescript-eslint/typescript-estree@8.13.0': + resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -2573,86 +2580,67 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.9.0': - resolution: {integrity: sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@8.5.0': - resolution: {integrity: sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==} + '@typescript-eslint/utils@8.13.0': + resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/utils@8.9.0': - resolution: {integrity: sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - - '@typescript-eslint/visitor-keys@8.5.0': - resolution: {integrity: sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.9.0': - resolution: {integrity: sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA==} + '@typescript-eslint/visitor-keys@8.13.0': + resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@uppy/companion-client@4.1.0': - resolution: {integrity: sha512-nQ8CQfZcYVBNtFQ6ePj7FDIq38DXlH0YpzP/91LR9gnDVISJKKUuvWfr6tPktj1lRw9FZV8jLmlMKT2ituVKiw==} + '@uppy/companion-client@4.1.1': + resolution: {integrity: sha512-MotCCdnyiLyw34LeyDd8NMxrjD6jvCJA8UCI8eiP2lpLjwJJYyCB2Z0VyO/Wn4yFdh5un2NKwQmtGE0AENHN6Q==} peerDependencies: - '@uppy/core': ^4.2.0 + '@uppy/core': ^4.2.3 - '@uppy/core@4.2.2': - resolution: {integrity: sha512-TfTXngDLHK+gNwbpt1tgKfQ0vQwa7V5ilAnD/VNT+6AGW+/dqGFLZbA6q8xKvVTZ2sUbwDMSWFtqem+G04AhNQ==} + '@uppy/core@4.2.3': + resolution: {integrity: sha512-JSZiTZksrIZeATAtq9+QxXiPl7snfA5HbCn8uL20WJJxLqSff5ctnKmjvj0QmtwctPli00YrryDvEhCMhgmP7g==} - '@uppy/drag-drop@4.0.3': - resolution: {integrity: sha512-k9CySaCNgRge0bZrntmLGNFi2qGVFbprP0oibK3k4FOjrCvbJyN+nNppHwpEbBOPfuh76H94j9zL0OplQVZhZA==} + '@uppy/drag-drop@4.0.4': + resolution: {integrity: sha512-Ibz+NI9OrAFJ9BmuA0LZT9BuKBpIltfsyUIGbMGbNKmtQ1AGGswwgQMnD93RQE0lHC7V1BeU9vG655lNok0CCQ==} peerDependencies: - '@uppy/core': ^4.2.2 + '@uppy/core': ^4.2.3 - '@uppy/progress-bar@4.0.0': - resolution: {integrity: sha512-hCUjlfGWHlvBPQDO5YBH/8HEr+3+ZEobTblBg0Wbn3ecJSiKkSRi0GkDVp3OMnwfqgK2wm8Ve+tR/5Gds7vE0A==} + '@uppy/progress-bar@4.0.1': + resolution: {integrity: sha512-4cJcGVk5OEXZJuw1s0rPHBzatf1bDX0Rqqo4N92bMcX31NKiICvfBct2GZcx5p1VaGHN0I/WUV7pZ0zW9fpg5Q==} peerDependencies: - '@uppy/core': ^4.0.0 + '@uppy/core': ^4.2.3 - '@uppy/store-default@4.1.0': - resolution: {integrity: sha512-z5VSc4PNXpAtrrUPg5hdKJO5Ul7u4ZYLyK+tYzvEgzgR4nLVZmpGzj/d4N90jXpUqEibWKXvevODEB5VlTLHzg==} + '@uppy/store-default@4.1.1': + resolution: {integrity: sha512-VP02Q44Cziw8mLH6v2txToqF2SwsNr+jSxpkvcC7/EaZhG26XnseTd3Ydv2wYxv7YALQY2xhF2/LCXZzzx4fYQ==} - '@uppy/tus@4.1.2': - resolution: {integrity: sha512-wa3Hv2L1hy5Amw1A+MzxFGzTqGvcYrcnCHIMFcQK8dLbxchzunJgMfJI6Wb3YF70LZDerQyYJWBqlhQ2RbrBmg==} + '@uppy/tus@4.1.3': + resolution: {integrity: sha512-oX6Krds1iygLsTbYnvkP7pnGZa2UeiHq0RRxm40IDtVcfeHnNS3lD84VDTYfwxdWzdvfezCHnWBkgNXC+mjWNw==} peerDependencies: - '@uppy/core': ^4.2.2 + '@uppy/core': ^4.2.3 - '@uppy/utils@6.0.3': - resolution: {integrity: sha512-GBVzyAIeVKNe/F3TT63rXR80MSL9ov/FG3BbApO+4wbIt4vai7xpOxGCeTXpW2JjEeOwEb50n1fn92zMCdV9Dg==} + '@uppy/utils@6.0.4': + resolution: {integrity: sha512-EIp1//8+cw7DHPGix8sTp1G1OVopJlC2+p9upKrXXrmvRLFM00n1Xcd2JIZRE89PsrXgQzWdGYeeosCzoPZB2w==} - '@volar/kit@2.4.6': - resolution: {integrity: sha512-OaMtpmLns6IYD1nOSd0NdG/F5KzJ7Jr4B7TLeb4byPzu+ExuuRVeO56Dn1C7Frnw6bGudUQd90cpQAmxdB+RlQ==} + '@volar/kit@2.4.8': + resolution: {integrity: sha512-HY+HTP9sSqj0St9j1N8l85YMu4w0GxCtelzkzZWuq2GVz0+QRYwlyc0mPH7749OknUAdtsdozBR5Ecez55Ncug==} peerDependencies: typescript: '*' - '@volar/language-core@2.4.6': - resolution: {integrity: sha512-FxUfxaB8sCqvY46YjyAAV6c3mMIq/NWQMVvJ+uS4yxr1KzOvyg61gAuOnNvgCvO4TZ7HcLExBEsWcDu4+K4E8A==} + '@volar/language-core@2.4.8': + resolution: {integrity: sha512-K/GxMOXGq997bO00cdFhTNuR85xPxj0BEEAy+BaqqayTmy9Tmhfgmq2wpJcVspRhcwfgPoE2/mEJa26emUhG/g==} - '@volar/language-server@2.4.6': - resolution: {integrity: sha512-ARIbMXapEUPj9UFbZqWqw/iZ+ZuxUcY+vY212+2uutZVo/jrdzhLPu2TfZd9oB9akX8XXuslinT3051DyHLLRA==} + '@volar/language-server@2.4.8': + resolution: {integrity: sha512-3Jd9Y+0Zhwi/zfdRxqoNrm7AxP6lgTsw4Ni9r6eCyWYGVsTnpVwGmlcbiZyDja6anoKZxnaeDatX1jkaHHWaRQ==} - '@volar/language-service@2.4.6': - resolution: {integrity: sha512-wNeEVBgBKgpP1MfMYPrgTf1K8nhOGEh3ac0+9n6ECyk2N03+j0pWCpQ2i99mRWT/POvo1PgizDmYFH8S67bZOA==} + '@volar/language-service@2.4.8': + resolution: {integrity: sha512-9y8X4cdUxXmy4s5HoB8jmOpDIZG7XVFu4iEFvouhZlJX2leCq0pbq5h7dhA+O8My0fne3vtE6cJ4t9nc+8UBZw==} - '@volar/source-map@2.4.6': - resolution: {integrity: sha512-Nsh7UW2ruK+uURIPzjJgF0YRGP5CX9nQHypA2OMqdM2FKy7rh+uv3XgPnWPw30JADbKvZ5HuBzG4gSbVDYVtiw==} + '@volar/source-map@2.4.8': + resolution: {integrity: sha512-jeWJBkC/WivdelMwxKkpFL811uH/jJ1kVxa+c7OvG48DXc3VrP7pplSWPP2W1dLMqBxD+awRlg55FQQfiup4cA==} - '@volar/typescript@2.4.6': - resolution: {integrity: sha512-NMIrA7y5OOqddL9VtngPWYmdQU03htNKFtAYidbYfWA0TOhyGVd9tfcP4TsLWQ+RBWDZCbBqsr8xzU0ZOxYTCQ==} + '@volar/typescript@2.4.8': + resolution: {integrity: sha512-6xkIYJ5xxghVBhVywMoPMidDDAFT1OoQeXwa27HSgJ6AiIKRe61RXLoik+14Z7r0JvnblXVsjsRLmCr42SGzqg==} '@vscode/emmet-helper@2.9.3': resolution: {integrity: sha512-rB39LHWWPQYYlYfpv9qCoZOVioPCftKXXqrsyqN1mTWZM6dTnONT63Db+03vgrBbHzJN45IrgS/AGxw9iiqfEw==} @@ -2736,6 +2724,7 @@ packages: '@xmldom/xmldom@0.7.13': resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} engines: {node: '>=10.0.0'} + deprecated: this version is no longer supported, please update to at least 0.8.* '@xmldom/xmldom@0.8.10': resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} @@ -2770,11 +2759,6 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2794,8 +2778,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true @@ -2869,10 +2853,6 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -2923,10 +2903,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - arr-union@3.1.0: - resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} - engines: {node: '>=0.10.0'} - array-bounds@1.0.1: resolution: {integrity: sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==} @@ -2981,10 +2957,6 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} - assign-symbols@1.0.0: - resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} - engines: {node: '>=0.10.0'} - ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -3002,8 +2974,8 @@ packages: peerDependencies: astro: ^4.0.0-beta || ^3.3.0 - astro@4.16.5: - resolution: {integrity: sha512-v8mKWcEPN7hkfyVsMFWV0F8UgL4GUJKT172KTB7dePO3yJb64HBwCckC0QnA60QQh3jK6+2xwyWEc5QvCeqjtg==} + astro@4.16.9: + resolution: {integrity: sha512-DFYzPZooVArKSGu969BBByUV44tJMVDPGKxgqWNFBaIrkvGljdVUqQSVwD+/iPYACoSkI8BRYvDMEBDkathIUQ==} engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -3047,8 +3019,8 @@ packages: babel-plugin-emotion@10.2.2: resolution: {integrity: sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==} - babel-plugin-formatjs@10.5.18: - resolution: {integrity: sha512-g54iGQ13Jskf9yT34vg6czBtOAIVfW7mVZ6qLsoLASPE2touq06vNjsNkA+CO29AnntdcxKnyVrpiwGB1Kdb2w==} + babel-plugin-formatjs@10.5.24: + resolution: {integrity: sha512-AgyeoTrGFVq6iQQfPvEUl7945uswFPduHKs0JvSSK0d3GXxUXE45wTQSD3JO706gTzl+aVC8BnfPXmQx2a9UGw==} babel-plugin-istanbul@7.0.0: resolution: {integrity: sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==} @@ -3103,8 +3075,8 @@ packages: bare-path@2.1.3: resolution: {integrity: sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==} - bare-stream@2.3.0: - resolution: {integrity: sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==} + bare-stream@2.3.2: + resolution: {integrity: sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A==} base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} @@ -3187,8 +3159,8 @@ packages: bricks.js@1.8.0: resolution: {integrity: sha512-XJsIGxoixpMDo/KoLXR+uQizFVGWNAQy1lLoIwXKxm6/Zpd9QQLSUd0otybbK7wjqX23ZvCXFxnIw+uCXJHo0A==} - browserslist@4.24.0: - resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3224,20 +3196,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - bytewise-core@1.2.3: - resolution: {integrity: sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==} - - bytewise@1.1.0: - resolution: {integrity: sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==} - cacache@16.1.3: resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3286,8 +3248,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001668: - resolution: {integrity: sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==} + caniuse-lite@1.0.30001677: + resolution: {integrity: sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==} canvas-fit@1.5.0: resolution: {integrity: sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==} @@ -3295,10 +3257,6 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3554,8 +3512,8 @@ packages: peerDependencies: webpack: ^5.1.0 - compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + compression@1.7.5: + resolution: {integrity: sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==} engines: {node: '>= 0.8.0'} concat-map@0.0.1: @@ -3608,11 +3566,11 @@ packages: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} - core-js-compat@3.38.1: - resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} - core-js@3.38.1: - resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==} + core-js@3.39.0: + resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3647,8 +3605,8 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - css-blank-pseudo@7.0.0: - resolution: {integrity: sha512-v9xXYGdm6LIn4iHEfu3egk/PM1g/yJr8uwTIj6E44kurv5dE/4y3QW7WdVmZ0PVnqfTuK+C0ClZcEEiaKWBL9Q==} + css-blank-pseudo@7.0.1: + resolution: {integrity: sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -3681,8 +3639,8 @@ packages: css-global-keywords@1.0.1: resolution: {integrity: sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==} - css-has-pseudo@7.0.0: - resolution: {integrity: sha512-vO6k9bBt4/eEZ2PeHmS2VXjJga5SBy6O1ESyaOkse5/lvp6piFqg8Sh5KTU7X33M7Uh/oqo+M3EeMktQrZoTCQ==} + css-has-pseudo@7.0.1: + resolution: {integrity: sha512-EOcoyJt+OsuKfCADgLT7gADZI5jMzIe/AeI6MeAYKiFBDmNmM7kk46DtSfMj5AohUJisqVzopBpnQTlvbyaBWg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -3750,8 +3708,8 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-tree@3.0.0: - resolution: {integrity: sha512-o88DVQ6GzsABn1+6+zo2ct801dBO5OASVyxbbvA2W20ue2puSh/VOuqUj90eUeMSX/xqGqBmOKiRQN7tJOuBXw==} + css-tree@3.0.1: + resolution: {integrity: sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} css-what@6.1.0: @@ -4014,8 +3972,8 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - devtools-protocol@0.0.1342118: - resolution: {integrity: sha512-75fMas7PkYNDTmDyb6PRJCH7ILmHLp+BhrZGeMsa4bCh40DTxgCz2NRy5UDzII4C5KuD0oBMZ9vXKhEl6UD/3w==} + devtools-protocol@0.0.1354347: + resolution: {integrity: sha512-BlmkSqV0V84E2WnEnoPnwyix57rQxAM5SKJjf4TbYOCGLAWtz8CDH8RIaGOjPgPCXo2Mce3kxSY497OySidY3Q==} diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} @@ -4113,8 +4071,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.38: - resolution: {integrity: sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==} + electron-to-chromium@1.5.51: + resolution: {integrity: sha512-kKeWV57KSS8jH4alKt/jKnvHPmJgBxXzGUSbMd4eQF+iOsVPl7bz2KUmu6eo80eMP8wVioTfTyTzdMgM15WXNg==} element-size@1.1.1: resolution: {integrity: sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==} @@ -4259,6 +4217,12 @@ packages: es6-weak-map@2.0.3: resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -4325,8 +4289,8 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-formatjs@5.1.0: - resolution: {integrity: sha512-IN3/MKq5XumXqgta7OnvixcEoQCyxLAsNVwzsOQIqQqughcHwkvv7JHiIurt+7V6IjBYDyfTiH7opZggR+t+7w==} + eslint-plugin-formatjs@5.2.2: + resolution: {integrity: sha512-EtLhaz2/6l86s/3cOo5SyNGYnsocStylO3KLIoMJI8Ch2LqejZsd06RR7wMpeHusIN9ogvTlJZV2M54mBlElKg==} peerDependencies: eslint: '9' @@ -4411,6 +4375,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + estree-util-to-js@2.0.0: resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} @@ -4471,10 +4438,6 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} - extend-shallow@3.0.2: - resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} - engines: {node: '>=0.10.0'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -4784,10 +4747,6 @@ packages: resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} engines: {node: '>= 14'} - get-value@2.0.6: - resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} - engines: {node: '>=0.10.0'} - github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -4850,8 +4809,8 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} - globals@15.11.0: - resolution: {integrity: sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==} + globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} engines: {node: '>=18'} globalthis@1.0.4: @@ -4950,10 +4909,6 @@ packages: has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -5098,8 +5053,8 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - html-webpack-plugin@5.6.0: - resolution: {integrity: sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==} + html-webpack-plugin@5.6.3: + resolution: {integrity: sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==} engines: {node: '>=10.13.0'} peerDependencies: '@rspack/core': 0.x || 1.x @@ -5177,8 +5132,8 @@ packages: resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} engines: {node: '>=10.18'} - i18next@23.16.0: - resolution: {integrity: sha512-Ni3CG6c14teOogY19YNRl+kYaE/Rb59khy0VyHVn4uOZ97E2E/Yziyi6r3C3s9+wacjdLZiq/LLYyx+Cgd+FCw==} + i18next@23.16.4: + resolution: {integrity: sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==} iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} @@ -5253,6 +5208,10 @@ packages: resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@5.0.0: + resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} + engines: {node: ^18.17.0 || >=20.5.0} + inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} @@ -5274,8 +5233,8 @@ packages: intersection-observer@0.11.0: resolution: {integrity: sha512-KZArj2QVnmdud9zTpKf279m2bbGfG+4/kn16UU0NL3pTVl52ZHiJ9IRNSsnn6jaHrL9EGLFM5eWjTx2fz/+zoQ==} - intl-messageformat@10.7.0: - resolution: {integrity: sha512-2P06M9jFTqJnEQzE072VGPjbAx6ZG1YysgopAwc8ui0ajSjtwX1MeQ6bXFXIzKcNENJTizKkcJIcZ0zlpl1zSg==} + intl-messageformat@10.7.6: + resolution: {integrity: sha512-IsMU/hqyy3FJwNJ0hxDfY2heJ7MteSuFvcnCebxRp67di4Fhx1gKKE+qS0bBwUF8yXkX9SsPUhLeX/B6h5SKUA==} ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} @@ -5358,10 +5317,6 @@ packages: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} - is-extendable@1.0.1: - resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} - engines: {node: '>=0.10.0'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -5467,9 +5422,6 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} - is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -5978,8 +5930,8 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} - markdown-table@3.0.3: - resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} math-log2@1.0.1: resolution: {integrity: sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==} @@ -5997,8 +5949,8 @@ packages: mdast-util-find-and-replace@3.0.1: resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} - mdast-util-from-markdown@2.0.1: - resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} mdast-util-gfm-autolink-literal@2.0.1: resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} @@ -6036,8 +5988,8 @@ packages: mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} - mdast-util-to-markdown@2.1.0: - resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} @@ -6048,8 +6000,8 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - mdn-data@2.10.0: - resolution: {integrity: sha512-qq7C3EtK3yJXMwz1zAab65pjl+UhohqMOctTgcqjLOWABqmwj+me02LSsCuEUxnst9X1lCBpoE0WArGKgdGDzw==} + mdn-data@2.12.1: + resolution: {integrity: sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==} media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} @@ -6231,8 +6183,8 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - mini-css-extract-plugin@2.9.1: - resolution: {integrity: sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==} + mini-css-extract-plugin@2.9.2: + resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 @@ -6353,8 +6305,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.0.7: - resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} + nanoid@5.0.8: + resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==} engines: {node: ^18 || >=20} hasBin: true @@ -6376,6 +6328,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -6552,8 +6508,8 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@8.1.0: - resolution: {integrity: sha512-GQEkNkH/GHOhPFXcqZs3IDahXEQcQxsSjEkK4KvEEST4t7eNzoMjxTzef+EZ+JluDEV+Raoi3WQ2CflnRdSVnQ==} + ora@8.1.1: + resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} engines: {node: '>=18'} ordered-read-streams@1.0.1: @@ -6607,8 +6563,8 @@ packages: resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} engines: {node: '>=16.17'} - p-timeout@6.1.2: - resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} + p-timeout@6.1.3: + resolution: {integrity: sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw==} engines: {node: '>=14.16'} p-try@2.2.0: @@ -6669,8 +6625,8 @@ packages: parse-unit@1.0.1: resolution: {integrity: sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==} - parse5@7.2.0: - resolution: {integrity: sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -6725,19 +6681,20 @@ packages: performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - pick-by-alias@1.2.0: resolution: {integrity: sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -6779,8 +6736,8 @@ packages: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} - postcss-attribute-case-insensitive@7.0.0: - resolution: {integrity: sha512-ETMUHIw67Kyv9Q81nden/NuJbRh+4/S963giXpfSLd5eaKK8kd1UdAHMVRV/NG/w/N6Cq8B0qZIZbZZWU/67+A==} + postcss-attribute-case-insensitive@7.0.1: + resolution: {integrity: sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -6797,8 +6754,8 @@ packages: peerDependencies: postcss: ^8.4.6 - postcss-color-functional-notation@7.0.3: - resolution: {integrity: sha512-mL3LVOwXr5sRX1N5so7AFCNciaYTNTxzXuv5bDoZ/JunV2NCAzGOuVfyICRKczDPFImoIuL4e0O33/zYap9D0w==} + postcss-color-functional-notation@7.0.5: + resolution: {integrity: sha512-zW97tq5t2sSSSZQcIS4y6NDZj79zVv8hrBIJ4PSFZFmMBcjYqCt8sRXFGIYZohCpfFHmimMNqJje2Qd3qqMNdg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -6827,26 +6784,26 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-custom-media@11.0.3: - resolution: {integrity: sha512-h52R7j0/QZP7NgnpsUaqx6wdssplK4U+ZuErvic2StgvXt3v5sPopFH86yjLvqz3jBrj/8Hkvr7Gio1LLRFP0g==} + postcss-custom-media@11.0.5: + resolution: {integrity: sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - postcss-custom-properties@14.0.2: - resolution: {integrity: sha512-ZDJLIXa6uT6FlK6mYdzHxr1fr5ec6lPbp/CZ5+7EZedFmfjJx1fvYQhAPCBebuyc1lkketmiA26ZVl2UkPQ9Ig==} + postcss-custom-properties@14.0.4: + resolution: {integrity: sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - postcss-custom-selectors@8.0.2: - resolution: {integrity: sha512-8y2fa+RgYHpVFtvR4h3/dHc7b0iWjT6GOpzWwB8VHJTEBdVNaqOB4FH9koa44hgRyaeDs3KTe3xP9EJf6NLvxQ==} + postcss-custom-selectors@8.0.4: + resolution: {integrity: sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - postcss-dir-pseudo-class@9.0.0: - resolution: {integrity: sha512-T59BG9lURiXmhcJMyKbyjNAK3KCyEQYEhaz9GAETHXfIy9XbGQeyz+H0zIwRJlrP4KKRPJolNYe3QjQPemMjBA==} + postcss-dir-pseudo-class@9.0.1: + resolution: {integrity: sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -6887,14 +6844,14 @@ packages: peerDependencies: postcss: ^8.4.6 - postcss-focus-visible@10.0.0: - resolution: {integrity: sha512-GJjzvTj7JY+zN7wVBQ4osdKX53QLUdr6r2rSEkBUqrEMDKu3fHMHKOY9rirdirbHCx3IETnK25EtpPARR2KWNw==} + postcss-focus-visible@10.0.1: + resolution: {integrity: sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - postcss-focus-within@9.0.0: - resolution: {integrity: sha512-QwflAWUToNZvQLGbc4qJhrQO8yZ5617L6hSNzNWDoqRX4FoIh9fbJbEjy0nvFPciaaOoCaeqcxBwYPbFU0HvBw==} + postcss-focus-within@9.0.1: + resolution: {integrity: sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -6922,8 +6879,8 @@ packages: peerDependencies: postcss: ^8.0.0 - postcss-lab-function@7.0.3: - resolution: {integrity: sha512-yCBscY/dwipfvqqy7rQHbn6k18zYZy9O57JY4fGuibot6wz7pbtzRnhRlWraHBNUs+N4p2KogHv2aBsoB6G+5Q==} + postcss-lab-function@7.0.5: + resolution: {integrity: sha512-q2M8CfQbjHxbwv1GPAny05EVuj0WByUgq/OWKgpfbTHnMchtUqsVQgaW1mztjSZ4UPufwuTLB14fmFGsoTE/VQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -7019,8 +6976,8 @@ packages: peerDependencies: postcss: ^8.2 - postcss-nesting@13.0.0: - resolution: {integrity: sha512-TCGQOizyqvEkdeTPM+t6NYwJ3EJszYE/8t8ILxw/YoeUvz2rz7aM8XTAmBWh9/DJjfaaabL88fWrsVHSPF2zgA==} + postcss-nesting@13.0.1: + resolution: {integrity: sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -7113,14 +7070,14 @@ packages: peerDependencies: postcss: '*' - postcss-preset-env@10.0.7: - resolution: {integrity: sha512-aUC/bMT2CULwaZ/RK1Ivzdsyv95DQCJs0dK98RTc9cZKUYIal1+85JdNwik0DXg35BKdRZM2ZwASU17PXoglsw==} + postcss-preset-env@10.0.9: + resolution: {integrity: sha512-mpfJWMAW6szov+ifW9HpNUUZE3BoXoHc4CDzNQHdH2I4CwsqulQ3bpFNUR6zh4tg0BUcqM7UUAbzG4UTel8QYw==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 - postcss-pseudo-class-any-link@10.0.0: - resolution: {integrity: sha512-bde8VE08Gq3ekKDq2BQ0ESOjNX54lrFDK3U9zABPINaqHblbZL/4Wfo5Y2vk6U64yVd/sjDwTzuiisFBpGNNIQ==} + postcss-pseudo-class-any-link@10.0.1: + resolution: {integrity: sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -7151,8 +7108,8 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-selector-not@8.0.0: - resolution: {integrity: sha512-g/juh7A83GWc3+kWL8BiS3YUIJb3XNqIVKz1kGvgN3OhoGCsPncy1qo/+q61tjy5r87OxBhSY1+hcH3yOhEW+g==} + postcss-selector-not@8.0.1: + resolution: {integrity: sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -7161,6 +7118,10 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} + postcss-selector-parser@7.0.0: + resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} + engines: {node: '>=4'} + postcss-simple-vars@7.0.1: resolution: {integrity: sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==} engines: {node: '>=14.0'} @@ -7297,12 +7258,12 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - puppeteer-core@23.5.3: - resolution: {integrity: sha512-V58MZD/B3CwkYsqSEQlHKbavMJptF04fzhMdUpiCRCmUVhwZNwSGEPhaiZ1f8I3ABQUirg3VNhXVB6Z1ubHXtQ==} + puppeteer-core@23.7.0: + resolution: {integrity: sha512-0kC81k3K6n6Upg/k04xv+Mi8yy62bNAJiK7LCA71zfq2XKEo9WAzas1t6UQiLgaNHtGNKM0d1KbR56p/+mgEiQ==} engines: {node: '>=18'} - puppeteer@23.5.3: - resolution: {integrity: sha512-FghmfBsr/UUpe48OiCg1gV3W4vVfQJKjQehbF07SjnQvEpWcvPTah1nykfGWdOQQ1ydJPIXcajzWN7fliCU3zw==} + puppeteer@23.7.0: + resolution: {integrity: sha512-YTgo0KFe8NtBcI9hCu/xsjPFumEhu8kA7QqLr6Uh79JcEsUcUt+go966NgKYXJ+P3Fuefrzn2SXwV3cyOe/UcQ==} engines: {node: '>=18'} hasBin: true @@ -7399,6 +7360,18 @@ packages: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.0: + resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + redent@4.0.0: resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} engines: {node: '>=12'} @@ -7416,8 +7389,8 @@ packages: regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - regex@4.3.3: - resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==} + regex@4.4.0: + resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} @@ -7438,8 +7411,8 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true - regjsparser@0.11.1: - resolution: {integrity: sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==} + regjsparser@0.11.2: + resolution: {integrity: sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==} hasBin: true regl-error2d@2.0.12: @@ -7469,6 +7442,9 @@ packages: rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-stringify@10.0.1: resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} @@ -7489,8 +7465,8 @@ packages: remark-gfm@4.0.0: resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} - remark-mdx@3.0.1: - resolution: {integrity: sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==} + remark-mdx@3.1.0: + resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -7617,8 +7593,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.24.0: - resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} + rollup@4.24.4: + resolution: {integrity: sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -7724,10 +7700,6 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} - set-value@2.0.1: - resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} - engines: {node: '>=0.10.0'} - setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} @@ -7759,8 +7731,8 @@ packages: shell-quote@1.8.1: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - shiki@1.22.0: - resolution: {integrity: sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==} + shiki@1.22.2: + resolution: {integrity: sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==} side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} @@ -7824,18 +7796,6 @@ packages: resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - sort-asc@0.2.0: - resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==} - engines: {node: '>=0.10.0'} - - sort-desc@0.2.0: - resolution: {integrity: sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==} - engines: {node: '>=0.10.0'} - - sort-object@3.0.3: - resolution: {integrity: sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==} - engines: {node: '>=0.10.0'} - sortablejs@1.15.3: resolution: {integrity: sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==} @@ -7896,10 +7856,6 @@ packages: spectrum-colorpicker@1.8.1: resolution: {integrity: sha512-x1picQ5giVso71ESII7jZ3+ZFdit8WthNkzwJqLNdPDPzrltKUQGpTohWyPfSAID+bK1zGdO6bDbSh1S6GoLYA==} - split-string@3.1.0: - resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} - engines: {node: '>=0.10.0'} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -8076,10 +8032,6 @@ packages: superscript-text@1.0.0: resolution: {integrity: sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==} - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -8177,8 +8129,8 @@ packages: uglify-js: optional: true - terser@5.34.1: - resolution: {integrity: sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==} + terser@5.36.0: + resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} engines: {node: '>=10'} hasBin: true @@ -8186,8 +8138,8 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - text-decoder@1.2.0: - resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} + text-decoder@1.2.1: + resolution: {integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==} text-field-edit@4.1.1: resolution: {integrity: sha512-bgvsU5CFdBm8YuDdpLALNqS7Th8bVaRD5cww9VMYaBYab324iN3XOXvuQxTxp9JSPhHHF6zGCDEyF08JUV2hPw==} @@ -8236,8 +8188,8 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@0.3.0: - resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} tinyqueue@2.0.3: resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} @@ -8248,21 +8200,17 @@ packages: tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} - tldts-core@6.1.51: - resolution: {integrity: sha512-bu9oCYYWC1iRjx+3UnAjqCsfrWNZV1ghNQf49b3w5xE8J/tNShHTzp5syWJfwGH+pxUgTTLUnzHnfuydW7wmbg==} + tldts-core@6.1.58: + resolution: {integrity: sha512-dR936xmhBm7AeqHIhCWwK765gZ7dFyL+IqLSFAjJbFlUXGMLCb8i2PzlzaOuWBuplBTaBYseSb565nk/ZEM0Bg==} - tldts@6.1.51: - resolution: {integrity: sha512-33lfQoL0JsDogIbZ8fgRyvv77GnRtwkNE/MOKocwUgPO1WrSfsq7+vQRKxRQZai5zd+zg97Iv9fpFQSzHyWdLA==} + tldts@6.1.58: + resolution: {integrity: sha512-MQJrJhjHOYGYb8DobR6Y4AdDbd4TYkyQ+KBDVc5ODzs1cbrvPpfN1IemYi9jfipJ/vR1YWvrDli0hg1y19VRoA==} hasBin: true to-absolute-glob@2.0.2: resolution: {integrity: sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==} engines: {node: '>=0.10.0'} - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - to-float32@1.1.0: resolution: {integrity: sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==} @@ -8316,8 +8264,8 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-api-utils@1.3.0: - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + ts-api-utils@1.4.0: + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' @@ -8349,8 +8297,8 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@2.7.0: - resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tsscmp@1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} @@ -8438,20 +8386,14 @@ packages: typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} - typescript-auto-import-cache@0.3.3: - resolution: {integrity: sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==} + typescript-auto-import-cache@0.3.5: + resolution: {integrity: sha512-fAIveQKsoYj55CozUiBoj4b/7WpN0i4o74wiGY5JVUEoD0XiqDk1tJqTEjgzL2/AizKQrXxyRosSebyDzBZKjw==} typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true - typewise-core@1.2.0: - resolution: {integrity: sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==} - - typewise@1.0.3: - resolution: {integrity: sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==} - uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -8470,9 +8412,6 @@ packages: underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -8498,10 +8437,6 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - union-value@1.0.1: - resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} - engines: {node: '>=0.10.0'} - unique-filename@2.0.1: resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -8658,8 +8593,8 @@ packages: resolution: {integrity: sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==} engines: {node: '>= 0.10'} - vite@5.4.9: - resolution: {integrity: sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==} + vite@5.4.10: + resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -8697,36 +8632,36 @@ packages: vite: optional: true - vnu-jar@23.4.11: - resolution: {integrity: sha512-lI5dzBYXtxhilNI7EeQ5iUduYnNBq7YWx4UjfBVLXfBQHnXYZSf3y3bpM0bSyDU6jy/+OyKV7nw4tzpR5lXSZg==} + vnu-jar@24.10.17: + resolution: {integrity: sha512-YT7gNrRY5PiJrI1GavlWRHWIwqq2o52COc6J9QeXPfoldKRiZ9BeGP4shNLLaVfi0naA+/LMksdYWkKCr4pnVg==} engines: {node: '>=0.10'} - volar-service-css@0.0.61: - resolution: {integrity: sha512-Ct9L/w+IB1JU8F4jofcNCGoHy6TF83aiapfZq9A0qYYpq+Kk5dH+ONS+rVZSsuhsunq8UvAuF8Gk6B8IFLfniw==} + volar-service-css@0.0.62: + resolution: {integrity: sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-emmet@0.0.61: - resolution: {integrity: sha512-iiYqBxjjcekqrRruw4COQHZME6EZYWVbkHjHDbULpml3g8HGJHzpAMkj9tXNCPxf36A+f1oUYjsvZt36qPg4cg==} + volar-service-emmet@0.0.62: + resolution: {integrity: sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-html@0.0.61: - resolution: {integrity: sha512-yFE+YmmgqIL5HI4ORqP++IYb1QaGcv+xBboI0WkCxJJ/M35HZj7f5rbT3eQ24ECLXFbFCFanckwyWJVz5KmN3Q==} + volar-service-html@0.0.62: + resolution: {integrity: sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-prettier@0.0.61: - resolution: {integrity: sha512-F612nql5I0IS8HxXemCGvOR2Uxd4XooIwqYVUvk7WSBxP/+xu1jYvE3QJ7EVpl8Ty3S4SxPXYiYTsG3bi+gzIQ==} + volar-service-prettier@0.0.62: + resolution: {integrity: sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w==} peerDependencies: '@volar/language-service': ~2.4.0 prettier: ^2.2 || ^3.0 @@ -8736,24 +8671,24 @@ packages: prettier: optional: true - volar-service-typescript-twoslash-queries@0.0.61: - resolution: {integrity: sha512-99FICGrEF0r1E2tV+SvprHPw9Knyg7BdW2fUch0tf59kG+KG+Tj4tL6tUg+cy8f23O/VXlmsWFMIE+bx1dXPnQ==} + volar-service-typescript-twoslash-queries@0.0.62: + resolution: {integrity: sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-typescript@0.0.61: - resolution: {integrity: sha512-4kRHxVbW7wFBHZWRU6yWxTgiKETBDIJNwmJUAWeP0mHaKpnDGj/astdRFKqGFRYVeEYl45lcUPhdJyrzanjsdQ==} + volar-service-typescript@0.0.62: + resolution: {integrity: sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-yaml@0.0.61: - resolution: {integrity: sha512-L+gbDiLDQQ1rZUbJ3mf3doDsoQUa8OZM/xdpk/unMg1Vz24Zmi2Ign8GrZyBD7bRoIQDwOH9gdktGDKzRPpUNw==} + volar-service-yaml@0.0.62: + resolution: {integrity: sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: @@ -8893,8 +8828,8 @@ packages: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} - webpack@5.95.0: - resolution: {integrity: sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==} + webpack@5.96.1: + resolution: {integrity: sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -9127,8 +9062,8 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - zod-to-json-schema@3.23.3: - resolution: {integrity: sha512-TYWChTxKQbRJp5ST22o/Irt9KC5nj7CdBKYB/AosCRdj/wxEMvv4NNaj9XVUHDOIp53ZxArGhnw5HMZziPFjog==} + zod-to-json-schema@3.23.5: + resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} peerDependencies: zod: ^3.23.3 @@ -9141,8 +9076,8 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - zulip-js@2.0.9: - resolution: {integrity: sha512-I8Cjnxa7qTaHwxN6YZ4IOL2IiTz89rD4NZul1t8Hzu+q8muSE4LT2iVAlnCrCut4KEbOZDA+Bsgp0/CtFkUbnA==} + zulip-js@2.1.0: + resolution: {integrity: sha512-kLdxzJZ/FvWHBotUJl7LXCHIkShTjy1FUk5HAWfsal1TM+hw0atCZwgasCpvFDBj01y+39ZEZXgjePaie74Xhg==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -9184,7 +9119,7 @@ snapshots: '@astrojs/check@0.9.4(prettier@3.3.3)(typescript@5.6.3)': dependencies: - '@astrojs/language-server': 2.15.0(prettier@3.3.3)(typescript@5.6.3) + '@astrojs/language-server': 2.15.4(prettier@3.3.3)(typescript@5.6.3) chokidar: 4.0.1 kleur: 4.1.5 typescript: 5.6.3 @@ -9197,24 +9132,24 @@ snapshots: '@astrojs/internal-helpers@0.4.1': {} - '@astrojs/language-server@2.15.0(prettier@3.3.3)(typescript@5.6.3)': + '@astrojs/language-server@2.15.4(prettier@3.3.3)(typescript@5.6.3)': dependencies: '@astrojs/compiler': 2.10.3 - '@astrojs/yaml2ts': 0.2.1 + '@astrojs/yaml2ts': 0.2.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@volar/kit': 2.4.6(typescript@5.6.3) - '@volar/language-core': 2.4.6 - '@volar/language-server': 2.4.6 - '@volar/language-service': 2.4.6 + '@volar/kit': 2.4.8(typescript@5.6.3) + '@volar/language-core': 2.4.8 + '@volar/language-server': 2.4.8 + '@volar/language-service': 2.4.8 fast-glob: 3.3.2 muggle-string: 0.4.1 - volar-service-css: 0.0.61(@volar/language-service@2.4.6) - volar-service-emmet: 0.0.61(@volar/language-service@2.4.6) - volar-service-html: 0.0.61(@volar/language-service@2.4.6) - volar-service-prettier: 0.0.61(@volar/language-service@2.4.6)(prettier@3.3.3) - volar-service-typescript: 0.0.61(@volar/language-service@2.4.6) - volar-service-typescript-twoslash-queries: 0.0.61(@volar/language-service@2.4.6) - volar-service-yaml: 0.0.61(@volar/language-service@2.4.6) + volar-service-css: 0.0.62(@volar/language-service@2.4.8) + volar-service-emmet: 0.0.62(@volar/language-service@2.4.8) + volar-service-html: 0.0.62(@volar/language-service@2.4.8) + volar-service-prettier: 0.0.62(@volar/language-service@2.4.8)(prettier@3.3.3) + volar-service-typescript: 0.0.62(@volar/language-service@2.4.8) + volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.8) + volar-service-yaml: 0.0.62(@volar/language-service@2.4.8) vscode-html-languageservice: 5.3.1 vscode-uri: 3.0.8 optionalDependencies: @@ -9236,7 +9171,7 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.1 remark-smartypants: 3.0.2 - shiki: 1.22.0 + shiki: 1.22.2 unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 @@ -9245,12 +9180,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@3.1.8(astro@4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3))': + '@astrojs/mdx@3.1.9(astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3))': dependencies: '@astrojs/markdown-remark': 5.3.0 - '@mdx-js/mdx': 3.0.1 - acorn: 8.12.1 - astro: 4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3) + '@mdx-js/mdx': 3.1.0(acorn@8.14.0) + acorn: 8.14.0 + astro: 4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3) es-module-lexer: 1.5.4 estree-util-visit: 2.0.0 gray-matter: 4.0.3 @@ -9275,23 +9210,24 @@ snapshots: stream-replace-string: 2.0.0 zod: 3.23.8 - '@astrojs/starlight@0.28.3(astro@4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3))': + '@astrojs/starlight@0.28.6(astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3))': dependencies: - '@astrojs/mdx': 3.1.8(astro@4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)) + '@astrojs/mdx': 3.1.9(astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3)) '@astrojs/sitemap': 3.2.1 '@pagefind/default-ui': 1.1.1 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - astro: 4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3) - astro-expressive-code: 0.35.6(astro@4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)) + astro: 4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3) + astro-expressive-code: 0.35.6(astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.3 hast-util-to-string: 3.0.1 hastscript: 9.0.0 - i18next: 23.16.0 + i18next: 23.16.4 + js-yaml: 4.1.0 mdast-util-directive: 3.0.0 - mdast-util-to-markdown: 2.1.0 + mdast-util-to-markdown: 2.1.2 mdast-util-to-string: 4.0.0 pagefind: 1.1.1 rehype: 13.0.2 @@ -9315,29 +9251,30 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/yaml2ts@0.2.1': + '@astrojs/yaml2ts@0.2.2': dependencies: yaml: 2.6.0 - '@babel/code-frame@7.25.7': + '@babel/code-frame@7.26.2': dependencies: - '@babel/highlight': 7.25.7 - picocolors: 1.1.0 + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@babel/compat-data@7.25.8': {} + '@babel/compat-data@7.26.2': {} - '@babel/core@7.25.8': + '@babel/core@7.26.0': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helpers': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/template': 7.25.7 - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 convert-source-map: 2.0.0 debug: 4.3.7 gensync: 1.0.0-beta.2 @@ -9346,695 +9283,694 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/eslint-parser@7.25.8(@babel/core@7.25.8)(eslint@8.57.1)': + '@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@8.57.1)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 eslint: 8.57.1 eslint-visitor-keys: 2.1.0 semver: 6.3.1 - '@babel/generator@7.25.7': + '@babel/generator@7.26.2': dependencies: - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.0.2 - '@babel/helper-annotate-as-pure@7.25.7': + '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 - '@babel/helper-builder-binary-assignment-operator-visitor@7.25.7': + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-compilation-targets@7.25.7': + '@babel/helper-compilation-targets@7.25.9': dependencies: - '@babel/compat-data': 7.25.8 - '@babel/helper-validator-option': 7.25.7 - browserslist: 4.24.0 + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.25.7(@babel/core@7.25.8)': + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-member-expression-to-functions': 7.25.7 - '@babel/helper-optimise-call-expression': 7.25.7 - '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.25.9 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.25.7(@babel/core@7.25.8)': + '@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 regexpu-core: 6.1.1 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.25.8)': + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 debug: 4.3.7 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: - supports-color - '@babel/helper-member-expression-to-functions@7.25.7': + '@babel/helper-member-expression-to-functions@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.25.7': + '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.25.7(@babel/core@7.25.8)': + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-imports': 7.25.7 - '@babel/helper-simple-access': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.25.7': + '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 - '@babel/helper-plugin-utils@7.25.7': {} + '@babel/helper-plugin-utils@7.25.9': {} - '@babel/helper-remap-async-to-generator@7.25.7(@babel/core@7.25.8)': + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-wrap-function': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.25.7(@babel/core@7.25.8)': + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-member-expression-to-functions': 7.25.7 - '@babel/helper-optimise-call-expression': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-simple-access@7.25.7': + '@babel/helper-simple-access@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.25.7': + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.25.7': {} + '@babel/helper-string-parser@7.25.9': {} - '@babel/helper-validator-identifier@7.25.7': {} + '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-option@7.25.7': {} + '@babel/helper-validator-option@7.25.9': {} - '@babel/helper-wrap-function@7.25.7': + '@babel/helper-wrap-function@7.25.9': dependencies: - '@babel/template': 7.25.7 - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helpers@7.25.7': + '@babel/helpers@7.26.0': dependencies: - '@babel/template': 7.25.7 - '@babel/types': 7.25.8 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 - '@babel/highlight@7.25.7': + '@babel/parser@7.26.2': dependencies: - '@babel/helper-validator-identifier': 7.25.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.1.0 + '@babel/types': 7.26.0 - '@babel/parser@7.25.8': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/types': 7.25.8 - - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.7(@babel/core@7.25.8)': - dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 - '@babel/plugin-transform-optional-chaining': 7.25.8(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 - '@babel/plugin-syntax-import-assertions@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-attributes@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-typescript@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.8)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-arrow-functions@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-async-generator-functions@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-remap-async-to-generator': 7.25.7(@babel/core@7.25.8) - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-imports': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-remap-async-to-generator': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-block-scoping@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-class-properties@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/template': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 - '@babel/plugin-transform-destructuring@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-dotall-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-duplicate-keys@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-dynamic-import@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-exponentiation-operator@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-export-namespace-from@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-for-of@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-literals@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-logical-assignment-operators@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-member-expression-literals@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-modules-amd@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-simple-access': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-simple-access': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-new-target@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-nullish-coalescing-operator@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-numeric-separator@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-object-rest-spread@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-transform-parameters': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-object-super@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-optional-chaining@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-private-methods@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-react-jsx@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-module-imports': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.8) - '@babel/types': 7.25.8 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-regenerator@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 regenerator-transform: 0.15.2 - '@babel/plugin-transform-reserved-words@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-shorthand-properties@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-spread@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-template-literals@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-typeof-symbol@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-typescript@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 - '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-property-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-sets-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/preset-env@7.25.8(@babel/core@7.25.8)': + '@babel/preset-env@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/compat-data': 7.25.8 - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-validator-option': 7.25.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8) - '@babel/plugin-syntax-import-assertions': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.8) - '@babel/plugin-transform-arrow-functions': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-async-generator-functions': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-async-to-generator': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-block-scoped-functions': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-block-scoping': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-class-properties': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-class-static-block': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-classes': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-computed-properties': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-destructuring': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-dotall-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-duplicate-keys': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-dynamic-import': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-exponentiation-operator': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-export-namespace-from': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-for-of': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-function-name': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-json-strings': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-logical-assignment-operators': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-member-expression-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-amd': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-commonjs': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-systemjs': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-umd': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-named-capturing-groups-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-new-target': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-nullish-coalescing-operator': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-numeric-separator': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-object-rest-spread': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-object-super': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-optional-catch-binding': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-optional-chaining': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-parameters': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-private-methods': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-private-property-in-object': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-property-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-regenerator': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-reserved-words': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-shorthand-properties': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-spread': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-sticky-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-template-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-typeof-symbol': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-escapes': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-property-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-sets-regex': 7.25.7(@babel/core@7.25.8) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.8) - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.25.8) - babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.25.8) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.25.8) - core-js-compat: 3.38.1 + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.26.0) + core-js-compat: 3.39.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.8)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/types': 7.25.8 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.0 esutils: 2.0.3 - '@babel/preset-typescript@7.25.7(@babel/core@7.25.8)': + '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-validator-option': 7.25.7 - '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-commonjs': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-typescript': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/register@7.25.7(@babel/core@7.25.8)': + '@babel/register@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 pirates: 4.0.6 source-map-support: 0.5.21 - '@babel/runtime@7.25.7': + '@babel/runtime@7.26.0': dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.25.7': + '@babel/template@7.25.9': dependencies: - '@babel/code-frame': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 - '@babel/traverse@7.25.7': + '@babel/traverse@7.25.9': dependencies: - '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/template': 7.25.7 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.25.8': + '@babel/types@7.26.0': dependencies: - '@babel/helper-string-parser': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - to-fast-properties: 2.0.0 + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 '@choojs/findup@0.2.1': dependencies: @@ -10046,78 +9982,78 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/cascade-layer-name-parser@2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2)': + '@csstools/cascade-layer-name-parser@2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/color-helpers@5.0.1': {} - '@csstools/css-calc@2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2)': + '@csstools/css-calc@2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 - '@csstools/css-color-parser@3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2)': + '@csstools/css-color-parser@3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/color-helpers': 5.0.1 - '@csstools/css-calc': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-calc': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 - '@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2)': + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-tokenizer': 3.0.3 - '@csstools/css-tokenizer@3.0.2': {} + '@csstools/css-tokenizer@3.0.3': {} - '@csstools/media-query-list-parser@3.0.1(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2)': + '@csstools/media-query-list-parser@3.0.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 - '@csstools/media-query-list-parser@4.0.0(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2)': + '@csstools/media-query-list-parser@4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 - '@csstools/postcss-cascade-layers@5.0.0(postcss@8.4.47)': + '@csstools/postcss-cascade-layers@5.0.1(postcss@8.4.47)': dependencies: - '@csstools/selector-specificity': 4.0.0(postcss-selector-parser@6.1.2) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.0.0) postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 - '@csstools/postcss-color-function@4.0.3(postcss@8.4.47)': + '@csstools/postcss-color-function@4.0.5(postcss@8.4.47)': dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 - '@csstools/postcss-color-mix-function@3.0.3(postcss@8.4.47)': + '@csstools/postcss-color-mix-function@3.0.5(postcss@8.4.47)': dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 - '@csstools/postcss-content-alt-text@2.0.2(postcss@8.4.47)': + '@csstools/postcss-content-alt-text@2.0.4(postcss@8.4.47)': dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 - '@csstools/postcss-exponential-functions@2.0.2(postcss@8.4.47)': + '@csstools/postcss-exponential-functions@2.0.4(postcss@8.4.47)': dependencies: - '@csstools/css-calc': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-calc': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 postcss: 8.4.47 '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.4.47)': @@ -10126,27 +10062,27 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@2.0.3(postcss@8.4.47)': + '@csstools/postcss-gamut-mapping@2.0.5(postcss@8.4.47)': dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 postcss: 8.4.47 - '@csstools/postcss-gradients-interpolation-method@5.0.3(postcss@8.4.47)': + '@csstools/postcss-gradients-interpolation-method@5.0.5(postcss@8.4.47)': dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 - '@csstools/postcss-hwb-function@4.0.3(postcss@8.4.47)': + '@csstools/postcss-hwb-function@4.0.5(postcss@8.4.47)': dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 @@ -10162,16 +10098,16 @@ snapshots: dependencies: postcss: 8.4.47 - '@csstools/postcss-is-pseudo-class@5.0.0(postcss@8.4.47)': + '@csstools/postcss-is-pseudo-class@5.0.1(postcss@8.4.47)': dependencies: - '@csstools/selector-specificity': 4.0.0(postcss-selector-parser@6.1.2) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.0.0) postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 - '@csstools/postcss-light-dark-function@2.0.5(postcss@8.4.47)': + '@csstools/postcss-light-dark-function@2.0.7(postcss@8.4.47)': dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 @@ -10193,25 +10129,25 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@3.0.2(postcss@8.4.47)': + '@csstools/postcss-logical-viewport-units@3.0.3(postcss@8.4.47)': dependencies: - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-tokenizer': 3.0.3 '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 - '@csstools/postcss-media-minmax@2.0.2(postcss@8.4.47)': + '@csstools/postcss-media-minmax@2.0.4(postcss@8.4.47)': dependencies: - '@csstools/css-calc': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 - '@csstools/media-query-list-parser': 4.0.0(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) + '@csstools/css-calc': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) postcss: 8.4.47 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.2(postcss@8.4.47)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.4(postcss@8.4.47)': dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 - '@csstools/media-query-list-parser': 4.0.0(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) postcss: 8.4.47 '@csstools/postcss-nested-calc@4.0.0(postcss@8.4.47)': @@ -10225,11 +10161,11 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@4.0.3(postcss@8.4.47)': + '@csstools/postcss-oklab-function@4.0.5(postcss@8.4.47)': dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 @@ -10239,25 +10175,25 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 - '@csstools/postcss-relative-color-syntax@3.0.3(postcss@8.4.47)': + '@csstools/postcss-relative-color-syntax@3.0.5(postcss@8.4.47)': dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 - '@csstools/postcss-scope-pseudo-class@4.0.0(postcss@8.4.47)': + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.4.47)': dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 - '@csstools/postcss-stepped-value-functions@4.0.2(postcss@8.4.47)': + '@csstools/postcss-stepped-value-functions@4.0.4(postcss@8.4.47)': dependencies: - '@csstools/css-calc': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-calc': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 postcss: 8.4.47 '@csstools/postcss-text-decoration-shorthand@4.0.1(postcss@8.4.47)': @@ -10266,20 +10202,20 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@4.0.2(postcss@8.4.47)': + '@csstools/postcss-trigonometric-functions@4.0.4(postcss@8.4.47)': dependencies: - '@csstools/css-calc': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-calc': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 postcss: 8.4.47 '@csstools/postcss-unset-value@4.0.0(postcss@8.4.47)': dependencies: postcss: 8.4.47 - '@csstools/selector-resolve-nested@2.0.0(postcss-selector-parser@6.1.2)': + '@csstools/selector-resolve-nested@3.0.0(postcss-selector-parser@7.0.0)': dependencies: - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2)': dependencies: @@ -10289,6 +10225,10 @@ snapshots: dependencies: postcss-selector-parser: 6.1.2 + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.0.0)': + dependencies: + postcss-selector-parser: 7.0.0 + '@csstools/utilities@2.0.0(postcss@8.4.47)': dependencies: postcss: 8.4.47 @@ -10330,13 +10270,13 @@ snapshots: '@emnapi/runtime@1.3.1': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 optional: true '@emotion/babel-plugin@11.12.0': dependencies: - '@babel/helper-module-imports': 7.25.7 - '@babel/runtime': 7.25.7 + '@babel/helper-module-imports': 7.25.9 + '@babel/runtime': 7.26.0 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.2 @@ -10364,7 +10304,7 @@ snapshots: '@emotion/weak-memoize': 0.4.0 stylis: 4.2.0 - '@emotion/css@11.9.0(@babel/core@7.25.8)': + '@emotion/css@11.9.0(@babel/core@7.26.0)': dependencies: '@emotion/babel-plugin': 11.12.0 '@emotion/cache': 11.13.1 @@ -10372,7 +10312,7 @@ snapshots: '@emotion/sheet': 1.4.0 '@emotion/utils': 1.4.1 optionalDependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 transitivePeerDependencies: - supports-color @@ -10487,12 +10427,12 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.1': {} + '@eslint-community/regexpp@4.12.1': {} '@eslint/eslintrc@2.1.4': dependencies: @@ -10529,7 +10469,7 @@ snapshots: '@expressive-code/plugin-shiki@0.35.6': dependencies: '@expressive-code/core': 0.35.6 - shiki: 1.22.0 + shiki: 1.22.2 '@expressive-code/plugin-text-markers@0.35.6': dependencies: @@ -10537,65 +10477,65 @@ snapshots: '@fontsource-variable/open-sans@5.1.0': {} - '@formatjs/cli@6.2.15': {} + '@formatjs/cli@6.3.8': {} - '@formatjs/ecma402-abstract@2.2.0': + '@formatjs/ecma402-abstract@2.2.3': dependencies: - '@formatjs/fast-memoize': 2.2.1 - '@formatjs/intl-localematcher': 0.5.5 - tslib: 2.7.0 + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/intl-localematcher': 0.5.7 + tslib: 2.8.1 - '@formatjs/fast-memoize@2.2.1': + '@formatjs/fast-memoize@2.2.3': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@2.7.10': + '@formatjs/icu-messageformat-parser@2.9.3': dependencies: - '@formatjs/ecma402-abstract': 2.2.0 - '@formatjs/icu-skeleton-parser': 1.8.4 - tslib: 2.7.0 + '@formatjs/ecma402-abstract': 2.2.3 + '@formatjs/icu-skeleton-parser': 1.8.7 + tslib: 2.8.1 - '@formatjs/icu-skeleton-parser@1.8.4': + '@formatjs/icu-skeleton-parser@1.8.7': dependencies: - '@formatjs/ecma402-abstract': 2.2.0 - tslib: 2.7.0 + '@formatjs/ecma402-abstract': 2.2.3 + tslib: 2.8.1 - '@formatjs/intl-displaynames@6.6.10': + '@formatjs/intl-displaynames@6.8.4': dependencies: - '@formatjs/ecma402-abstract': 2.2.0 - '@formatjs/intl-localematcher': 0.5.5 - tslib: 2.7.0 + '@formatjs/ecma402-abstract': 2.2.3 + '@formatjs/intl-localematcher': 0.5.7 + tslib: 2.8.1 - '@formatjs/intl-listformat@7.5.9': + '@formatjs/intl-listformat@7.7.4': dependencies: - '@formatjs/ecma402-abstract': 2.2.0 - '@formatjs/intl-localematcher': 0.5.5 - tslib: 2.7.0 + '@formatjs/ecma402-abstract': 2.2.3 + '@formatjs/intl-localematcher': 0.5.7 + tslib: 2.8.1 - '@formatjs/intl-localematcher@0.5.5': + '@formatjs/intl-localematcher@0.5.7': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 - '@formatjs/intl@2.10.8(typescript@5.6.3)': + '@formatjs/intl@2.10.14(typescript@5.6.3)': dependencies: - '@formatjs/ecma402-abstract': 2.2.0 - '@formatjs/fast-memoize': 2.2.1 - '@formatjs/icu-messageformat-parser': 2.7.10 - '@formatjs/intl-displaynames': 6.6.10 - '@formatjs/intl-listformat': 7.5.9 - intl-messageformat: 10.7.0 - tslib: 2.7.0 + '@formatjs/ecma402-abstract': 2.2.3 + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/icu-messageformat-parser': 2.9.3 + '@formatjs/intl-displaynames': 6.8.4 + '@formatjs/intl-listformat': 7.7.4 + intl-messageformat: 10.7.6 + tslib: 2.8.1 optionalDependencies: typescript: 5.6.3 - '@formatjs/ts-transformer@3.13.16': + '@formatjs/ts-transformer@3.13.22': dependencies: - '@formatjs/icu-messageformat-parser': 2.7.10 - '@types/json-stable-stringify': 1.0.36 - '@types/node': 18.19.55 + '@formatjs/icu-messageformat-parser': 2.9.3 + '@types/json-stable-stringify': 1.1.0 + '@types/node': 22.9.0 chalk: 4.1.2 json-stable-stringify: 1.1.1 - tslib: 2.7.0 + tslib: 2.8.1 typescript: 5.6.3 '@gar/promisify@1.1.3': {} @@ -10607,7 +10547,7 @@ snapshots: '@giphy/js-analytics@5.0.0': dependencies: '@giphy/js-types': 5.1.0 - '@giphy/js-util': 5.1.0 + '@giphy/js-util': 5.2.0 append-query: 2.1.1 throttle-debounce: 3.0.1 @@ -10617,14 +10557,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@giphy/js-components@5.13.0(@babel/core@7.25.8)': + '@giphy/js-components@5.13.0(@babel/core@7.26.0)': dependencies: - '@emotion/css': 11.9.0(@babel/core@7.25.8) + '@emotion/css': 11.9.0(@babel/core@7.26.0) '@giphy/js-analytics': 5.0.0 '@giphy/js-brand': 3.0.0 '@giphy/js-fetch-api': 5.6.0 '@giphy/js-types': 5.1.0 - '@giphy/js-util': 5.1.0 + '@giphy/js-util': 5.2.0 bricks.js: 1.8.0 intersection-observer: 0.11.0 preact: 10.4.8 @@ -10636,11 +10576,11 @@ snapshots: '@giphy/js-fetch-api@5.6.0': dependencies: '@giphy/js-types': 5.1.0 - '@giphy/js-util': 5.1.0 + '@giphy/js-util': 5.2.0 '@giphy/js-types@5.1.0': {} - '@giphy/js-util@5.1.0': + '@giphy/js-util@5.2.0': dependencies: '@giphy/js-types': 5.1.0 uuid: 9.0.1 @@ -10753,7 +10693,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -10786,21 +10726,21 @@ snapshots: '@jsdevtools/ono@7.1.3': {} - '@jsonjoy.com/base64@1.1.2(tslib@2.7.0)': + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 - '@jsonjoy.com/json-pack@1.1.0(tslib@2.7.0)': + '@jsonjoy.com/json-pack@1.1.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/base64': 1.1.2(tslib@2.7.0) - '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.5.0(tslib@2.8.1) hyperdyperid: 1.2.0 - thingies: 1.21.0(tslib@2.7.0) - tslib: 2.7.0 + thingies: 1.21.0(tslib@2.8.1) + tslib: 2.8.1 - '@jsonjoy.com/util@1.5.0(tslib@2.7.0)': + '@jsonjoy.com/util@1.5.0(tslib@2.8.1)': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 '@koa/bodyparser@5.1.1(koa@2.15.3)': dependencies: @@ -10840,7 +10780,7 @@ snapshots: '@mapbox/whoots-js@3.1.0': {} - '@maplibre/maplibre-gl-style-spec@20.3.1': + '@maplibre/maplibre-gl-style-spec@20.4.0': dependencies: '@mapbox/jsonlint-lines-primitives': 2.0.2 '@mapbox/unitbezier': 0.0.1 @@ -10848,10 +10788,9 @@ snapshots: minimist: 1.2.8 quickselect: 2.0.0 rw: 1.3.3 - sort-object: 3.0.3 tinyqueue: 3.0.0 - '@mdx-js/mdx@3.0.1': + '@mdx-js/mdx@3.1.0(acorn@8.14.0)': dependencies: '@types/estree': 1.0.6 '@types/estree-jsx': 1.0.5 @@ -10859,15 +10798,16 @@ snapshots: '@types/mdx': 2.0.13 collapse-white-space: 2.1.0 devlop: 1.1.0 - estree-util-build-jsx: 3.0.1 estree-util-is-identifier-name: 3.0.0 - estree-util-to-js: 2.0.0 + estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-estree: 3.1.0 hast-util-to-jsx-runtime: 2.3.2 markdown-extensions: 2.0.0 - periscopic: 3.1.0 - remark-mdx: 3.0.1 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.14.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 remark-parse: 11.0.0 remark-rehype: 11.1.1 source-map: 0.7.4 @@ -10877,6 +10817,7 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 transitivePeerDependencies: + - acorn - supports-color '@mixmark-io/domino@2.2.0': {} @@ -10987,7 +10928,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@puppeteer/browsers@2.4.0': + '@puppeteer/browsers@2.4.1': dependencies: debug: 4.3.7 extract-zip: 2.0.1 @@ -11000,132 +10941,138 @@ snapshots: transitivePeerDependencies: - supports-color - '@rollup/pluginutils@5.1.2(rollup@4.24.0)': + '@rollup/pluginutils@5.1.3(rollup@4.24.4)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 - picomatch: 2.3.1 + picomatch: 4.0.2 optionalDependencies: - rollup: 4.24.0 + rollup: 4.24.4 - '@rollup/rollup-android-arm-eabi@4.24.0': + '@rollup/rollup-android-arm-eabi@4.24.4': optional: true - '@rollup/rollup-android-arm64@4.24.0': + '@rollup/rollup-android-arm64@4.24.4': optional: true - '@rollup/rollup-darwin-arm64@4.24.0': + '@rollup/rollup-darwin-arm64@4.24.4': optional: true - '@rollup/rollup-darwin-x64@4.24.0': + '@rollup/rollup-darwin-x64@4.24.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': + '@rollup/rollup-freebsd-arm64@4.24.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.24.0': + '@rollup/rollup-freebsd-x64@4.24.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.24.0': + '@rollup/rollup-linux-arm-gnueabihf@4.24.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.24.0': + '@rollup/rollup-linux-arm-musleabihf@4.24.4': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': + '@rollup/rollup-linux-arm64-gnu@4.24.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.24.0': + '@rollup/rollup-linux-arm64-musl@4.24.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.24.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.24.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.24.0': + '@rollup/rollup-linux-riscv64-gnu@4.24.4': optional: true - '@rollup/rollup-linux-x64-musl@4.24.0': + '@rollup/rollup-linux-s390x-gnu@4.24.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.24.0': + '@rollup/rollup-linux-x64-gnu@4.24.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.24.0': + '@rollup/rollup-linux-x64-musl@4.24.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.24.0': + '@rollup/rollup-win32-arm64-msvc@4.24.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.24.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.24.4': optional: true '@rtsao/scc@1.1.0': {} - '@sentry-internal/browser-utils@8.34.0': + '@sentry-internal/browser-utils@8.37.1': dependencies: - '@sentry/core': 8.34.0 - '@sentry/types': 8.34.0 - '@sentry/utils': 8.34.0 + '@sentry/core': 8.37.1 + '@sentry/types': 8.37.1 + '@sentry/utils': 8.37.1 - '@sentry-internal/feedback@8.34.0': + '@sentry-internal/feedback@8.37.1': dependencies: - '@sentry/core': 8.34.0 - '@sentry/types': 8.34.0 - '@sentry/utils': 8.34.0 + '@sentry/core': 8.37.1 + '@sentry/types': 8.37.1 + '@sentry/utils': 8.37.1 - '@sentry-internal/replay-canvas@8.34.0': + '@sentry-internal/replay-canvas@8.37.1': dependencies: - '@sentry-internal/replay': 8.34.0 - '@sentry/core': 8.34.0 - '@sentry/types': 8.34.0 - '@sentry/utils': 8.34.0 + '@sentry-internal/replay': 8.37.1 + '@sentry/core': 8.37.1 + '@sentry/types': 8.37.1 + '@sentry/utils': 8.37.1 - '@sentry-internal/replay@8.34.0': + '@sentry-internal/replay@8.37.1': dependencies: - '@sentry-internal/browser-utils': 8.34.0 - '@sentry/core': 8.34.0 - '@sentry/types': 8.34.0 - '@sentry/utils': 8.34.0 + '@sentry-internal/browser-utils': 8.37.1 + '@sentry/core': 8.37.1 + '@sentry/types': 8.37.1 + '@sentry/utils': 8.37.1 - '@sentry/browser@8.34.0': + '@sentry/browser@8.37.1': dependencies: - '@sentry-internal/browser-utils': 8.34.0 - '@sentry-internal/feedback': 8.34.0 - '@sentry-internal/replay': 8.34.0 - '@sentry-internal/replay-canvas': 8.34.0 - '@sentry/core': 8.34.0 - '@sentry/types': 8.34.0 - '@sentry/utils': 8.34.0 + '@sentry-internal/browser-utils': 8.37.1 + '@sentry-internal/feedback': 8.37.1 + '@sentry-internal/replay': 8.37.1 + '@sentry-internal/replay-canvas': 8.37.1 + '@sentry/core': 8.37.1 + '@sentry/types': 8.37.1 + '@sentry/utils': 8.37.1 - '@sentry/core@8.34.0': + '@sentry/core@8.37.1': dependencies: - '@sentry/types': 8.34.0 - '@sentry/utils': 8.34.0 + '@sentry/types': 8.37.1 + '@sentry/utils': 8.37.1 - '@sentry/types@8.34.0': {} + '@sentry/types@8.37.1': {} - '@sentry/utils@8.34.0': + '@sentry/utils@8.37.1': dependencies: - '@sentry/types': 8.34.0 + '@sentry/types': 8.37.1 - '@shikijs/core@1.22.0': + '@shikijs/core@1.22.2': dependencies: - '@shikijs/engine-javascript': 1.22.0 - '@shikijs/engine-oniguruma': 1.22.0 - '@shikijs/types': 1.22.0 + '@shikijs/engine-javascript': 1.22.2 + '@shikijs/engine-oniguruma': 1.22.2 + '@shikijs/types': 1.22.2 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 hast-util-to-html: 9.0.3 - '@shikijs/engine-javascript@1.22.0': + '@shikijs/engine-javascript@1.22.2': dependencies: - '@shikijs/types': 1.22.0 + '@shikijs/types': 1.22.2 '@shikijs/vscode-textmate': 9.3.0 oniguruma-to-js: 0.4.3 - '@shikijs/engine-oniguruma@1.22.0': + '@shikijs/engine-oniguruma@1.22.2': dependencies: - '@shikijs/types': 1.22.0 + '@shikijs/types': 1.22.2 '@shikijs/vscode-textmate': 9.3.0 - '@shikijs/types@1.22.0': + '@shikijs/types@1.22.2': dependencies: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 @@ -11155,26 +11102,26 @@ snapshots: '@turf/helpers': 7.1.0 '@turf/meta': 7.1.0 '@types/geojson': 7946.0.14 - tslib: 2.7.0 + tslib: 2.8.1 '@turf/bbox@7.1.0': dependencies: '@turf/helpers': 7.1.0 '@turf/meta': 7.1.0 '@types/geojson': 7946.0.14 - tslib: 2.7.0 + tslib: 2.8.1 '@turf/centroid@7.1.0': dependencies: '@turf/helpers': 7.1.0 '@turf/meta': 7.1.0 '@types/geojson': 7946.0.14 - tslib: 2.7.0 + tslib: 2.8.1 '@turf/helpers@7.1.0': dependencies: '@types/geojson': 7946.0.14 - tslib: 2.7.0 + tslib: 2.8.1 '@turf/meta@7.1.0': dependencies: @@ -11183,7 +11130,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/acorn@4.0.6': dependencies: @@ -11193,15 +11140,15 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 '@types/babel__helper-plugin-utils@7.10.3': dependencies: @@ -11209,37 +11156,37 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 '@types/blueimp-md5@2.18.2': {} '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/bonjour@3.5.13': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/co-body@6.1.3': dependencies: - '@types/node': 20.16.11 - '@types/qs': 6.9.16 + '@types/node': 22.9.0 + '@types/qs': 6.9.17 '@types/connect-history-api-fallback@1.5.4': dependencies: - '@types/express-serve-static-core': 5.0.0 - '@types/node': 20.16.11 + '@types/express-serve-static-core': 5.0.1 + '@types/node': 22.9.0 '@types/connect@3.4.38': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/content-disposition@0.5.8': {} @@ -11250,12 +11197,17 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.0 '@types/keygrip': 1.0.6 - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.6 + '@types/eslint@9.6.1': dependencies: '@types/estree': 1.0.6 @@ -11269,15 +11221,15 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 20.16.11 - '@types/qs': 6.9.16 + '@types/node': 22.9.0 + '@types/qs': 6.9.17 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 - '@types/express-serve-static-core@5.0.0': + '@types/express-serve-static-core@5.0.1': dependencies: - '@types/node': 20.16.11 - '@types/qs': 6.9.16 + '@types/node': 22.9.0 + '@types/qs': 6.9.17 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -11285,14 +11237,14 @@ snapshots: dependencies: '@types/body-parser': 1.19.5 '@types/express-serve-static-core': 4.19.6 - '@types/qs': 6.9.16 + '@types/qs': 6.9.17 '@types/serve-static': 1.15.7 '@types/express@5.0.0': dependencies: '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 5.0.0 - '@types/qs': 6.9.16 + '@types/express-serve-static-core': 5.0.1 + '@types/qs': 6.9.17 '@types/serve-static': 1.15.7 '@types/geojson-vt@3.2.5': @@ -11307,13 +11259,13 @@ snapshots: '@types/html-minifier-terser@6.1.0': {} - '@types/http-assert@1.5.5': {} + '@types/http-assert@1.5.6': {} '@types/http-errors@2.0.4': {} '@types/http-proxy@1.17.15': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/is-url@1.2.32': {} @@ -11329,17 +11281,17 @@ snapshots: '@types/jquery.validation@1.17.0': dependencies: - '@types/jquery': 3.5.31 + '@types/jquery': 3.5.32 - '@types/jquery@3.5.31': + '@types/jquery@3.5.32': dependencies: - '@types/sizzle': 2.3.8 + '@types/sizzle': 2.3.9 '@types/js-cookie@3.0.6': {} '@types/json-schema@7.0.15': {} - '@types/json-stable-stringify@1.0.36': {} + '@types/json-stable-stringify@1.1.0': {} '@types/json5@0.0.29': {} @@ -11356,17 +11308,17 @@ snapshots: '@types/accepts': 1.3.7 '@types/content-disposition': 0.5.8 '@types/cookies': 0.9.0 - '@types/http-assert': 1.5.5 + '@types/http-assert': 1.5.6 '@types/http-errors': 2.0.4 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.10 + '@types/lodash': 4.17.13 - '@types/lodash@4.17.10': {} + '@types/lodash@4.17.13': {} '@types/mapbox__point-geometry@0.1.4': {} @@ -11398,15 +11350,11 @@ snapshots: '@types/node-forge@1.3.11': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/node@17.0.45': {} - '@types/node@18.19.55': - dependencies: - undici-types: 5.26.5 - - '@types/node@20.16.11': + '@types/node@22.9.0': dependencies: undici-types: 6.19.8 @@ -11416,11 +11364,11 @@ snapshots: '@types/pbf@3.0.5': {} - '@types/picomatch@2.3.4': {} + '@types/picomatch@3.0.1': {} - '@types/plotly.js@2.33.4': {} + '@types/plotly.js@2.33.5': {} - '@types/qs@6.9.16': {} + '@types/qs@6.9.17': {} '@types/range-parser@1.2.7': {} @@ -11428,12 +11376,12 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/serve-index@1.9.4': dependencies: @@ -11442,20 +11390,20 @@ snapshots: '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/send': 0.17.4 - '@types/sizzle@2.3.8': {} + '@types/sizzle@2.3.9': {} '@types/sockjs@0.3.36': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/sortablejs@1.15.8': {} '@types/spectrum@1.8.7': dependencies: - '@types/jquery': 3.5.31 + '@types/jquery': 3.5.32 '@types/tinycolor2': 1.4.6 '@types/supercluster@7.1.3': @@ -11474,9 +11422,9 @@ snapshots: '@types/unist@3.0.3': {} - '@types/ws@8.5.12': + '@types/ws@8.5.13': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 '@types/yargs-parser@21.0.3': {} @@ -11486,33 +11434,33 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 optional: true - '@typescript-eslint/eslint-plugin@8.9.0(@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@eslint-community/regexpp': 4.11.1 - '@typescript-eslint/parser': 8.9.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.9.0 - '@typescript-eslint/type-utils': 8.9.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/utils': 8.9.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.9.0 + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.13.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/type-utils': 8.13.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.13.0 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/scope-manager': 8.9.0 - '@typescript-eslint/types': 8.9.0 - '@typescript-eslint/typescript-estree': 8.9.0(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.9.0 + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.13.0 debug: 4.3.7 eslint: 8.57.1 optionalDependencies: @@ -11520,158 +11468,120 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.5.0': + '@typescript-eslint/scope-manager@8.13.0': dependencies: - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/visitor-keys': 8.5.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/visitor-keys': 8.13.0 - '@typescript-eslint/scope-manager@8.9.0': + '@typescript-eslint/type-utils@8.13.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.9.0 - '@typescript-eslint/visitor-keys': 8.9.0 - - '@typescript-eslint/type-utils@8.9.0(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@typescript-eslint/typescript-estree': 8.9.0(typescript@5.6.3) - '@typescript-eslint/utils': 8.9.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@8.57.1)(typescript@5.6.3) debug: 4.3.7 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color - '@typescript-eslint/types@8.5.0': {} + '@typescript-eslint/types@8.13.0': {} - '@typescript-eslint/types@8.9.0': {} - - '@typescript-eslint/typescript-estree@8.5.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@8.13.0(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/visitor-keys': 8.5.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/visitor-keys': 8.13.0 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.9.0(typescript@5.6.3)': + '@typescript-eslint/utils@8.13.0(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.9.0 - '@typescript-eslint/visitor-keys': 8.9.0 - debug: 4.3.7 - fast-glob: 3.3.2 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.5.0(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.5.0 - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.3) + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) eslint: 8.57.1 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/utils@8.9.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/visitor-keys@8.13.0': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.9.0 - '@typescript-eslint/types': 8.9.0 - '@typescript-eslint/typescript-estree': 8.9.0(typescript@5.6.3) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@8.5.0': - dependencies: - '@typescript-eslint/types': 8.5.0 - eslint-visitor-keys: 3.4.3 - - '@typescript-eslint/visitor-keys@8.9.0': - dependencies: - '@typescript-eslint/types': 8.9.0 + '@typescript-eslint/types': 8.13.0 eslint-visitor-keys: 3.4.3 '@ungap/structured-clone@1.2.0': {} - '@uppy/companion-client@4.1.0(@uppy/core@4.2.2)': + '@uppy/companion-client@4.1.1(@uppy/core@4.2.3)': dependencies: - '@uppy/core': 4.2.2 - '@uppy/utils': 6.0.3 + '@uppy/core': 4.2.3 + '@uppy/utils': 6.0.4 namespace-emitter: 2.0.1 p-retry: 6.2.0 - '@uppy/core@4.2.2': + '@uppy/core@4.2.3': dependencies: '@transloadit/prettier-bytes': 0.3.4 - '@uppy/store-default': 4.1.0 - '@uppy/utils': 6.0.3 + '@uppy/store-default': 4.1.1 + '@uppy/utils': 6.0.4 lodash: 4.17.21 mime-match: 1.0.2 namespace-emitter: 2.0.1 - nanoid: 5.0.7 + nanoid: 5.0.8 preact: 10.24.3 - '@uppy/drag-drop@4.0.3(@uppy/core@4.2.2)': + '@uppy/drag-drop@4.0.4(@uppy/core@4.2.3)': dependencies: - '@uppy/core': 4.2.2 - '@uppy/utils': 6.0.3 + '@uppy/core': 4.2.3 + '@uppy/utils': 6.0.4 preact: 10.24.3 - '@uppy/progress-bar@4.0.0(@uppy/core@4.2.2)': + '@uppy/progress-bar@4.0.1(@uppy/core@4.2.3)': dependencies: - '@uppy/core': 4.2.2 - '@uppy/utils': 6.0.3 + '@uppy/core': 4.2.3 + '@uppy/utils': 6.0.4 preact: 10.24.3 - '@uppy/store-default@4.1.0': {} + '@uppy/store-default@4.1.1': {} - '@uppy/tus@4.1.2(@uppy/core@4.2.2)': + '@uppy/tus@4.1.3(@uppy/core@4.2.3)': dependencies: - '@uppy/companion-client': 4.1.0(@uppy/core@4.2.2) - '@uppy/core': 4.2.2 - '@uppy/utils': 6.0.3 + '@uppy/companion-client': 4.1.1(@uppy/core@4.2.3) + '@uppy/core': 4.2.3 + '@uppy/utils': 6.0.4 tus-js-client: 4.2.3 - '@uppy/utils@6.0.3': + '@uppy/utils@6.0.4': dependencies: lodash: 4.17.21 preact: 10.24.3 - '@volar/kit@2.4.6(typescript@5.6.3)': + '@volar/kit@2.4.8(typescript@5.6.3)': dependencies: - '@volar/language-service': 2.4.6 - '@volar/typescript': 2.4.6 + '@volar/language-service': 2.4.8 + '@volar/typescript': 2.4.8 typesafe-path: 0.2.2 typescript: 5.6.3 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - '@volar/language-core@2.4.6': + '@volar/language-core@2.4.8': dependencies: - '@volar/source-map': 2.4.6 + '@volar/source-map': 2.4.8 - '@volar/language-server@2.4.6': + '@volar/language-server@2.4.8': dependencies: - '@volar/language-core': 2.4.6 - '@volar/language-service': 2.4.6 - '@volar/typescript': 2.4.6 + '@volar/language-core': 2.4.8 + '@volar/language-service': 2.4.8 + '@volar/typescript': 2.4.8 path-browserify: 1.0.1 request-light: 0.7.0 vscode-languageserver: 9.0.1 @@ -11679,18 +11589,18 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - '@volar/language-service@2.4.6': + '@volar/language-service@2.4.8': dependencies: - '@volar/language-core': 2.4.6 + '@volar/language-core': 2.4.8 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - '@volar/source-map@2.4.6': {} + '@volar/source-map@2.4.8': {} - '@volar/typescript@2.4.6': + '@volar/typescript@2.4.8': dependencies: - '@volar/language-core': 2.4.6 + '@volar/language-core': 2.4.8 path-browserify: 1.0.1 vscode-uri: 3.0.8 @@ -11796,22 +11706,22 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.95.0)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.96.1)': dependencies: - webpack: 5.95.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.95.0) + webpack: 5.96.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.96.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.95.0)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.96.1)': dependencies: - webpack: 5.95.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.95.0) + webpack: 5.96.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.96.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.95.0)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.96.1)': dependencies: - webpack: 5.95.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.95.0) + webpack: 5.96.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.96.1) optionalDependencies: - webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.95.0) + webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.96.1) '@xmldom/xmldom@0.7.13': {} @@ -11842,23 +11752,19 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-import-attributes@1.9.5(acorn@8.12.1): + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: - acorn: 8.12.1 - - acorn-jsx@5.3.2(acorn@8.12.1): - dependencies: - acorn: 8.12.1 + acorn: 8.14.0 acorn-walk@8.3.4: dependencies: - acorn: 8.12.1 + acorn: 8.14.0 acorn@7.4.1: {} acorn@8.11.3: {} - acorn@8.12.1: {} + acorn@8.14.0: {} agent-base@6.0.2: dependencies: @@ -11928,10 +11834,6 @@ snapshots: ansi-regex@6.1.0: {} - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -11976,8 +11878,6 @@ snapshots: aria-query@5.3.2: {} - arr-union@3.1.0: {} - array-bounds@1.0.1: {} array-buffer-byte-length@1.0.1: @@ -12046,35 +11946,33 @@ snapshots: arrify@1.0.1: {} - assign-symbols@1.0.0: {} - ast-types@0.13.4: dependencies: - tslib: 2.7.0 + tslib: 2.8.1 astral-regex@2.0.0: {} astring@1.9.0: {} - astro-expressive-code@0.35.6(astro@4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)): + astro-expressive-code@0.35.6(astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3)): dependencies: - astro: 4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3) + astro: 4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3) rehype-expressive-code: 0.35.6 - astro@4.16.5(@types/node@20.16.11)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3): + astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(terser@5.36.0)(typescript@5.6.3): dependencies: '@astrojs/compiler': 2.10.3 '@astrojs/internal-helpers': 0.4.1 '@astrojs/markdown-remark': 5.3.0 '@astrojs/telemetry': 3.1.0 - '@babel/core': 7.25.8 - '@babel/plugin-transform-react-jsx': 7.25.7(@babel/core@7.25.8) - '@babel/types': 7.25.8 + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.1.2(rollup@4.24.0) + '@rollup/pluginutils': 5.1.3(rollup@4.24.4) '@types/babel__core': 7.20.5 '@types/cookie': 0.6.0 - acorn: 8.12.1 + acorn: 8.14.0 aria-query: 5.3.2 axobject-query: 4.1.0 boxen: 8.0.1 @@ -12105,25 +12003,25 @@ snapshots: micromatch: 4.0.8 mrmime: 2.0.0 neotraverse: 0.6.18 - ora: 8.1.0 + ora: 8.1.1 p-limit: 6.1.0 p-queue: 8.0.1 preferred-pm: 4.0.0 prompts: 2.4.2 rehype: 13.0.2 semver: 7.6.3 - shiki: 1.22.0 - tinyexec: 0.3.0 + shiki: 1.22.2 + tinyexec: 0.3.1 tsconfck: 3.1.4(typescript@5.6.3) unist-util-visit: 5.0.0 vfile: 6.0.3 - vite: 5.4.9(@types/node@20.16.11)(terser@5.34.1) - vitefu: 1.0.3(vite@5.4.9(@types/node@20.16.11)(terser@5.34.1)) + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + vitefu: 1.0.3(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0)) which-pm: 3.0.0 xxhash-wasm: 1.0.2 yargs-parser: 21.1.1 zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) + zod-to-json-schema: 3.23.5(zod@3.23.8) zod-to-ts: 1.2.0(typescript@5.6.3)(zod@3.23.8) optionalDependencies: sharp: 0.33.5 @@ -12146,11 +12044,11 @@ snapshots: autoprefixer@10.4.20(postcss@8.4.47): dependencies: - browserslist: 4.24.0 - caniuse-lite: 1.0.30001668 + browserslist: 4.24.2 + caniuse-lite: 1.0.30001677 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.1.0 + picocolors: 1.1.1 postcss: 8.4.47 postcss-value-parser: 4.2.0 @@ -12166,16 +12064,16 @@ snapshots: b4a@1.6.7: {} - babel-loader@9.2.1(@babel/core@7.25.8)(webpack@5.95.0): + babel-loader@9.2.1(@babel/core@7.26.0)(webpack@5.96.1): dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 find-cache-dir: 4.0.0 schema-utils: 4.2.0 - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) babel-plugin-emotion@10.2.2: dependencies: - '@babel/helper-module-imports': 7.25.7 + '@babel/helper-module-imports': 7.25.9 '@emotion/hash': 0.8.0 '@emotion/memoize': 0.7.4 '@emotion/serialize': 0.11.16 @@ -12188,26 +12086,26 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-formatjs@10.5.18: + babel-plugin-formatjs@10.5.24: dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.8) - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 - '@formatjs/icu-messageformat-parser': 2.7.10 - '@formatjs/ts-transformer': 3.13.16 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + '@formatjs/icu-messageformat-parser': 2.9.3 + '@formatjs/ts-transformer': 3.13.22 '@types/babel__core': 7.20.5 '@types/babel__helper-plugin-utils': 7.10.3 '@types/babel__traverse': 7.20.6 - tslib: 2.7.0 + tslib: 2.8.1 transitivePeerDependencies: - supports-color - ts-jest babel-plugin-istanbul@7.0.0: dependencies: - '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-plugin-utils': 7.25.9 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 6.0.3 @@ -12217,37 +12115,37 @@ snapshots: babel-plugin-macros@2.8.0: dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.26.0 cosmiconfig: 6.0.0 resolve: 1.22.8 babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.26.0 cosmiconfig: 7.1.0 resolve: 1.22.8 - babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.25.8): + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.26.0): dependencies: - '@babel/compat-data': 7.25.8 - '@babel/core': 7.25.8 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.25.8): + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): dependencies: - '@babel/core': 7.25.8 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) - core-js-compat: 3.38.1 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) + core-js-compat: 3.39.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.25.8): + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.26.0): dependencies: - '@babel/core': 7.25.8 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) transitivePeerDependencies: - supports-color @@ -12268,7 +12166,7 @@ snapshots: dependencies: bare-events: 2.5.0 bare-path: 2.1.3 - bare-stream: 2.3.0 + bare-stream: 2.3.2 optional: true bare-os@2.4.4: @@ -12279,9 +12177,8 @@ snapshots: bare-os: 2.4.4 optional: true - bare-stream@2.3.0: + bare-stream@2.3.2: dependencies: - b4a: 1.6.7 streamx: 2.20.1 optional: true @@ -12380,12 +12277,12 @@ snapshots: dependencies: knot.js: 1.1.5 - browserslist@4.24.0: + browserslist@4.24.2: dependencies: - caniuse-lite: 1.0.30001668 - electron-to-chromium: 1.5.38 + caniuse-lite: 1.0.30001677 + electron-to-chromium: 1.5.51 node-releases: 2.0.18 - update-browserslist-db: 1.1.1(browserslist@4.24.0) + update-browserslist-db: 1.1.1(browserslist@4.24.2) buffer-crc32@0.2.13: {} @@ -12420,19 +12317,8 @@ snapshots: dependencies: run-applescript: 7.0.0 - bytes@3.0.0: {} - bytes@3.1.2: {} - bytewise-core@1.2.3: - dependencies: - typewise-core: 1.2.0 - - bytewise@1.1.0: - dependencies: - bytewise-core: 1.2.3 - typewise: 1.0.3 - cacache@16.1.3: dependencies: '@npmcli/fs': 2.1.2 @@ -12483,7 +12369,7 @@ snapshots: camel-case@4.1.2: dependencies: pascal-case: 3.1.2 - tslib: 2.7.0 + tslib: 2.8.1 camelcase-keys@7.0.2: dependencies: @@ -12502,12 +12388,12 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.24.0 - caniuse-lite: 1.0.30001668 + browserslist: 4.24.2 + caniuse-lite: 1.0.30001677 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001668: {} + caniuse-lite@1.0.30001677: {} canvas-fit@1.5.0: dependencies: @@ -12515,12 +12401,6 @@ snapshots: ccount@2.0.1: {} - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -12556,9 +12436,9 @@ snapshots: chrome-trace-event@1.0.4: {} - chromium-bidi@0.8.0(devtools-protocol@0.0.1342118): + chromium-bidi@0.8.0(devtools-protocol@0.0.1354347): dependencies: - devtools-protocol: 0.0.1342118 + devtools-protocol: 0.0.1354347 mitt: 3.0.1 urlpattern-polyfill: 10.0.0 zod: 3.23.8 @@ -12760,20 +12640,20 @@ snapshots: dependencies: mime-db: 1.53.0 - compression-webpack-plugin@11.1.0(webpack@5.95.0): + compression-webpack-plugin@11.1.0(webpack@5.96.1): dependencies: schema-utils: 4.2.0 serialize-javascript: 6.0.2 - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) - compression@1.7.4: + compression@1.7.5: dependencies: - accepts: 1.3.8 - bytes: 3.0.0 + bytes: 3.1.2 compressible: 2.0.18 debug: 2.6.9 + negotiator: 0.6.4 on-headers: 1.0.2 - safe-buffer: 5.1.2 + safe-buffer: 5.2.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -12821,11 +12701,11 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - core-js-compat@3.38.1: + core-js-compat@3.39.0: dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 - core-js@3.38.1: {} + core-js@3.39.0: {} core-util-is@1.0.3: {} @@ -12871,10 +12751,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-blank-pseudo@7.0.0(postcss@8.4.47): + css-blank-pseudo@7.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 css-declaration-sorter@7.2.0(postcss@8.4.47): dependencies: @@ -12904,14 +12784,14 @@ snapshots: css-global-keywords@1.0.1: {} - css-has-pseudo@7.0.0(postcss@8.4.47): + css-has-pseudo@7.0.1(postcss@8.4.47): dependencies: - '@csstools/selector-specificity': 4.0.0(postcss-selector-parser@6.1.2) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.0.0) postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-value-parser: 4.2.0 - css-loader@7.1.2(webpack@5.95.0): + css-loader@7.1.2(webpack@5.96.1): dependencies: icss-utils: 5.1.0(postcss@8.4.47) postcss: 8.4.47 @@ -12922,9 +12802,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) - css-minimizer-webpack-plugin@7.0.0(clean-css@5.3.3)(webpack@5.95.0): + css-minimizer-webpack-plugin@7.0.0(clean-css@5.3.3)(webpack@5.96.1): dependencies: '@jridgewell/trace-mapping': 0.3.25 cssnano: 7.0.6(postcss@8.4.47) @@ -12932,7 +12812,7 @@ snapshots: postcss: 8.4.47 schema-utils: 4.2.0 serialize-javascript: 6.0.2 - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) optionalDependencies: clean-css: 5.3.3 @@ -12970,9 +12850,9 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 - css-tree@3.0.0: + css-tree@3.0.1: dependencies: - mdn-data: 2.10.0 + mdn-data: 2.12.1 source-map-js: 1.2.1 css-what@6.1.0: {} @@ -12987,7 +12867,7 @@ snapshots: cssnano-preset-default@7.0.6(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 css-declaration-sorter: 7.2.0(postcss@8.4.47) cssnano-utils: 5.0.0(postcss@8.4.47) postcss: 8.4.47 @@ -13221,7 +13101,7 @@ snapshots: dependencies: dequal: 2.0.3 - devtools-protocol@0.0.1342118: {} + devtools-protocol@0.0.1354347: {} diff@4.0.2: {} @@ -13294,7 +13174,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 draw-svg-path@1.0.0: dependencies: @@ -13322,7 +13202,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.38: {} + electron-to-chromium@1.5.51: {} element-size@1.1.1: {} @@ -13518,6 +13398,20 @@ snapshots: es6-iterator: 2.0.3 es6-symbol: 3.1.4 + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.14.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -13574,11 +13468,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.31.0)(webpack@5.95.0): + eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.31.0)(webpack@5.96.1): dependencies: debug: 3.2.7 enhanced-resolve: 0.9.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1) find-root: 1.1.0 hasown: 2.0.2 interpret: 1.4.0 @@ -13587,40 +13481,40 @@ snapshots: lodash: 4.17.21 resolve: 2.0.0-next.5 semver: 5.7.2 - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.9.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 8.13.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-webpack: 0.13.9(eslint-plugin-import@2.31.0)(webpack@5.95.0) + eslint-import-resolver-webpack: 0.13.9(eslint-plugin-import@2.31.0)(webpack@5.96.1) transitivePeerDependencies: - supports-color - eslint-plugin-formatjs@5.1.0(eslint@8.57.1): + eslint-plugin-formatjs@5.2.2(eslint@8.57.1)(typescript@5.6.3): dependencies: - '@formatjs/icu-messageformat-parser': 2.7.10 - '@formatjs/ts-transformer': 3.13.16 + '@formatjs/icu-messageformat-parser': 2.9.3 + '@formatjs/ts-transformer': 3.13.22 '@types/eslint': 9.6.1 - '@types/picomatch': 2.3.4 - '@typescript-eslint/utils': 8.5.0(eslint@8.57.1)(typescript@5.6.3) + '@types/picomatch': 3.0.1 + '@typescript-eslint/utils': 8.13.0(eslint@8.57.1)(typescript@5.6.3) emoji-regex: 10.4.0 eslint: 8.57.1 magic-string: 0.30.12 - picomatch: 2.3.1 - tslib: 2.7.0 - typescript: 5.6.3 + picomatch: 4.0.2 + tslib: 2.8.1 unicode-emoji-utils: 1.2.0 transitivePeerDependencies: - supports-color - ts-jest + - typescript - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -13631,7 +13525,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.9.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -13643,7 +13537,7 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.9.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 8.13.0(eslint@8.57.1)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -13655,14 +13549,14 @@ snapshots: eslint-plugin-unicorn@56.0.0(eslint@8.57.1): dependencies: - '@babel/helper-validator-identifier': 7.25.7 - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) + '@babel/helper-validator-identifier': 7.25.9 + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) ci-info: 4.0.0 clean-regexp: 1.0.0 - core-js-compat: 3.38.1 + core-js-compat: 3.39.0 eslint: 8.57.1 esquery: 1.6.0 - globals: 15.11.0 + globals: 15.12.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 jsesc: 3.0.2 @@ -13689,8 +13583,8 @@ snapshots: eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) - '@eslint-community/regexpp': 4.11.1 + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.57.1 '@humanwhocodes/config-array': 0.13.0 @@ -13739,8 +13633,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -13770,6 +13664,11 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + estree-util-to-js@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -13806,9 +13705,9 @@ snapshots: exponential-backoff@3.1.1: {} - expose-loader@5.0.0(webpack@5.95.0): + expose-loader@5.0.0(webpack@5.96.1): dependencies: - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) express@4.21.1: dependencies: @@ -13861,11 +13760,6 @@ snapshots: dependencies: is-extendable: 0.1.1 - extend-shallow@3.0.2: - dependencies: - assign-symbols: 1.0.0 - is-extendable: 1.0.1 - extend@3.0.2: {} extract-zip@2.0.1: @@ -14210,8 +14104,6 @@ snapshots: transitivePeerDependencies: - supports-color - get-value@2.0.6: {} - github-slugger@2.0.0: {} gl-mat4@1.2.0: {} @@ -14315,7 +14207,7 @@ snapshots: dependencies: type-fest: 0.20.2 - globals@15.11.0: {} + globals@15.12.0: {} globalthis@1.0.4: dependencies: @@ -14459,8 +14351,6 @@ snapshots: has-bigints@1.0.2: {} - has-flag@3.0.0: {} - has-flag@4.0.0: {} has-hover@1.0.1: @@ -14514,7 +14404,7 @@ snapshots: '@types/hast': 3.0.4 devlop: 1.1.0 hast-util-from-parse5: 8.0.1 - parse5: 7.2.0 + parse5: 7.2.1 vfile: 6.0.3 vfile-message: 4.0.2 @@ -14570,7 +14460,7 @@ snapshots: hast-util-to-parse5: 8.0.0 html-void-elements: 3.0.0 mdast-util-to-hast: 13.2.0 - parse5: 7.2.0 + parse5: 7.2.1 unist-util-position: 5.0.0 unist-util-visit: 5.0.0 vfile: 6.0.3 @@ -14728,13 +14618,13 @@ snapshots: he: 1.2.0 param-case: 3.0.4 relateurl: 0.2.7 - terser: 5.34.1 + terser: 5.36.0 html-tags@3.3.1: {} html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.0(webpack@5.95.0): + html-webpack-plugin@5.6.3(webpack@5.96.1): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -14742,7 +14632,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) html-whitespace-sensitive-tag-names@3.0.1: {} @@ -14842,9 +14732,9 @@ snapshots: hyperdyperid@1.2.0: {} - i18next@23.16.0: + i18next@23.16.4: dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.26.0 iconv-lite@0.4.24: dependencies: @@ -14899,6 +14789,8 @@ snapshots: ini@4.1.3: {} + ini@5.0.0: {} + inline-style-parser@0.1.1: {} inline-style-parser@0.2.4: {} @@ -14915,12 +14807,12 @@ snapshots: intersection-observer@0.11.0: {} - intl-messageformat@10.7.0: + intl-messageformat@10.7.6: dependencies: - '@formatjs/ecma402-abstract': 2.2.0 - '@formatjs/fast-memoize': 2.2.1 - '@formatjs/icu-messageformat-parser': 2.7.10 - tslib: 2.7.0 + '@formatjs/ecma402-abstract': 2.2.3 + '@formatjs/fast-memoize': 2.2.3 + '@formatjs/icu-messageformat-parser': 2.9.3 + tslib: 2.8.1 ip-address@9.0.5: dependencies: @@ -14993,10 +14885,6 @@ snapshots: is-extendable@0.1.1: {} - is-extendable@1.0.1: - dependencies: - is-plain-object: 2.0.4 - is-extglob@2.1.1: {} is-finite@1.1.0: {} @@ -15065,10 +14953,6 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-reference@3.0.2: - dependencies: - '@types/estree': 1.0.6 - is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -15165,8 +15049,8 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.25.8 - '@babel/parser': 7.25.8 + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.3 @@ -15204,7 +15088,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.16.11 + '@types/node': 22.9.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -15212,13 +15096,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -15261,7 +15145,7 @@ snapshots: https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.13 - parse5: 7.2.0 + parse5: 7.2.1 rrweb-cssom: 0.7.1 saxes: 6.0.0 symbol-tree: 3.2.4 @@ -15394,7 +15278,7 @@ snapshots: launch-editor@2.9.1: dependencies: - picocolors: 1.1.0 + picocolors: 1.1.1 shell-quote: 1.8.1 lazystream@1.0.1: @@ -15532,7 +15416,7 @@ snapshots: lower-case@2.0.2: dependencies: - tslib: 2.7.0 + tslib: 2.8.1 lru-cache@5.1.1: dependencies: @@ -15550,8 +15434,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 source-map-js: 1.2.1 make-dir@2.1.0: @@ -15583,7 +15467,7 @@ snapshots: minipass-fetch: 2.1.2 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 - negotiator: 0.6.3 + negotiator: 0.6.4 promise-retry: 2.0.1 socks-proxy-agent: 7.0.0 ssri: 9.0.1 @@ -15633,7 +15517,7 @@ snapshots: '@mapbox/unitbezier': 0.0.1 '@mapbox/vector-tile': 1.3.1 '@mapbox/whoots-js': 3.1.0 - '@maplibre/maplibre-gl-style-spec': 20.3.1 + '@maplibre/maplibre-gl-style-spec': 20.4.0 '@types/geojson': 7946.0.14 '@types/geojson-vt': 3.2.5 '@types/mapbox__point-geometry': 0.1.4 @@ -15655,7 +15539,7 @@ snapshots: markdown-extensions@2.0.0: {} - markdown-table@3.0.3: {} + markdown-table@3.0.4: {} math-log2@1.0.1: {} @@ -15672,8 +15556,8 @@ snapshots: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.1 stringify-entities: 4.0.4 unist-util-visit-parents: 6.0.1 @@ -15687,7 +15571,7 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - mdast-util-from-markdown@2.0.1: + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 @@ -15716,8 +15600,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.0 transitivePeerDependencies: - supports-color @@ -15725,8 +15609,8 @@ snapshots: mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -15734,9 +15618,9 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - markdown-table: 3.0.3 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -15744,20 +15628,20 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color mdast-util-gfm@3.0.0: dependencies: - mdast-util-from-markdown: 2.0.1 + mdast-util-from-markdown: 2.0.2 mdast-util-gfm-autolink-literal: 2.0.1 mdast-util-gfm-footnote: 2.0.0 mdast-util-gfm-strikethrough: 2.0.0 mdast-util-gfm-table: 2.0.0 mdast-util-gfm-task-list-item: 2.0.0 - mdast-util-to-markdown: 2.1.0 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -15767,8 +15651,8 @@ snapshots: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -15780,8 +15664,8 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.1 stringify-entities: 4.0.4 unist-util-stringify-position: 4.0.0 @@ -15791,11 +15675,11 @@ snapshots: mdast-util-mdx@3.0.0: dependencies: - mdast-util-from-markdown: 2.0.1 + mdast-util-from-markdown: 2.0.2 mdast-util-mdx-expression: 2.0.1 mdast-util-mdx-jsx: 3.1.3 mdast-util-mdxjs-esm: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -15805,8 +15689,8 @@ snapshots: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -15827,13 +15711,14 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 - mdast-util-to-markdown@2.1.0: + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 longest-streak: 3.1.0 mdast-util-phrasing: 4.1.0 mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.0 micromark-util-decode-string: 2.0.0 unist-util-visit: 5.0.0 zwitch: 2.0.4 @@ -15846,16 +15731,16 @@ snapshots: mdn-data@2.0.30: {} - mdn-data@2.10.0: {} + mdn-data@2.12.1: {} media-typer@0.3.0: {} memfs@4.14.0: dependencies: - '@jsonjoy.com/json-pack': 1.1.0(tslib@2.7.0) - '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) - tree-dump: 1.0.2(tslib@2.7.0) - tslib: 2.7.0 + '@jsonjoy.com/json-pack': 1.1.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.5.0(tslib@2.8.1) + tree-dump: 1.0.2(tslib@2.8.1) + tslib: 2.8.1 memory-fs@0.2.0: {} @@ -16016,8 +15901,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) micromark-extension-mdx-expression: 3.0.0 micromark-extension-mdx-jsx: 3.0.1 micromark-extension-mdx-md: 2.0.0 @@ -16187,11 +16072,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.1(webpack@5.95.0): + mini-css-extract-plugin@2.9.2(webpack@5.96.1): dependencies: schema-utils: 4.2.0 tapable: 2.2.1 - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) minimalistic-assert@1.0.1: {} @@ -16297,7 +16182,7 @@ snapshots: nanoid@3.3.7: {} - nanoid@5.0.7: {} + nanoid@5.0.8: {} native-promise-only@0.8.1: {} @@ -16317,6 +16202,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@0.6.4: {} + neo-async@2.6.2: {} neotraverse@0.6.18: {} @@ -16334,7 +16221,7 @@ snapshots: no-case@3.0.4: dependencies: lower-case: 2.0.2 - tslib: 2.7.0 + tslib: 2.8.1 node-fetch@2.7.0(encoding@0.1.13): dependencies: @@ -16510,7 +16397,7 @@ snapshots: oniguruma-to-js@0.4.3: dependencies: - regex: 4.3.3 + regex: 4.4.0 only@0.0.2: {} @@ -16549,7 +16436,7 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@8.1.0: + ora@8.1.1: dependencies: chalk: 5.3.0 cli-cursor: 5.0.0 @@ -16608,7 +16495,7 @@ snapshots: p-queue@8.0.1: dependencies: eventemitter3: 5.0.1 - p-timeout: 6.1.2 + p-timeout: 6.1.3 p-retry@6.2.0: dependencies: @@ -16616,7 +16503,7 @@ snapshots: is-network-error: 1.1.0 retry: 0.13.1 - p-timeout@6.1.2: {} + p-timeout@6.1.3: {} p-try@2.2.0: {} @@ -16666,7 +16553,7 @@ snapshots: param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 parent-module@1.0.1: dependencies: @@ -16687,7 +16574,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.25.7 + '@babel/code-frame': 7.26.2 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -16709,7 +16596,7 @@ snapshots: parse-unit@1.0.1: {} - parse5@7.2.0: + parse5@7.2.1: dependencies: entities: 4.5.0 @@ -16718,7 +16605,7 @@ snapshots: pascal-case@3.1.2: dependencies: no-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 path-browserify@1.0.1: {} @@ -16749,18 +16636,14 @@ snapshots: performance-now@2.1.0: {} - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.6 - estree-walker: 3.0.3 - is-reference: 3.0.2 - pick-by-alias@1.2.0: {} - picocolors@1.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} + picomatch@4.0.2: {} + pify@2.3.0: {} pify@4.0.1: {} @@ -16779,7 +16662,7 @@ snapshots: dependencies: find-up: 6.3.0 - plotly.js@2.35.2(mapbox-gl@1.13.3)(webpack@5.95.0): + plotly.js@2.35.2(mapbox-gl@1.13.3)(webpack@5.96.1): dependencies: '@plotly/d3': 3.8.2 '@plotly/d3-sankey': 0.7.2 @@ -16795,7 +16678,7 @@ snapshots: color-parse: 2.0.0 color-rgba: 2.1.1 country-regex: 1.1.0 - css-loader: 7.1.2(webpack@5.95.0) + css-loader: 7.1.2(webpack@5.96.1) d3-force: 1.2.1 d3-format: 1.4.5 d3-geo: 1.12.1 @@ -16825,7 +16708,7 @@ snapshots: regl-scatter2d: 3.3.1 regl-splom: 1.0.14 strongly-connected-components: 1.0.1 - style-loader: 4.0.0(webpack@5.95.0) + style-loader: 4.0.0(webpack@5.96.1) superscript-text: 1.0.0 svg-path-sdf: 1.1.3 tinycolor2: 1.6.0 @@ -16847,10 +16730,10 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-attribute-case-insensitive@7.0.0(postcss@8.4.47): + postcss-attribute-case-insensitive@7.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-calc@10.0.2(postcss@8.4.47): dependencies: @@ -16863,11 +16746,11 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@7.0.3(postcss@8.4.47): + postcss-color-functional-notation@7.0.5(postcss@8.4.47): dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 @@ -16886,7 +16769,7 @@ snapshots: postcss-colormin@7.0.2(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 caniuse-api: 3.0.0 colord: 2.9.3 postcss: 8.4.47 @@ -16894,39 +16777,39 @@ snapshots: postcss-convert-values@7.0.4(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-custom-media@11.0.3(postcss@8.4.47): + postcss-custom-media@11.0.5(postcss@8.4.47): dependencies: - '@csstools/cascade-layer-name-parser': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 - '@csstools/media-query-list-parser': 4.0.0(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) + '@csstools/cascade-layer-name-parser': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) postcss: 8.4.47 - postcss-custom-properties@14.0.2(postcss@8.4.47): + postcss-custom-properties@14.0.4(postcss@8.4.47): dependencies: - '@csstools/cascade-layer-name-parser': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/cascade-layer-name-parser': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-custom-selectors@8.0.2(postcss@8.4.47): + postcss-custom-selectors@8.0.4(postcss@8.4.47): dependencies: - '@csstools/cascade-layer-name-parser': 2.0.2(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/cascade-layer-name-parser': 2.0.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 - postcss-dir-pseudo-class@9.0.0(postcss@8.4.47): + postcss-dir-pseudo-class@9.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-discard-comments@7.0.3(postcss@8.4.47): dependencies: @@ -16957,15 +16840,15 @@ snapshots: postcss: 8.4.47 postcss-nesting: 10.2.0(postcss@8.4.47) - postcss-focus-visible@10.0.0(postcss@8.4.47): + postcss-focus-visible@10.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 - postcss-focus-within@9.0.0(postcss@8.4.47): + postcss-focus-within@9.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-font-variant@5.0.0(postcss@8.4.47): dependencies: @@ -16988,23 +16871,23 @@ snapshots: read-cache: 1.0.0 resolve: 1.22.8 - postcss-lab-function@7.0.3(postcss@8.4.47): + postcss-lab-function@7.0.5(postcss@8.4.47): dependencies: - '@csstools/css-color-parser': 3.0.3(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 + '@csstools/css-color-parser': 3.0.5(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) '@csstools/utilities': 2.0.0(postcss@8.4.47) postcss: 8.4.47 - postcss-loader@8.1.1(postcss@8.4.47)(typescript@5.6.3)(webpack@5.95.0): + postcss-loader@8.1.1(postcss@8.4.47)(typescript@5.6.3)(webpack@5.96.1): dependencies: cosmiconfig: 9.0.0(typescript@5.6.3) jiti: 1.21.6 postcss: 8.4.47 semver: 7.6.3 optionalDependencies: - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) transitivePeerDependencies: - typescript @@ -17021,7 +16904,7 @@ snapshots: postcss-merge-rules@7.0.4(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 caniuse-api: 3.0.0 cssnano-utils: 5.0.0(postcss@8.4.47) postcss: 8.4.47 @@ -17041,7 +16924,7 @@ snapshots: postcss-minify-params@7.0.2(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 cssnano-utils: 5.0.0(postcss@8.4.47) postcss: 8.4.47 postcss-value-parser: 4.2.0 @@ -17084,12 +16967,12 @@ snapshots: postcss: 8.4.47 postcss-selector-parser: 6.1.2 - postcss-nesting@13.0.0(postcss@8.4.47): + postcss-nesting@13.0.1(postcss@8.4.47): dependencies: - '@csstools/selector-resolve-nested': 2.0.0(postcss-selector-parser@6.1.2) - '@csstools/selector-specificity': 4.0.0(postcss-selector-parser@6.1.2) + '@csstools/selector-resolve-nested': 3.0.0(postcss-selector-parser@7.0.0) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.0.0) postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-normalize-charset@7.0.0(postcss@8.4.47): dependencies: @@ -17122,7 +17005,7 @@ snapshots: postcss-normalize-unicode@7.0.2(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 postcss: 8.4.47 postcss-value-parser: 4.2.0 @@ -17164,79 +17047,79 @@ snapshots: dependencies: postcss: 8.4.47 - postcss-preset-env@10.0.7(postcss@8.4.47): + postcss-preset-env@10.0.9(postcss@8.4.47): dependencies: - '@csstools/postcss-cascade-layers': 5.0.0(postcss@8.4.47) - '@csstools/postcss-color-function': 4.0.3(postcss@8.4.47) - '@csstools/postcss-color-mix-function': 3.0.3(postcss@8.4.47) - '@csstools/postcss-content-alt-text': 2.0.2(postcss@8.4.47) - '@csstools/postcss-exponential-functions': 2.0.2(postcss@8.4.47) + '@csstools/postcss-cascade-layers': 5.0.1(postcss@8.4.47) + '@csstools/postcss-color-function': 4.0.5(postcss@8.4.47) + '@csstools/postcss-color-mix-function': 3.0.5(postcss@8.4.47) + '@csstools/postcss-content-alt-text': 2.0.4(postcss@8.4.47) + '@csstools/postcss-exponential-functions': 2.0.4(postcss@8.4.47) '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.4.47) - '@csstools/postcss-gamut-mapping': 2.0.3(postcss@8.4.47) - '@csstools/postcss-gradients-interpolation-method': 5.0.3(postcss@8.4.47) - '@csstools/postcss-hwb-function': 4.0.3(postcss@8.4.47) + '@csstools/postcss-gamut-mapping': 2.0.5(postcss@8.4.47) + '@csstools/postcss-gradients-interpolation-method': 5.0.5(postcss@8.4.47) + '@csstools/postcss-hwb-function': 4.0.5(postcss@8.4.47) '@csstools/postcss-ic-unit': 4.0.0(postcss@8.4.47) '@csstools/postcss-initial': 2.0.0(postcss@8.4.47) - '@csstools/postcss-is-pseudo-class': 5.0.0(postcss@8.4.47) - '@csstools/postcss-light-dark-function': 2.0.5(postcss@8.4.47) + '@csstools/postcss-is-pseudo-class': 5.0.1(postcss@8.4.47) + '@csstools/postcss-light-dark-function': 2.0.7(postcss@8.4.47) '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.4.47) '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.4.47) '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.4.47) '@csstools/postcss-logical-resize': 3.0.0(postcss@8.4.47) - '@csstools/postcss-logical-viewport-units': 3.0.2(postcss@8.4.47) - '@csstools/postcss-media-minmax': 2.0.2(postcss@8.4.47) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.2(postcss@8.4.47) + '@csstools/postcss-logical-viewport-units': 3.0.3(postcss@8.4.47) + '@csstools/postcss-media-minmax': 2.0.4(postcss@8.4.47) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.4(postcss@8.4.47) '@csstools/postcss-nested-calc': 4.0.0(postcss@8.4.47) '@csstools/postcss-normalize-display-values': 4.0.0(postcss@8.4.47) - '@csstools/postcss-oklab-function': 4.0.3(postcss@8.4.47) + '@csstools/postcss-oklab-function': 4.0.5(postcss@8.4.47) '@csstools/postcss-progressive-custom-properties': 4.0.0(postcss@8.4.47) - '@csstools/postcss-relative-color-syntax': 3.0.3(postcss@8.4.47) - '@csstools/postcss-scope-pseudo-class': 4.0.0(postcss@8.4.47) - '@csstools/postcss-stepped-value-functions': 4.0.2(postcss@8.4.47) + '@csstools/postcss-relative-color-syntax': 3.0.5(postcss@8.4.47) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.4.47) + '@csstools/postcss-stepped-value-functions': 4.0.4(postcss@8.4.47) '@csstools/postcss-text-decoration-shorthand': 4.0.1(postcss@8.4.47) - '@csstools/postcss-trigonometric-functions': 4.0.2(postcss@8.4.47) + '@csstools/postcss-trigonometric-functions': 4.0.4(postcss@8.4.47) '@csstools/postcss-unset-value': 4.0.0(postcss@8.4.47) autoprefixer: 10.4.20(postcss@8.4.47) - browserslist: 4.24.0 - css-blank-pseudo: 7.0.0(postcss@8.4.47) - css-has-pseudo: 7.0.0(postcss@8.4.47) + browserslist: 4.24.2 + css-blank-pseudo: 7.0.1(postcss@8.4.47) + css-has-pseudo: 7.0.1(postcss@8.4.47) css-prefers-color-scheme: 10.0.0(postcss@8.4.47) cssdb: 8.1.2 postcss: 8.4.47 - postcss-attribute-case-insensitive: 7.0.0(postcss@8.4.47) + postcss-attribute-case-insensitive: 7.0.1(postcss@8.4.47) postcss-clamp: 4.1.0(postcss@8.4.47) - postcss-color-functional-notation: 7.0.3(postcss@8.4.47) + postcss-color-functional-notation: 7.0.5(postcss@8.4.47) postcss-color-hex-alpha: 10.0.0(postcss@8.4.47) postcss-color-rebeccapurple: 10.0.0(postcss@8.4.47) - postcss-custom-media: 11.0.3(postcss@8.4.47) - postcss-custom-properties: 14.0.2(postcss@8.4.47) - postcss-custom-selectors: 8.0.2(postcss@8.4.47) - postcss-dir-pseudo-class: 9.0.0(postcss@8.4.47) + postcss-custom-media: 11.0.5(postcss@8.4.47) + postcss-custom-properties: 14.0.4(postcss@8.4.47) + postcss-custom-selectors: 8.0.4(postcss@8.4.47) + postcss-dir-pseudo-class: 9.0.1(postcss@8.4.47) postcss-double-position-gradients: 6.0.0(postcss@8.4.47) - postcss-focus-visible: 10.0.0(postcss@8.4.47) - postcss-focus-within: 9.0.0(postcss@8.4.47) + postcss-focus-visible: 10.0.1(postcss@8.4.47) + postcss-focus-within: 9.0.1(postcss@8.4.47) postcss-font-variant: 5.0.0(postcss@8.4.47) postcss-gap-properties: 6.0.0(postcss@8.4.47) postcss-image-set-function: 7.0.0(postcss@8.4.47) - postcss-lab-function: 7.0.3(postcss@8.4.47) + postcss-lab-function: 7.0.5(postcss@8.4.47) postcss-logical: 8.0.0(postcss@8.4.47) - postcss-nesting: 13.0.0(postcss@8.4.47) + postcss-nesting: 13.0.1(postcss@8.4.47) postcss-opacity-percentage: 3.0.0(postcss@8.4.47) postcss-overflow-shorthand: 6.0.0(postcss@8.4.47) postcss-page-break: 3.0.4(postcss@8.4.47) postcss-place: 10.0.0(postcss@8.4.47) - postcss-pseudo-class-any-link: 10.0.0(postcss@8.4.47) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.4.47) postcss-replace-overflow-wrap: 4.0.0(postcss@8.4.47) - postcss-selector-not: 8.0.0(postcss@8.4.47) + postcss-selector-not: 8.0.1(postcss@8.4.47) - postcss-pseudo-class-any-link@10.0.0(postcss@8.4.47): + postcss-pseudo-class-any-link@10.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-reduce-initial@7.0.2(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 caniuse-api: 3.0.0 postcss: 8.4.47 @@ -17255,16 +17138,21 @@ snapshots: dependencies: postcss: 8.4.47 - postcss-selector-not@8.0.0(postcss@8.4.47): + postcss-selector-not@8.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.0.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-simple-vars@7.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 @@ -17285,7 +17173,7 @@ snapshots: postcss@8.4.47: dependencies: nanoid: 3.3.7 - picocolors: 1.1.0 + picocolors: 1.1.1 source-map-js: 1.2.1 potpack@1.0.2: {} @@ -17401,12 +17289,12 @@ snapshots: punycode@2.3.1: {} - puppeteer-core@23.5.3(patch_hash=arlsztxuq6xlcowulqo36wnkoa): + puppeteer-core@23.7.0: dependencies: - '@puppeteer/browsers': 2.4.0 - chromium-bidi: 0.8.0(devtools-protocol@0.0.1342118) + '@puppeteer/browsers': 2.4.1 + chromium-bidi: 0.8.0(devtools-protocol@0.0.1354347) debug: 4.3.7 - devtools-protocol: 0.0.1342118 + devtools-protocol: 0.0.1354347 typed-query-selector: 2.12.0 ws: 8.18.0 transitivePeerDependencies: @@ -17414,13 +17302,13 @@ snapshots: - supports-color - utf-8-validate - puppeteer@23.5.3(typescript@5.6.3): + puppeteer@23.7.0(typescript@5.6.3): dependencies: - '@puppeteer/browsers': 2.4.0 - chromium-bidi: 0.8.0(devtools-protocol@0.0.1342118) + '@puppeteer/browsers': 2.4.1 + chromium-bidi: 0.8.0(devtools-protocol@0.0.1354347) cosmiconfig: 9.0.0(typescript@5.6.3) - devtools-protocol: 0.0.1342118 - puppeteer-core: 23.5.3(patch_hash=arlsztxuq6xlcowulqo36wnkoa) + devtools-protocol: 0.0.1354347 + puppeteer-core: 23.7.0 typed-query-selector: 2.12.0 transitivePeerDependencies: - bufferutil @@ -17541,6 +17429,36 @@ snapshots: dependencies: resolve: 1.22.8 + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.6 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.0(acorn@8.14.0): + dependencies: + acorn-jsx: 5.3.2(acorn@8.14.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.6 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.6 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + redent@4.0.0: dependencies: indent-string: 5.0.0 @@ -17556,9 +17474,9 @@ snapshots: regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.26.0 - regex@4.3.3: {} + regex@4.4.0: {} regexp-tree@0.1.27: {} @@ -17574,7 +17492,7 @@ snapshots: regenerate: 1.4.2 regenerate-unicode-properties: 10.2.0 regjsgen: 0.8.0 - regjsparser: 0.11.1 + regjsparser: 0.11.2 unicode-match-property-ecmascript: 2.0.0 unicode-match-property-value-ecmascript: 2.2.0 @@ -17584,7 +17502,7 @@ snapshots: dependencies: jsesc: 0.5.0 - regjsparser@0.11.1: + regjsparser@0.11.2: dependencies: jsesc: 3.0.2 @@ -17664,6 +17582,14 @@ snapshots: hast-util-raw: 9.0.4 vfile: 6.0.3 + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.0 + transitivePeerDependencies: + - supports-color + rehype-stringify@10.0.1: dependencies: '@types/hast': 3.0.4 @@ -17703,7 +17629,7 @@ snapshots: transitivePeerDependencies: - supports-color - remark-mdx@3.0.1: + remark-mdx@3.1.0: dependencies: mdast-util-mdx: 3.0.0 micromark-extension-mdxjs: 3.0.0 @@ -17713,7 +17639,7 @@ snapshots: remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.1 + mdast-util-from-markdown: 2.0.2 micromark-util-types: 2.0.0 unified: 11.0.5 transitivePeerDependencies: @@ -17737,7 +17663,7 @@ snapshots: remark-stringify@11.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.0 + mdast-util-to-markdown: 2.1.2 unified: 11.0.5 remove-bom-buffer@3.0.0: @@ -17851,26 +17777,28 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.24.0: + rollup@4.24.4: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.24.0 - '@rollup/rollup-android-arm64': 4.24.0 - '@rollup/rollup-darwin-arm64': 4.24.0 - '@rollup/rollup-darwin-x64': 4.24.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.24.0 - '@rollup/rollup-linux-arm-musleabihf': 4.24.0 - '@rollup/rollup-linux-arm64-gnu': 4.24.0 - '@rollup/rollup-linux-arm64-musl': 4.24.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.24.0 - '@rollup/rollup-linux-riscv64-gnu': 4.24.0 - '@rollup/rollup-linux-s390x-gnu': 4.24.0 - '@rollup/rollup-linux-x64-gnu': 4.24.0 - '@rollup/rollup-linux-x64-musl': 4.24.0 - '@rollup/rollup-win32-arm64-msvc': 4.24.0 - '@rollup/rollup-win32-ia32-msvc': 4.24.0 - '@rollup/rollup-win32-x64-msvc': 4.24.0 + '@rollup/rollup-android-arm-eabi': 4.24.4 + '@rollup/rollup-android-arm64': 4.24.4 + '@rollup/rollup-darwin-arm64': 4.24.4 + '@rollup/rollup-darwin-x64': 4.24.4 + '@rollup/rollup-freebsd-arm64': 4.24.4 + '@rollup/rollup-freebsd-x64': 4.24.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.24.4 + '@rollup/rollup-linux-arm-musleabihf': 4.24.4 + '@rollup/rollup-linux-arm64-gnu': 4.24.4 + '@rollup/rollup-linux-arm64-musl': 4.24.4 + '@rollup/rollup-linux-powerpc64le-gnu': 4.24.4 + '@rollup/rollup-linux-riscv64-gnu': 4.24.4 + '@rollup/rollup-linux-s390x-gnu': 4.24.4 + '@rollup/rollup-linux-x64-gnu': 4.24.4 + '@rollup/rollup-linux-x64-musl': 4.24.4 + '@rollup/rollup-win32-arm64-msvc': 4.24.4 + '@rollup/rollup-win32-ia32-msvc': 4.24.4 + '@rollup/rollup-win32-x64-msvc': 4.24.4 fsevents: 2.3.3 rrweb-cssom@0.7.1: {} @@ -18004,13 +17932,6 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 - set-value@2.0.1: - dependencies: - extend-shallow: 2.0.1 - is-extendable: 0.1.1 - is-plain-object: 2.0.4 - split-string: 3.1.0 - setprototypeof@1.1.0: {} setprototypeof@1.2.0: {} @@ -18057,12 +17978,12 @@ snapshots: shell-quote@1.8.1: {} - shiki@1.22.0: + shiki@1.22.2: dependencies: - '@shikijs/core': 1.22.0 - '@shikijs/engine-javascript': 1.22.0 - '@shikijs/engine-oniguruma': 1.22.0 - '@shikijs/types': 1.22.0 + '@shikijs/core': 1.22.2 + '@shikijs/engine-javascript': 1.22.2 + '@shikijs/engine-oniguruma': 1.22.2 + '@shikijs/types': 1.22.2 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 @@ -18145,19 +18066,6 @@ snapshots: ip-address: 9.0.5 smart-buffer: 4.2.0 - sort-asc@0.2.0: {} - - sort-desc@0.2.0: {} - - sort-object@3.0.3: - dependencies: - bytewise: 1.1.0 - get-value: 2.0.6 - is-extendable: 0.1.1 - sort-asc: 0.2.0 - sort-desc: 0.2.0 - union-value: 1.0.1 - sortablejs@1.15.3: {} sorttable@1.0.2: {} @@ -18227,10 +18135,6 @@ snapshots: spectrum-colorpicker@1.8.1: {} - split-string@3.1.0: - dependencies: - extend-shallow: 3.0.2 - sprintf-js@1.0.3: {} sprintf-js@1.1.3: {} @@ -18279,7 +18183,7 @@ snapshots: dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 - text-decoder: 1.2.0 + text-decoder: 1.2.1 optionalDependencies: bare-events: 2.5.0 @@ -18361,9 +18265,9 @@ snapshots: strongly-connected-components@1.0.1: {} - style-loader@4.0.0(webpack@5.95.0): + style-loader@4.0.0(webpack@5.96.1): dependencies: - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) style-to-object@0.4.4: dependencies: @@ -18375,7 +18279,7 @@ snapshots: stylehacks@7.0.4(postcss@8.4.47): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 postcss: 8.4.47 postcss-selector-parser: 6.1.2 @@ -18395,16 +18299,16 @@ snapshots: stylelint@16.10.0(typescript@5.6.3): dependencies: - '@csstools/css-parser-algorithms': 3.0.2(@csstools/css-tokenizer@3.0.2) - '@csstools/css-tokenizer': 3.0.2 - '@csstools/media-query-list-parser': 3.0.1(@csstools/css-parser-algorithms@3.0.2(@csstools/css-tokenizer@3.0.2))(@csstools/css-tokenizer@3.0.2) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + '@csstools/media-query-list-parser': 3.0.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) '@csstools/selector-specificity': 4.0.0(postcss-selector-parser@6.1.2) '@dual-bundle/import-meta-resolve': 4.1.0 balanced-match: 2.0.0 colord: 2.9.3 cosmiconfig: 9.0.0(typescript@5.6.3) css-functions-list: 3.2.3 - css-tree: 3.0.0 + css-tree: 3.0.1 debug: 4.3.7 fast-glob: 3.3.2 fastest-levenshtein: 1.0.16 @@ -18421,7 +18325,7 @@ snapshots: meow: 13.2.0 micromatch: 4.0.8 normalize-path: 3.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 postcss: 8.4.47 postcss-resolve-nested-selector: 0.1.6 postcss-safe-parser: 7.0.1(postcss@8.4.47) @@ -18449,10 +18353,6 @@ snapshots: superscript-text@1.0.0: {} - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -18516,7 +18416,7 @@ snapshots: css-tree: 2.3.1 css-what: 6.1.0 csso: 5.0.5 - picocolors: 1.1.0 + picocolors: 1.1.1 svgpath@2.6.0: {} @@ -18567,19 +18467,19 @@ snapshots: dependencies: bintrees: 1.0.2 - terser-webpack-plugin@5.3.10(webpack@5.95.0): + terser-webpack-plugin@5.3.10(webpack@5.96.1): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 - terser: 5.34.1 - webpack: 5.95.0(webpack-cli@5.1.4) + terser: 5.36.0 + webpack: 5.96.1(webpack-cli@5.1.4) - terser@5.34.1: + terser@5.36.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.12.1 + acorn: 8.14.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -18589,9 +18489,7 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - text-decoder@1.2.0: - dependencies: - b4a: 1.6.7 + text-decoder@1.2.1: {} text-field-edit@4.1.1: {} @@ -18601,9 +18499,9 @@ snapshots: textarea-caret@3.1.0: {} - thingies@1.21.0(tslib@2.7.0): + thingies@1.21.0(tslib@2.8.1): dependencies: - tslib: 2.7.0 + tslib: 2.8.1 throttle-debounce@3.0.1: {} @@ -18634,7 +18532,7 @@ snapshots: tinycolor2@1.6.0: {} - tinyexec@0.3.0: {} + tinyexec@0.3.1: {} tinyqueue@2.0.3: {} @@ -18644,19 +18542,17 @@ snapshots: dependencies: '@popperjs/core': 2.11.8 - tldts-core@6.1.51: {} + tldts-core@6.1.58: {} - tldts@6.1.51: + tldts@6.1.58: dependencies: - tldts-core: 6.1.51 + tldts-core: 6.1.58 to-absolute-glob@2.0.2: dependencies: is-absolute: 1.0.0 is-negated-glob: 1.0.0 - to-fast-properties@2.0.0: {} - to-float32@1.1.0: {} to-px@1.0.1: @@ -18679,7 +18575,7 @@ snapshots: tough-cookie@5.0.0: dependencies: - tldts: 6.1.51 + tldts: 6.1.58 tr46@0.0.3: {} @@ -18687,9 +18583,9 @@ snapshots: dependencies: punycode: 2.3.1 - tree-dump@1.0.2(tslib@2.7.0): + tree-dump@1.0.2(tslib@2.8.1): dependencies: - tslib: 2.7.0 + tslib: 2.8.1 trim-lines@3.0.1: {} @@ -18699,19 +18595,19 @@ snapshots: trough@2.2.0: {} - ts-api-utils@1.3.0(typescript@5.6.3): + ts-api-utils@1.4.0(typescript@5.6.3): dependencies: typescript: 5.6.3 - ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3): + ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.16.11 - acorn: 8.12.1 + '@types/node': 22.9.0 + acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 @@ -18732,7 +18628,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.7.0: {} + tslib@2.8.1: {} tsscmp@1.0.6: {} @@ -18837,18 +18733,12 @@ snapshots: typesafe-path@0.2.2: {} - typescript-auto-import-cache@0.3.3: + typescript-auto-import-cache@0.3.5: dependencies: semver: 7.6.3 typescript@5.6.3: {} - typewise-core@1.2.0: {} - - typewise@1.0.3: - dependencies: - typewise-core: 1.2.0 - uglify-js@3.19.3: optional: true @@ -18868,8 +18758,6 @@ snapshots: underscore@1.13.7: {} - undici-types@5.26.5: {} - undici-types@6.19.8: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -18897,13 +18785,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - union-value@1.0.1: - dependencies: - arr-union: 3.1.0 - get-value: 2.0.6 - is-extendable: 0.1.1 - set-value: 2.0.1 - unique-filename@2.0.1: dependencies: unique-slug: 3.0.0 @@ -18969,11 +18850,11 @@ snapshots: unquote@1.1.1: {} - update-browserslist-db@1.1.1(browserslist@4.24.0): + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: - browserslist: 4.24.0 + browserslist: 4.24.2 escalade: 3.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 update-diff@1.1.0: {} @@ -18983,12 +18864,12 @@ snapshots: url-join@4.0.1: {} - url-loader@4.1.1(webpack@5.95.0): + url-loader@4.1.1(webpack@5.96.1): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) url-parse@1.5.10: dependencies: @@ -19088,77 +18969,77 @@ snapshots: remove-trailing-separator: 1.1.0 replace-ext: 1.0.1 - vite@5.4.9(@types/node@20.16.11)(terser@5.34.1): + vite@5.4.10(@types/node@22.9.0)(terser@5.36.0): dependencies: esbuild: 0.21.5 postcss: 8.4.47 - rollup: 4.24.0 + rollup: 4.24.4 optionalDependencies: - '@types/node': 20.16.11 + '@types/node': 22.9.0 fsevents: 2.3.3 - terser: 5.34.1 + terser: 5.36.0 - vitefu@1.0.3(vite@5.4.9(@types/node@20.16.11)(terser@5.34.1)): + vitefu@1.0.3(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0)): optionalDependencies: - vite: 5.4.9(@types/node@20.16.11)(terser@5.34.1) + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) - vnu-jar@23.4.11: {} + vnu-jar@24.10.17: {} - volar-service-css@0.0.61(@volar/language-service@2.4.6): + volar-service-css@0.0.62(@volar/language-service@2.4.8): dependencies: vscode-css-languageservice: 6.3.1 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 optionalDependencies: - '@volar/language-service': 2.4.6 + '@volar/language-service': 2.4.8 - volar-service-emmet@0.0.61(@volar/language-service@2.4.6): + volar-service-emmet@0.0.62(@volar/language-service@2.4.8): dependencies: '@emmetio/css-parser': 0.4.0 '@emmetio/html-matcher': 1.3.0 '@vscode/emmet-helper': 2.9.3 vscode-uri: 3.0.8 optionalDependencies: - '@volar/language-service': 2.4.6 + '@volar/language-service': 2.4.8 - volar-service-html@0.0.61(@volar/language-service@2.4.6): + volar-service-html@0.0.62(@volar/language-service@2.4.8): dependencies: vscode-html-languageservice: 5.3.1 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 optionalDependencies: - '@volar/language-service': 2.4.6 + '@volar/language-service': 2.4.8 - volar-service-prettier@0.0.61(@volar/language-service@2.4.6)(prettier@3.3.3): + volar-service-prettier@0.0.62(@volar/language-service@2.4.8)(prettier@3.3.3): dependencies: vscode-uri: 3.0.8 optionalDependencies: - '@volar/language-service': 2.4.6 + '@volar/language-service': 2.4.8 prettier: 3.3.3 - volar-service-typescript-twoslash-queries@0.0.61(@volar/language-service@2.4.6): + volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.8): dependencies: vscode-uri: 3.0.8 optionalDependencies: - '@volar/language-service': 2.4.6 + '@volar/language-service': 2.4.8 - volar-service-typescript@0.0.61(@volar/language-service@2.4.6): + volar-service-typescript@0.0.62(@volar/language-service@2.4.8): dependencies: path-browserify: 1.0.1 semver: 7.6.3 - typescript-auto-import-cache: 0.3.3 + typescript-auto-import-cache: 0.3.5 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 vscode-uri: 3.0.8 optionalDependencies: - '@volar/language-service': 2.4.6 + '@volar/language-service': 2.4.8 - volar-service-yaml@0.0.61(@volar/language-service@2.4.6): + volar-service-yaml@0.0.62(@volar/language-service@2.4.8): dependencies: vscode-uri: 3.0.8 yaml-language-server: 1.15.0 optionalDependencies: - '@volar/language-service': 2.4.6 + '@volar/language-service': 2.4.8 vscode-css-languageservice@6.3.1: dependencies: @@ -19265,12 +19146,12 @@ snapshots: lodash.get: 4.4.2 lodash.topairs: 4.3.0 - webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.95.0): + webpack-cli@5.1.4(webpack-dev-server@5.1.0)(webpack@5.96.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.95.0) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.95.0) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.95.0) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.96.1) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.96.1) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.1.0)(webpack@5.96.1) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.3 @@ -19279,12 +19160,12 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) webpack-merge: 5.10.0 optionalDependencies: - webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.95.0) + webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.96.1) - webpack-dev-middleware@7.4.2(webpack@5.95.0): + webpack-dev-middleware@7.4.2(webpack@5.96.1): dependencies: colorette: 2.0.20 memfs: 4.14.0 @@ -19293,9 +19174,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.2.0 optionalDependencies: - webpack: 5.95.0(webpack-cli@5.1.4) + webpack: 5.96.1(webpack-cli@5.1.4) - webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.95.0): + webpack-dev-server@5.1.0(webpack-cli@5.1.4)(webpack@5.96.1): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -19303,12 +19184,12 @@ snapshots: '@types/serve-index': 1.9.4 '@types/serve-static': 1.15.7 '@types/sockjs': 0.3.36 - '@types/ws': 8.5.12 + '@types/ws': 8.5.13 ansi-html-community: 0.0.8 bonjour-service: 1.2.1 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.7.4 + compression: 1.7.5 connect-history-api-fallback: 2.0.0 express: 4.21.1 graceful-fs: 4.2.11 @@ -19323,11 +19204,11 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.95.0) + webpack-dev-middleware: 7.4.2(webpack@5.96.1) ws: 8.18.0 optionalDependencies: - webpack: 5.95.0(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.95.0) + webpack: 5.96.1(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.96.1) transitivePeerDependencies: - bufferutil - debug @@ -19342,15 +19223,15 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.95.0(webpack-cli@5.1.4): + webpack@5.96.1(webpack-cli@5.1.4): dependencies: + '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 '@webassemblyjs/ast': 1.12.1 '@webassemblyjs/wasm-edit': 1.12.1 '@webassemblyjs/wasm-parser': 1.12.1 - acorn: 8.12.1 - acorn-import-attributes: 1.9.5(acorn@8.12.1) - browserslist: 4.24.0 + acorn: 8.14.0 + browserslist: 4.24.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.17.1 es-module-lexer: 1.5.4 @@ -19364,11 +19245,11 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.95.0) + terser-webpack-plugin: 5.3.10(webpack@5.96.1) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: - webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.95.0) + webpack-cli: 5.1.4(webpack-dev-server@5.1.0)(webpack@5.96.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -19611,7 +19492,7 @@ snapshots: optionalDependencies: commander: 9.5.0 - zod-to-json-schema@3.23.3(zod@3.23.8): + zod-to-json-schema@3.23.5(zod@3.23.8): dependencies: zod: 3.23.8 @@ -19622,10 +19503,10 @@ snapshots: zod@3.23.8: {} - zulip-js@2.0.9(encoding@0.1.13): + zulip-js@2.1.0(encoding@0.1.13): dependencies: - '@babel/runtime': 7.25.7 - ini: 1.3.8 + '@babel/runtime': 7.26.0 + ini: 5.0.0 isomorphic-fetch: 3.0.0(encoding@0.1.13) isomorphic-form-data: 2.0.0 transitivePeerDependencies: diff --git a/version.py b/version.py index 297c369b02..1bd3b93407 100644 --- a/version.py +++ b/version.py @@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 319 # Last bumped for message-link class # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = (297, 0) # bumped 2024-11-05 to upgrade Node.js +PROVISION_VERSION = (298, 0) # bumped 2024-11-05 to upgrade JavaScript dependencies diff --git a/web/babel.config.js b/web/babel.config.js index 845a83c7c1..e0bc155efc 100644 --- a/web/babel.config.js +++ b/web/babel.config.js @@ -14,7 +14,7 @@ module.exports = { [ "@babel/preset-env", { - corejs: "3.37", + corejs: "3.39", include: ["transform-optional-chaining"], shippedProposals: true, useBuiltIns: "usage", From 792e9fa047a9d0f07744a57e6bcb46bcd05601bd Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 5 Nov 2024 12:48:20 -0800 Subject: [PATCH 254/276] install-shfmt: Upgrade shfmt from 3.9.0 to 3.10.0. Signed-off-by: Anders Kaseorg --- tools/setup/install-shfmt | 6 +++--- version.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/setup/install-shfmt b/tools/setup/install-shfmt index d79b5a3406..9b07c94f46 100755 --- a/tools/setup/install-shfmt +++ b/tools/setup/install-shfmt @@ -1,18 +1,18 @@ #!/usr/bin/env bash set -eu -version=3.9.0 +version=3.10.0 arch="$(uname -m)" case $arch in "x86_64") binary="shfmt_v${version}_linux_amd64" - sha256=d99b06506aee2ac9113daec3049922e70dc8cffb84658e3ae512c6a6cbe101b6 + sha256=1f57a384d59542f8fac5f503da1f3ea44242f46dff969569e80b524d64b71dbc ;; "aarch64") binary="shfmt_v${version}_linux_arm64" - sha256=5e511463068f3d27ae1b087fb597fb9e8ad865be2ac501964a222a834fc1c463 + sha256=9d23013d56640e228732fd2a04a9ede0ab46bc2d764bf22a4a35fb1b14d707a8 ;; esac diff --git a/version.py b/version.py index 1bd3b93407..8684d70b09 100644 --- a/version.py +++ b/version.py @@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 319 # Last bumped for message-link class # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = (298, 0) # bumped 2024-11-05 to upgrade JavaScript dependencies +PROVISION_VERSION = (298, 1) # bumped 2024-11-05 to upgrade shfmt From e50a51a8e1918b31efadea3c704cc674fb5bfdec Mon Sep 17 00:00:00 2001 From: evykassirer Date: Wed, 30 Oct 2024 14:23:41 -0700 Subject: [PATCH 255/276] settings: Convert general_notifications_table_labels.stream to tuples. This will help with the typescript conversion. --- web/src/settings_config.ts | 21 ++++++++++++--------- web/src/stream_edit.js | 5 ++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts index c2bdfc552b..a11dc95867 100644 --- a/web/src/settings_config.ts +++ b/web/src/settings_config.ts @@ -647,15 +647,18 @@ export const general_notifications_table_labels = { "email", "all_mentions", ], - stream: { - is_muted: $t({defaultMessage: "Mute channel"}), - desktop_notifications: $t({defaultMessage: "Visual desktop notifications"}), - audible_notifications: $t({defaultMessage: "Audible desktop notifications"}), - push_notifications: $t({defaultMessage: "Mobile notifications"}), - email_notifications: $t({defaultMessage: "Email notifications"}), - pin_to_top: $t({defaultMessage: "Pin channel to top of left sidebar"}), - wildcard_mentions_notify: $t({defaultMessage: "Notifications for @all/@everyone mentions"}), - }, + stream: [ + ["is_muted", $t({defaultMessage: "Mute channel"})], + ["desktop_notifications", $t({defaultMessage: "Visual desktop notifications"})], + ["audible_notifications", $t({defaultMessage: "Audible desktop notifications"})], + ["push_notifications", $t({defaultMessage: "Mobile notifications"})], + ["email_notifications", $t({defaultMessage: "Email notifications"})], + ["pin_to_top", $t({defaultMessage: "Pin channel to top of left sidebar"})], + [ + "wildcard_mentions_notify", + $t({defaultMessage: "Notifications for @all/@everyone mentions"}), + ], + ] as const, }; export const stream_specific_notification_settings: (keyof StreamSpecificNotificationSettings)[] = [ diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index 2072c91145..a2f10f21f2 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -185,10 +185,10 @@ export function stream_settings(sub) { const check_realm_setting = settings_config.all_notifications(user_settings).disabled_notification_settings; - const settings = Object.keys(settings_labels).map((setting) => { + return settings_labels.map(([setting, label]) => { const ret = { name: setting, - label: settings_labels[setting], + label, disabled_realm_setting: check_realm_setting[setting], is_disabled: check_realm_setting[setting], has_global_notification_setting: has_global_notification_setting(setting), @@ -206,7 +206,6 @@ export function stream_settings(sub) { ret.is_checked = sub[setting] && !check_realm_setting[setting]; return ret; }); - return settings; } function setup_dropdown(sub, slim_sub) { From 527d4672f85adfb2d6cbb93acf9a4e580e30984e Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 16:16:22 -0800 Subject: [PATCH 256/276] settings_components: Fix types in save_discard_stream_settings_widget_status_handler. This had been getting undefined `sub` already, in the modal to create a new channel, but managed to not cause errors because `properties_elements` was empty and `button_state` didn't equal `"unsaved"`, leaving areas that treated `sub` as defined not accessible. This commit fixes the type and handles the `undefined` case more directly. --- web/src/settings_components.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/web/src/settings_components.ts b/web/src/settings_components.ts index 70c5b099bf..e44a7b354a 100644 --- a/web/src/settings_components.ts +++ b/web/src/settings_components.ts @@ -1203,14 +1203,17 @@ export function save_discard_realm_settings_widget_status_handler($subsection: J export function save_discard_stream_settings_widget_status_handler( $subsection: JQuery, - sub: StreamSubscription, + sub: StreamSubscription | undefined, ): void { $subsection.find(".subsection-failed-status p").hide(); $subsection.find(".save-button").show(); const properties_elements = get_subsection_property_elements($subsection); - const show_change_process_button = properties_elements.some((elem) => - check_stream_settings_property_changed(elem, sub), - ); + let show_change_process_button = false; + if (sub !== undefined) { + show_change_process_button = properties_elements.some((elem) => + check_stream_settings_property_changed(elem, sub), + ); + } const $save_btn_controls = $subsection.find(".subsection-header .save-button-controls"); const button_state = show_change_process_button ? "unsaved" : "discarded"; @@ -1222,6 +1225,7 @@ export function save_discard_stream_settings_widget_status_handler( // making it private unless they subscribe. if ( button_state === "unsaved" && + sub && !sub.invite_only && !sub.subscribed && switching_to_private(properties_elements) From 924ebee421b7965a206940211e9cf03fca4f7b28 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 11:26:12 -0800 Subject: [PATCH 257/276] stream_edit: Remove unecessary checks for invalid stream id. None of these have showed up in Sentry in the last 90 days, and it will be easier to type this file if we can assume we always get a valid stream id and a valid sub for that stream id. --- web/src/stream_edit.js | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index a2f10f21f2..a69e94545c 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -83,17 +83,7 @@ function get_stream_id(target) { function get_sub_for_target(target) { const stream_id = get_stream_id(target); - if (!stream_id) { - blueslip.error("Cannot find stream id for target"); - return undefined; - } - - const sub = sub_store.get(stream_id); - if (!sub) { - blueslip.error("get_sub_for_target() failed id lookup", {stream_id}); - return undefined; - } - return sub; + return sub_store.get(stream_id); } export function open_edit_panel_for_row(stream_row) { @@ -339,10 +329,6 @@ function stream_setting_changed(e) { const sub = get_sub_for_target(e.target); const $status_element = $(e.target).closest(".subsection-parent").find(".alert-notification"); const setting = e.target.name; - if (!sub) { - blueslip.error("undefined sub in stream_setting_changed()"); - return; - } if (has_global_notification_setting(setting) && sub[setting] === null) { sub[setting] = user_settings[settings_config.generalize_stream_notification_setting[setting]]; @@ -608,23 +594,9 @@ export function initialize() { e.stopPropagation(); const stream_id = get_stream_id(e.target); - if (!stream_id) { - ui_report.client_error( - $t_html({defaultMessage: "Invalid channel ID"}), - $(".stream_change_property_info"), - ); - return; - } function do_archive_stream() { const stream_id = Number($(".dialog_submit_button").attr("data-stream-id")); - if (!stream_id) { - ui_report.client_error( - $t_html({defaultMessage: "Invalid channel ID"}), - $(".stream_change_property_info"), - ); - return; - } const $row = $(".stream-row.active"); archive_stream(stream_id, $(".stream_change_property_info"), $row); } From e739e9ae2910f3b8466af2aed271f3d20b536e34 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 11:37:15 -0800 Subject: [PATCH 258/276] stream_edit: Don't set undefined text for email address. `sub.email_address` can be `undefined`, `$foo.text(undefined)` has no effect (it doesn't clear the text), and `text()` doesn't formally accept `undefined`, so here we only call `text()` if the new value isn't undefined. --- web/src/stream_edit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index a69e94545c..da048e239d 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -109,7 +109,9 @@ export function open_edit_panel_empty() { export function update_stream_name(sub, new_name) { const $edit_container = stream_settings_containers.get_edit_container(sub); - $edit_container.find(".email-address").text(sub.email_address); + if (sub.email_address !== undefined) { + $edit_container.find(".email-address").text(sub.email_address); + } $edit_container.find(".sub-stream-name").text(new_name); const active_data = stream_settings_components.get_active_data(); From 686372e8a2f57b2ba12058ce4415f2671bf55aa7 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 11:53:40 -0800 Subject: [PATCH 259/276] stream_edit: Parse notification labels instead of checking substrings. --- web/src/stream_edit.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index da048e239d..b3e839cd7d 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -34,12 +34,15 @@ import * as stream_settings_api from "./stream_settings_api"; import * as stream_settings_components from "./stream_settings_components"; import * as stream_settings_containers from "./stream_settings_containers"; import * as stream_settings_data from "./stream_settings_data"; +import {stream_specific_notification_settings_schema} from "./stream_types"; import * as stream_ui_updates from "./stream_ui_updates"; import * as sub_store from "./sub_store"; import * as ui_report from "./ui_report"; import * as user_groups from "./user_groups"; import {user_settings} from "./user_settings"; +const notification_labels_schema = stream_specific_notification_settings_schema.keyof(); + export function setup_subscriptions_tab_hash(tab_key_value) { if ($("#subscription_overlay .right").hasClass("show")) { return; @@ -159,17 +162,10 @@ function show_subscription_settings(sub) { }); } -function has_global_notification_setting(setting_label) { - if (setting_label.includes("_notifications")) { - return true; - } else if (setting_label.includes("_notify")) { - return true; - } - return false; -} - function is_notification_setting(setting_label) { - return has_global_notification_setting(setting_label) || setting_label === "is_muted"; + return ( + notification_labels_schema.safeParse(setting_label).success || setting_label === "is_muted" + ); } export function stream_settings(sub) { @@ -178,14 +174,15 @@ export function stream_settings(sub) { settings_config.all_notifications(user_settings).disabled_notification_settings; return settings_labels.map(([setting, label]) => { + const notification_setting = notification_labels_schema.safeParse(setting); const ret = { name: setting, label, disabled_realm_setting: check_realm_setting[setting], is_disabled: check_realm_setting[setting], - has_global_notification_setting: has_global_notification_setting(setting), + has_global_notification_setting: notification_setting.success, }; - if (has_global_notification_setting(setting)) { + if (notification_setting.success) { // This block ensures we correctly display to users the // current state of stream-level notification settings // with a value of `null`, which inherit the user's global @@ -331,9 +328,12 @@ function stream_setting_changed(e) { const sub = get_sub_for_target(e.target); const $status_element = $(e.target).closest(".subsection-parent").find(".alert-notification"); const setting = e.target.name; - if (has_global_notification_setting(setting) && sub[setting] === null) { + const notification_setting = notification_labels_schema.safeParse(setting); + if (notification_setting.success && sub[setting] === null) { sub[setting] = - user_settings[settings_config.generalize_stream_notification_setting[setting]]; + user_settings[ + settings_config.generalize_stream_notification_setting[notification_setting.data] + ]; } stream_settings_api.set_stream_property( sub, From 287c7e8f05a9d9065548dd363cba3bf4139ee2f7 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 12:14:14 -0800 Subject: [PATCH 260/276] stream_edit: Pass string instead of function as html_body. It seems like this was working fine, thanks to some internal workings of Handlebars, but when we convert this file to typescript, it will want a string for `html_body`. --- web/src/stream_edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index b3e839cd7d..3220e919dd 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -703,7 +703,7 @@ export function initialize() { } dialog_widget.launch({ html_heading: $t_html({defaultMessage: "Confirm changing access permissions"}), - html_body: render_confirm_stream_privacy_change_modal, + html_body: render_confirm_stream_privacy_change_modal(), id: "confirm_stream_privacy_change", html_submit_button: $t_html({defaultMessage: "Confirm"}), on_click() { From de8caa1b435044af719eb49b82ffc3735998f6ec Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 13:46:40 -0800 Subject: [PATCH 261/276] stream_edit: Use `this` instead of `e.target`. These were all manually checked to still work. `e.currentTarget` is equivalent to `this`, and all instance of `e.target` were either the same as `this` or used with a `closest` function that still got the same value. --- web/src/stream_edit.js | 72 ++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index 3220e919dd..dfcaa82377 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -305,8 +305,8 @@ export function update_muting_rendering(sub) { $edit_container.find(".mute-note").toggleClass("hide-mute-note", !sub.is_muted); } -function stream_notification_reset(e) { - const sub = get_sub_for_target(e.target); +function stream_notification_reset(elem) { + const sub = get_sub_for_target(elem); const data = [{stream_id: sub.stream_id, property: "is_muted", value: false}]; for (const [per_stream_setting_name, global_setting_name] of Object.entries( settings_config.generalize_stream_notification_setting, @@ -320,14 +320,14 @@ function stream_notification_reset(e) { stream_settings_api.bulk_set_stream_property( data, - $(e.target).closest(".subsection-parent").find(".alert-notification"), + $(elem).closest(".subsection-parent").find(".alert-notification"), ); } -function stream_setting_changed(e) { - const sub = get_sub_for_target(e.target); - const $status_element = $(e.target).closest(".subsection-parent").find(".alert-notification"); - const setting = e.target.name; +function stream_setting_changed(elem) { + const sub = get_sub_for_target(elem); + const $status_element = $(elem).closest(".subsection-parent").find(".alert-notification"); + const setting = elem.name; const notification_setting = notification_labels_schema.safeParse(setting); if (notification_setting.success && sub[setting] === null) { sub[setting] = @@ -337,7 +337,7 @@ function stream_setting_changed(e) { } stream_settings_api.set_stream_property( sub, - {property: setting, value: e.target.checked}, + {property: setting, value: elem.checked}, $status_element, ); } @@ -444,10 +444,10 @@ export function initialize() { stream_settings_components.sub_or_unsub(sub); }); - $("#channels_overlay_container").on("click", "#open_stream_info_modal", (e) => { + $("#channels_overlay_container").on("click", "#open_stream_info_modal", function (e) { e.preventDefault(); e.stopPropagation(); - const stream_id = get_stream_id(e.target); + const stream_id = get_stream_id(this); const stream = sub_store.get(stream_id); const template_data = { stream_name: stream.name, @@ -513,8 +513,8 @@ export function initialize() { }, ); - function save_stream_info(e) { - const sub = get_sub_for_target(e.currentTarget); + function save_stream_info() { + const sub = get_sub_for_target(this); const url = `/json/streams/${sub.stream_id}`; const data = {}; @@ -531,11 +531,11 @@ export function initialize() { dialog_widget.submit_api_request(channel.patch, url, data); } - $("#channels_overlay_container").on("click", ".copy_email_button", (e) => { + $("#channels_overlay_container").on("click", ".copy_email_button", function (e) { e.preventDefault(); e.stopPropagation(); - const stream_id = get_stream_id(e.target); + const stream_id = get_stream_id(this); channel.get({ url: "/json/streams/" + stream_id + "/email_address", @@ -556,24 +556,28 @@ export function initialize() { $("#channels_overlay_container").on( "click", ".subsection-parent .reset-stream-notifications-button", - stream_notification_reset, + function on_click() { + stream_notification_reset(this); + }, ); $("#channels_overlay_container").on( "change", ".sub_setting_checkbox .sub_setting_control", - stream_setting_changed, + function on_change() { + stream_setting_changed(this); + }, ); // This handler isn't part of the normal edit interface; it's the convenient // checkmark in the subscriber list. - $("#channels_overlay_container").on("click", ".sub_unsub_button", (e) => { - if ($(e.currentTarget).hasClass("disabled")) { + $("#channels_overlay_container").on("click", ".sub_unsub_button", function (e) { + if ($(this).hasClass("disabled")) { // We do not allow users to subscribe themselves to private streams. return; } - const sub = get_sub_for_target(e.target); + const sub = get_sub_for_target(this); // Makes sure we take the correct stream_row. const $stream_row = $( `#channels_overlay_container div.stream-row[data-stream-id='${CSS.escape( @@ -591,11 +595,11 @@ export function initialize() { e.stopPropagation(); }); - $("#channels_overlay_container").on("click", ".deactivate", (e) => { + $("#channels_overlay_container").on("click", ".deactivate", function (e) { e.preventDefault(); e.stopPropagation(); - const stream_id = get_stream_id(e.target); + const stream_id = get_stream_id(this); function do_archive_stream() { const stream_id = Number($(".dialog_submit_button").attr("data-stream-id")); @@ -640,34 +644,34 @@ export function initialize() { $(".dialog_submit_button").attr("data-stream-id", stream_id); }); - $("#channels_overlay_container").on("click", ".stream-row", function (e) { - if ($(e.target).closest(".check, .subscription_settings").length === 0) { + $("#channels_overlay_container").on("click", ".stream-row", function () { + if ($(this).closest(".check, .subscription_settings").length === 0) { open_edit_panel_for_row(this); } }); - $("#channels_overlay_container").on("change", ".stream_message_retention_setting", (e) => { - const message_retention_setting_dropdown_value = e.target.value; + $("#channels_overlay_container").on("change", ".stream_message_retention_setting", function () { + const message_retention_setting_dropdown_value = this.value; settings_components.change_element_block_display_property( "stream_message_retention_custom_input", message_retention_setting_dropdown_value === "custom_period", ); }); - $("#channels_overlay_container").on("change input", "input, select, textarea", (e) => { + $("#channels_overlay_container").on("change input", "input, select, textarea", function (e) { e.preventDefault(); e.stopPropagation(); - if ($(e.target).hasClass("no-input-change-detection")) { + if ($(this).hasClass("no-input-change-detection")) { // This is to prevent input changes detection in elements // within a subsection whose changes should not affect the // visibility of the discard button return false; } - const stream_id = get_stream_id(e.target); + const stream_id = get_stream_id(this); const sub = sub_store.get(stream_id); - const $subsection = $(e.target).closest(".settings-subsection-parent"); + const $subsection = $(this).closest(".settings-subsection-parent"); settings_components.save_discard_stream_settings_widget_status_handler($subsection, sub); if (sub && $subsection.attr("id") === "stream_permission_settings") { stream_ui_updates.update_default_stream_and_stream_privacy_state($subsection); @@ -678,10 +682,10 @@ export function initialize() { $("#channels_overlay_container").on( "click", ".subsection-header .subsection-changes-save button", - (e) => { + function (e) { e.preventDefault(); e.stopPropagation(); - const $save_button = $(e.currentTarget); + const $save_button = $(this); const $subsection_elem = $save_button.closest(".settings-subsection-parent"); const stream_id = Number( @@ -717,16 +721,16 @@ export function initialize() { $("#channels_overlay_container").on( "click", ".subsection-header .subsection-changes-discard button", - (e) => { + function (e) { e.preventDefault(); e.stopPropagation(); const stream_id = Number( - $(e.target).closest(".subscription_settings.show").attr("data-stream-id"), + $(this).closest(".subscription_settings.show").attr("data-stream-id"), ); const sub = sub_store.get(stream_id); - const $subsection = $(e.target).closest(".settings-subsection-parent"); + const $subsection = $(this).closest(".settings-subsection-parent"); settings_org.discard_stream_settings_subsection_changes($subsection, sub); if ($subsection.attr("id") === "stream_permission_settings") { stream_ui_updates.update_default_stream_and_stream_privacy_state($subsection); From 16e3894bfa26d08b85088cfe93ebf58abfad697a Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 16:31:45 -0800 Subject: [PATCH 262/276] stream_edit: Remove use of e.trigger for copy link button. This will help with the typescript conversion for this file. --- web/src/stream_edit.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index dfcaa82377..6054a5f463 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -40,6 +40,7 @@ import * as sub_store from "./sub_store"; import * as ui_report from "./ui_report"; import * as user_groups from "./user_groups"; import {user_settings} from "./user_settings"; +import * as util from "./util"; const notification_labels_schema = stream_specific_notification_settings_schema.keyof(); @@ -405,15 +406,16 @@ function show_stream_email_address_modal(address) { }); $("#show-sender").prop("checked", true); - const clipboard = new ClipboardJS("#copy_email_address_modal .dialog_submit_button", { + const submit_button = util.the($("#copy_email_address_modal .dialog_submit_button")); + const clipboard = new ClipboardJS(submit_button, { text() { return address; }, }); // Show a tippy tooltip when the stream email address copied - clipboard.on("success", (e) => { - show_copied_confirmation(e.trigger); + clipboard.on("success", () => { + show_copied_confirmation(submit_button); }); $("#copy_email_address_modal .tag-checkbox").on("change", () => { From 01b1a51a8622165d0c0c6ba5965372c8eb9baf45 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Wed, 16 Oct 2024 17:46:07 -0700 Subject: [PATCH 263/276] stream_edit: Convert module to typescript. --- tools/test-js-with-node | 2 +- web/src/peer_data.ts | 2 +- web/src/settings_org.ts | 2 +- web/src/{stream_edit.js => stream_edit.ts} | 392 ++++++++++++--------- web/src/stream_settings_api.ts | 19 +- web/src/stream_settings_components.ts | 5 +- 6 files changed, 248 insertions(+), 174 deletions(-) rename web/src/{stream_edit.js => stream_edit.ts} (70%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 148ae37f49..d14559aa0c 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -243,7 +243,7 @@ EXEMPT_FILES = make_set( "web/src/stream_color_events.ts", "web/src/stream_create.ts", "web/src/stream_create_subscribers.ts", - "web/src/stream_edit.js", + "web/src/stream_edit.ts", "web/src/stream_edit_subscribers.ts", "web/src/stream_edit_toggler.ts", "web/src/stream_list.ts", diff --git a/web/src/peer_data.ts b/web/src/peer_data.ts index 1801cc05b4..20b1fd8093 100644 --- a/web/src/peer_data.ts +++ b/web/src/peer_data.ts @@ -43,7 +43,7 @@ export function potential_subscribers(stream_id: number): User[] { stream. This may include some bots. We currently use it for typeahead in - stream_edit.js. + stream_edit.ts. This may be a superset of the actual subscribers that you can change in some cases diff --git a/web/src/settings_org.ts b/web/src/settings_org.ts index ea8273d1fd..2bdf2b4a6a 100644 --- a/web/src/settings_org.ts +++ b/web/src/settings_org.ts @@ -801,7 +801,7 @@ export function save_organization_settings( data: Record, $save_button: JQuery, patch_url: string, - success_continuation: (() => void) | undefined, + success_continuation: (() => void) | undefined = undefined, ): void { const $subsection_parent = $save_button.closest(".settings-subsection-parent"); const $save_btn_container = $subsection_parent.find(".save-button-controls"); diff --git a/web/src/stream_edit.js b/web/src/stream_edit.ts similarity index 70% rename from web/src/stream_edit.js rename to web/src/stream_edit.ts index 6054a5f463..8fcd591bd9 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.ts @@ -1,5 +1,7 @@ import ClipboardJS from "clipboard"; import $ from "jquery"; +import assert from "minimalistic-assert"; +import {z} from "zod"; import render_settings_deactivation_stream_modal from "../templates/confirm_dialog/confirm_deactivate_stream.hbs"; import render_inline_decorated_stream_name from "../templates/inline_decorated_stream_name.hbs"; @@ -31,20 +33,43 @@ import * as stream_data from "./stream_data"; import * as stream_edit_subscribers from "./stream_edit_subscribers"; import * as stream_edit_toggler from "./stream_edit_toggler"; import * as stream_settings_api from "./stream_settings_api"; +import type {SubData} from "./stream_settings_api"; import * as stream_settings_components from "./stream_settings_components"; import * as stream_settings_containers from "./stream_settings_containers"; import * as stream_settings_data from "./stream_settings_data"; -import {stream_specific_notification_settings_schema} from "./stream_types"; +import type {SettingsSubscription} from "./stream_settings_data"; +import { + stream_properties_schema, + stream_specific_notification_settings_schema, +} from "./stream_types"; import * as stream_ui_updates from "./stream_ui_updates"; import * as sub_store from "./sub_store"; +import type {StreamSubscription} from "./sub_store"; import * as ui_report from "./ui_report"; import * as user_groups from "./user_groups"; import {user_settings} from "./user_settings"; import * as util from "./util"; +type StreamSetting = { + name: z.output; + label: string; + disabled_realm_setting: boolean; + is_disabled: boolean; + has_global_notification_setting: boolean; + is_checked: boolean; +}; + +const settings_labels_schema = stream_properties_schema.omit({color: true}).keyof(); + +const realm_labels_schema = z.enum([ + "push_notifications", + "enable_online_push_notifications", + "message_content_in_email_notifications", +]); + const notification_labels_schema = stream_specific_notification_settings_schema.keyof(); -export function setup_subscriptions_tab_hash(tab_key_value) { +export function setup_subscriptions_tab_hash(tab_key_value: string): void { if ($("#subscription_overlay .right").hasClass("show")) { return; } @@ -67,7 +92,7 @@ export function setup_subscriptions_tab_hash(tab_key_value) { } } -export function get_display_text_for_realm_message_retention_setting() { +export function get_display_text_for_realm_message_retention_setting(): string { const realm_message_retention_days = realm.realm_message_retention_days; if (realm_message_retention_days === settings_config.retain_message_forever) { return $t({defaultMessage: "(forever)"}); @@ -78,19 +103,21 @@ export function get_display_text_for_realm_message_retention_setting() { ); } -function get_stream_id(target) { +function get_stream_id(target: HTMLElement): number { const $row = $(target).closest( ".stream-row, .stream_settings_header, .subscription_settings, .save-button", ); - return Number.parseInt($row.attr("data-stream-id"), 10); + return Number.parseInt($row.attr("data-stream-id")!, 10); } -function get_sub_for_target(target) { +function get_sub_for_target(target: HTMLElement): StreamSubscription { const stream_id = get_stream_id(target); - return sub_store.get(stream_id); + const sub = sub_store.get(stream_id); + assert(sub !== undefined); + return sub; } -export function open_edit_panel_for_row(stream_row) { +export function open_edit_panel_for_row(stream_row: HTMLElement): void { const sub = get_sub_for_target(stream_row); $(".stream-row.active").removeClass("active"); @@ -99,19 +126,22 @@ export function open_edit_panel_for_row(stream_row) { setup_stream_settings(stream_row); } -export function empty_right_panel() { +export function empty_right_panel(): void { $(".stream-row.active").removeClass("active"); $("#subscription_overlay .right").removeClass("show"); stream_settings_components.show_subs_pane.nothing_selected(); } -export function open_edit_panel_empty() { - const tab_key = stream_settings_components.get_active_data().$tabs.first().attr("data-tab-key"); +export function open_edit_panel_empty(): void { + const tab_key = stream_settings_components + .get_active_data() + .$tabs.first() + .attr("data-tab-key")!; empty_right_panel(); setup_subscriptions_tab_hash(tab_key); } -export function update_stream_name(sub, new_name) { +export function update_stream_name(sub: StreamSubscription, new_name: string): void { const $edit_container = stream_settings_containers.get_edit_container(sub); if (sub.email_address !== undefined) { $edit_container.find(".email-address").text(sub.email_address); @@ -124,7 +154,7 @@ export function update_stream_name(sub, new_name) { } } -export function update_stream_description(sub) { +export function update_stream_description(sub: StreamSubscription): void { const $edit_container = stream_settings_containers.get_edit_container(sub); $edit_container.find("input.description").val(sub.description); const html = render_stream_description({ @@ -133,7 +163,7 @@ export function update_stream_description(sub) { $edit_container.find(".stream-description").html(html); } -function show_subscription_settings(sub) { +function show_subscription_settings(sub: SettingsSubscription): void { const $edit_container = stream_settings_containers.get_edit_container(sub); const $colorpicker = $edit_container.find(".colorpicker"); @@ -163,42 +193,48 @@ function show_subscription_settings(sub) { }); } -function is_notification_setting(setting_label) { +function is_notification_setting(setting_label: string): boolean { return ( notification_labels_schema.safeParse(setting_label).success || setting_label === "is_muted" ); } -export function stream_settings(sub) { +export function stream_settings(sub: StreamSubscription): StreamSetting[] { const settings_labels = settings_config.general_notifications_table_labels.stream; const check_realm_setting = settings_config.all_notifications(user_settings).disabled_notification_settings; return settings_labels.map(([setting, label]) => { + const parsed_realm_setting = realm_labels_schema.safeParse(setting); + const realm_setting = parsed_realm_setting.success + ? check_realm_setting[parsed_realm_setting.data] + : false; const notification_setting = notification_labels_schema.safeParse(setting); - const ret = { - name: setting, - label, - disabled_realm_setting: check_realm_setting[setting], - is_disabled: check_realm_setting[setting], - has_global_notification_setting: notification_setting.success, - }; + + let is_checked; if (notification_setting.success) { // This block ensures we correctly display to users the // current state of stream-level notification settings // with a value of `null`, which inherit the user's global // notification settings for streams. - ret.is_checked = - stream_data.receives_notifications(sub.stream_id, setting) && - !check_realm_setting[setting]; - return ret; + is_checked = + stream_data.receives_notifications(sub.stream_id, notification_setting.data) && + !realm_setting; + } else { + is_checked = Boolean(sub[setting]) && !realm_setting; } - ret.is_checked = sub[setting] && !check_realm_setting[setting]; - return ret; + return { + name: setting, + label, + disabled_realm_setting: realm_setting, + is_disabled: realm_setting, + has_global_notification_setting: notification_setting.success, + is_checked, + }; }); } -function setup_dropdown(sub, slim_sub) { +function setup_dropdown(sub: StreamSubscription, slim_sub: StreamSubscription): void { const can_remove_subscribers_group_widget = new dropdown_widget.DropdownWidget({ widget_name: "can_remove_subscribers_group", get_options: () => @@ -231,17 +267,18 @@ function setup_dropdown(sub, slim_sub) { can_remove_subscribers_group_widget.setup(); } -export function show_settings_for(node) { +export function show_settings_for(node: HTMLElement): void { // Hide any tooltips or popovers before we rerender / change // currently displayed stream settings. popovers.hide_all(); const stream_id = get_stream_id(node); const slim_sub = sub_store.get(stream_id); + assert(slim_sub !== undefined); stream_data.clean_up_description(slim_sub); const sub = stream_settings_data.get_sub_for_settings(slim_sub); const all_settings = stream_settings(sub); - const other_settings = []; + const other_settings: StreamSetting[] = []; const notification_settings = all_settings.filter((setting) => { if (is_notification_setting(setting.name)) { return true; @@ -293,12 +330,12 @@ export function show_settings_for(node) { ); } -export function setup_stream_settings(node) { +export function setup_stream_settings(node: HTMLElement): void { stream_edit_toggler.setup_toggler(); show_settings_for(node); } -export function update_muting_rendering(sub) { +export function update_muting_rendering(sub: StreamSubscription): void { const $edit_container = stream_settings_containers.get_edit_container(sub); const $is_muted_checkbox = $edit_container.find("#sub_is_muted_setting .sub_setting_control"); @@ -306,15 +343,15 @@ export function update_muting_rendering(sub) { $edit_container.find(".mute-note").toggleClass("hide-mute-note", !sub.is_muted); } -function stream_notification_reset(elem) { +function stream_notification_reset(elem: HTMLElement): void { const sub = get_sub_for_target(elem); - const data = [{stream_id: sub.stream_id, property: "is_muted", value: false}]; + const data: SubData = [{stream_id: sub.stream_id, property: "is_muted", value: false}]; for (const [per_stream_setting_name, global_setting_name] of Object.entries( settings_config.generalize_stream_notification_setting, )) { data.push({ stream_id: sub.stream_id, - property: per_stream_setting_name, + property: settings_labels_schema.parse(per_stream_setting_name), value: user_settings[global_setting_name], }); } @@ -325,10 +362,10 @@ function stream_notification_reset(elem) { ); } -function stream_setting_changed(elem) { +function stream_setting_changed(elem: HTMLInputElement): void { const sub = get_sub_for_target(elem); const $status_element = $(elem).closest(".subsection-parent").find(".alert-notification"); - const setting = elem.name; + const setting = settings_labels_schema.parse(elem.name); const notification_setting = notification_labels_schema.safeParse(setting); if (notification_setting.success && sub[setting] === null) { sub[setting] = @@ -343,7 +380,11 @@ function stream_setting_changed(elem) { ); } -export function archive_stream(stream_id, $alert_element, $stream_row) { +export function archive_stream( + stream_id: number, + $alert_element: JQuery, + $stream_row: JQuery, +): void { channel.del({ url: "/json/streams/" + stream_id, error(xhr) { @@ -355,7 +396,7 @@ export function archive_stream(stream_id, $alert_element, $stream_row) { }); } -export function get_stream_email_address(flags, address) { +export function get_stream_email_address(flags: string[], address: string): string { const clean_address = address .replace(".show-sender", "") .replace(".include-footer", "") @@ -367,7 +408,7 @@ export function get_stream_email_address(flags, address) { return clean_address.replace("@", flag_string + "@"); } -function show_stream_email_address_modal(address) { +function show_stream_email_address_modal(address: string): void { const copy_email_address_modal_html = render_copy_email_address_modal({ email_address: address, tags: [ @@ -401,7 +442,9 @@ function show_stream_email_address_modal(address) { html_submit_button: $t_html({defaultMessage: "Copy address"}), html_exit_button: $t_html({defaultMessage: "Close"}), help_link: "/help/message-a-channel-by-email#configuration-options", - on_click() {}, + on_click() { + // This is handled by the ClipboardJS object below. + }, close_on_submit: false, }); $("#show-sender").prop("checked", true); @@ -421,10 +464,10 @@ function show_stream_email_address_modal(address) { $("#copy_email_address_modal .tag-checkbox").on("change", () => { const $checked_checkboxes = $(".copy-email-modal").find("input:checked"); - const flags = []; + const flags: string[] = []; $($checked_checkboxes).each(function () { - flags.push($(this).attr("id")); + flags.push($(this).attr("id")!); }); address = get_stream_email_address(flags, address); @@ -433,7 +476,7 @@ function show_stream_email_address_modal(address) { }); } -export function initialize() { +export function initialize(): void { $("#main_div").on("click", ".stream_sub_unsub_button", (e) => { e.preventDefault(); e.stopPropagation(); @@ -446,35 +489,40 @@ export function initialize() { stream_settings_components.sub_or_unsub(sub); }); - $("#channels_overlay_container").on("click", "#open_stream_info_modal", function (e) { - e.preventDefault(); - e.stopPropagation(); - const stream_id = get_stream_id(this); - const stream = sub_store.get(stream_id); - const template_data = { - stream_name: stream.name, - stream_description: stream.description, - max_stream_name_length: realm.max_stream_name_length, - max_stream_description_length: realm.max_stream_description_length, - }; - const change_stream_info_modal = render_change_stream_info_modal(template_data); - dialog_widget.launch({ - html_heading: $t_html( - {defaultMessage: "Edit #{channel_name}"}, - {channel_name: stream.name}, - ), - html_body: change_stream_info_modal, - id: "change_stream_info_modal", - loading_spinner: true, - on_click: save_stream_info, - post_render() { - $("#change_stream_info_modal .dialog_submit_button") - .addClass("save-button") - .attr("data-stream-id", stream_id); - }, - update_submit_disabled_state_on_change: true, - }); - }); + $("#channels_overlay_container").on( + "click", + "#open_stream_info_modal", + function (this: HTMLElement, e) { + e.preventDefault(); + e.stopPropagation(); + const stream_id = get_stream_id(this); + const stream = sub_store.get(stream_id); + assert(stream !== undefined); + const template_data = { + stream_name: stream.name, + stream_description: stream.description, + max_stream_name_length: realm.max_stream_name_length, + max_stream_description_length: realm.max_stream_description_length, + }; + const change_stream_info_modal = render_change_stream_info_modal(template_data); + dialog_widget.launch({ + html_heading: $t_html( + {defaultMessage: "Edit #{channel_name}"}, + {channel_name: stream.name}, + ), + html_body: change_stream_info_modal, + id: "change_stream_info_modal", + loading_spinner: true, + on_click: save_stream_info, + post_render() { + $("#change_stream_info_modal .dialog_submit_button") + .addClass("save-button") + .attr("data-stream-id", stream_id); + }, + update_submit_disabled_state_on_change: true, + }); + }, + ); $("#channels_overlay_container").on("keypress", "#change_stream_description", (e) => { // Stream descriptions cannot be multiline, so disable enter key @@ -502,26 +550,30 @@ export function initialize() { event.stopPropagation(); const $target = $(event.target).parents(".main-view-banner"); - const stream_id = Number.parseInt($target.attr("data-stream-id"), 10); + const stream_id = Number.parseInt($target.attr("data-stream-id")!, 10); // Makes sure we take the correct stream_row. const $stream_row = $( `#channels_overlay_container div.stream-row[data-stream-id='${CSS.escape( - stream_id, + stream_id.toString(), )}']`, ); const sub = sub_store.get(stream_id); + assert(sub !== undefined); stream_settings_components.sub_or_unsub(sub, $stream_row); $("#stream_permission_settings .stream-permissions-warning-banner").empty(); }, ); - function save_stream_info() { - const sub = get_sub_for_target(this); - + function save_stream_info(): void { + const sub = get_sub_for_target( + util.the($("#change_stream_info_modal .dialog_submit_button")), + ); const url = `/json/streams/${sub.stream_id}`; - const data = {}; - const new_name = $("#change_stream_name").val().trim(); - const new_description = $("#change_stream_description").val().trim(); + const data: {new_name?: string; description?: string} = {}; + const new_name = $("input#change_stream_name").val()!.trim(); + const new_description = $("textarea#change_stream_description") + .val()! + .trim(); if (new_name !== sub.name) { data.new_name = new_name; @@ -533,82 +585,89 @@ export function initialize() { dialog_widget.submit_api_request(channel.patch, url, data); } - $("#channels_overlay_container").on("click", ".copy_email_button", function (e) { - e.preventDefault(); - e.stopPropagation(); - - const stream_id = get_stream_id(this); - - channel.get({ - url: "/json/streams/" + stream_id + "/email_address", - success(data) { - const address = data.email; - show_stream_email_address_modal(address); - }, - error(xhr) { - ui_report.error( - $t_html({defaultMessage: "Failed"}), - xhr, - $(".stream_email_address_error"), - ); - }, - }); - }); - $("#channels_overlay_container").on( "click", - ".subsection-parent .reset-stream-notifications-button", - function on_click() { - stream_notification_reset(this); + ".copy_email_button", + function (this: HTMLElement, e) { + e.preventDefault(); + e.stopPropagation(); + + const stream_id = get_stream_id(this); + + channel.get({ + url: "/json/streams/" + stream_id + "/email_address", + success(data) { + const address = z.object({email: z.string()}).parse(data).email; + show_stream_email_address_modal(address); + }, + error(xhr) { + ui_report.error( + $t_html({defaultMessage: "Failed"}), + xhr, + $(".stream_email_address_error"), + ); + }, + }); }, ); $("#channels_overlay_container").on( + "click", + ".subsection-parent .reset-stream-notifications-button", + function on_click(this: HTMLElement) { + stream_notification_reset(this); + }, + ); + + $("input#channels_overlay_container").on( "change", ".sub_setting_checkbox .sub_setting_control", - function on_change() { + function on_change(this: HTMLInputElement) { stream_setting_changed(this); }, ); // This handler isn't part of the normal edit interface; it's the convenient // checkmark in the subscriber list. - $("#channels_overlay_container").on("click", ".sub_unsub_button", function (e) { - if ($(this).hasClass("disabled")) { - // We do not allow users to subscribe themselves to private streams. - return; - } + $("#channels_overlay_container").on( + "click", + ".sub_unsub_button", + function (this: HTMLElement, e) { + if ($(this).hasClass("disabled")) { + // We do not allow users to subscribe themselves to private streams. + return; + } - const sub = get_sub_for_target(this); - // Makes sure we take the correct stream_row. - const $stream_row = $( - `#channels_overlay_container div.stream-row[data-stream-id='${CSS.escape( - sub.stream_id, - )}']`, - ); - stream_settings_components.sub_or_unsub(sub, $stream_row); + const sub = get_sub_for_target(this); + // Makes sure we take the correct stream_row. + const $stream_row = $( + `#channels_overlay_container div.stream-row[data-stream-id='${CSS.escape( + sub.stream_id.toString(), + )}']`, + ); + stream_settings_components.sub_or_unsub(sub, $stream_row); - if (!sub.subscribed) { - open_edit_panel_for_row($stream_row); - } - stream_ui_updates.update_regular_sub_settings(sub); + if (!sub.subscribed) { + open_edit_panel_for_row(util.the($stream_row)); + } + stream_ui_updates.update_regular_sub_settings(sub); - e.preventDefault(); - e.stopPropagation(); - }); + e.preventDefault(); + e.stopPropagation(); + }, + ); - $("#channels_overlay_container").on("click", ".deactivate", function (e) { + $("#channels_overlay_container").on("click", ".deactivate", function (this: HTMLElement, e) { e.preventDefault(); e.stopPropagation(); - const stream_id = get_stream_id(this); - - function do_archive_stream() { + function do_archive_stream(): void { const stream_id = Number($(".dialog_submit_button").attr("data-stream-id")); const $row = $(".stream-row.active"); archive_stream(stream_id, $(".stream_change_property_info"), $row); } + const stream_id = get_stream_id(this); const stream = sub_store.get(stream_id); const stream_name_with_privacy_symbol_html = render_inline_decorated_stream_name({stream}); @@ -646,45 +705,56 @@ export function initialize() { $(".dialog_submit_button").attr("data-stream-id", stream_id); }); - $("#channels_overlay_container").on("click", ".stream-row", function () { + $("#channels_overlay_container").on("click", ".stream-row", function (this: HTMLElement) { if ($(this).closest(".check, .subscription_settings").length === 0) { open_edit_panel_for_row(this); } }); - $("#channels_overlay_container").on("change", ".stream_message_retention_setting", function () { - const message_retention_setting_dropdown_value = this.value; - settings_components.change_element_block_display_property( - "stream_message_retention_custom_input", - message_retention_setting_dropdown_value === "custom_period", - ); - }); + $("#channels_overlay_container").on( + "change", + "select.stream_message_retention_setting", + function (this: HTMLSelectElement) { + const message_retention_setting_dropdown_value = this.value; + settings_components.change_element_block_display_property( + "stream_message_retention_custom_input", + message_retention_setting_dropdown_value === "custom_period", + ); + }, + ); - $("#channels_overlay_container").on("change input", "input, select, textarea", function (e) { - e.preventDefault(); - e.stopPropagation(); + $("#channels_overlay_container").on( + "change input", + "input, select, textarea", + function (this: HTMLElement, e): boolean { + e.preventDefault(); + e.stopPropagation(); - if ($(this).hasClass("no-input-change-detection")) { - // This is to prevent input changes detection in elements - // within a subsection whose changes should not affect the - // visibility of the discard button - return false; - } + if ($(this).hasClass("no-input-change-detection")) { + // This is to prevent input changes detection in elements + // within a subsection whose changes should not affect the + // visibility of the discard button + return false; + } - const stream_id = get_stream_id(this); - const sub = sub_store.get(stream_id); - const $subsection = $(this).closest(".settings-subsection-parent"); - settings_components.save_discard_stream_settings_widget_status_handler($subsection, sub); - if (sub && $subsection.attr("id") === "stream_permission_settings") { - stream_ui_updates.update_default_stream_and_stream_privacy_state($subsection); - } - return true; - }); + const stream_id = get_stream_id(this); + const sub = sub_store.get(stream_id); + const $subsection = $(this).closest(".settings-subsection-parent"); + settings_components.save_discard_stream_settings_widget_status_handler( + $subsection, + sub, + ); + if (sub && $subsection.attr("id") === "stream_permission_settings") { + stream_ui_updates.update_default_stream_and_stream_privacy_state($subsection); + } + return true; + }, + ); $("#channels_overlay_container").on( "click", ".subsection-header .subsection-changes-save button", - function (e) { + function (this: HTMLElement, e) { e.preventDefault(); e.stopPropagation(); const $save_button = $(this); @@ -694,6 +764,7 @@ export function initialize() { $save_button.closest(".subscription_settings.show").attr("data-stream-id"), ); const sub = sub_store.get(stream_id); + assert(sub !== undefined); const data = settings_components.populate_data_for_stream_settings_request( $subsection_elem, sub, @@ -731,6 +802,7 @@ export function initialize() { $(this).closest(".subscription_settings.show").attr("data-stream-id"), ); const sub = sub_store.get(stream_id); + assert(sub !== undefined); const $subsection = $(this).closest(".settings-subsection-parent"); settings_org.discard_stream_settings_subsection_changes($subsection, sub); diff --git a/web/src/stream_settings_api.ts b/web/src/stream_settings_api.ts index e81c53fb77..30fe753b94 100644 --- a/web/src/stream_settings_api.ts +++ b/web/src/stream_settings_api.ts @@ -5,16 +5,15 @@ import * as settings_ui from "./settings_ui"; import type {StreamProperties, StreamSubscription} from "./sub_store"; import * as sub_store from "./sub_store"; -export function bulk_set_stream_property( - sub_data: { - [Property in keyof StreamProperties]: { - stream_id: number; - property: Property; - value: StreamProperties[Property]; - }; - }[keyof StreamProperties][], - $status_element?: JQuery, -): void { +export type SubData = { + [Property in keyof StreamProperties]: { + stream_id: number; + property: Property; + value: StreamProperties[Property]; + }; +}[keyof StreamProperties][]; + +export function bulk_set_stream_property(sub_data: SubData, $status_element?: JQuery): void { const url = "/json/users/me/subscriptions/properties"; const data = {subscription_data: JSON.stringify(sub_data)}; if (!$status_element) { diff --git a/web/src/stream_settings_components.ts b/web/src/stream_settings_components.ts index 2e54db013c..a936255f35 100644 --- a/web/src/stream_settings_components.ts +++ b/web/src/stream_settings_components.ts @@ -280,7 +280,10 @@ export function unsubscribe_from_private_stream(sub: StreamSubscription): void { }); } -export function sub_or_unsub(sub: StreamSubscription, $stream_row: JQuery): void { +export function sub_or_unsub( + sub: StreamSubscription, + $stream_row: JQuery | undefined = undefined, +): void { if (sub.subscribed) { // TODO: This next line should allow guests to access web-public streams. if (sub.invite_only || current_user.is_guest) { From 85f8665851c88df786986b35e2c75cec83862dd1 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 4 Nov 2024 18:55:44 -0800 Subject: [PATCH 264/276] util: Move compare_a_b from user_sort and generalize it. --- web/src/settings_users.js | 7 ++++--- web/src/user_group_components.ts | 5 +++-- web/src/user_sort.ts | 10 +--------- web/src/util.ts | 9 +++++++++ web/tests/util.test.js | 26 ++++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/web/src/settings_users.js b/web/src/settings_users.js index 2d964370cc..b3e0b86f32 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.js @@ -22,6 +22,7 @@ import * as timerender from "./timerender"; import * as user_deactivation_ui from "./user_deactivation_ui"; import * as user_profile from "./user_profile"; import * as user_sort from "./user_sort"; +import * as util from "./util"; export const active_user_list_dropdown_widget_name = "active_user_list_select_user_role"; export const deactivated_user_list_dropdown_widget_name = "deactivated_user_list_select_user_role"; @@ -54,7 +55,7 @@ function sort_bot_email(a, b) { return (bot.display_email || "").toLowerCase(); } - return user_sort.compare_a_b(email(a), email(b)); + return util.compare_a_b(email(a), email(b)); } function sort_bot_owner(a, b) { @@ -62,11 +63,11 @@ function sort_bot_owner(a, b) { return (bot.bot_owner_full_name || "").toLowerCase(); } - return user_sort.compare_a_b(owner_name(a), owner_name(b)); + return util.compare_a_b(owner_name(a), owner_name(b)); } function sort_last_active(a, b) { - return user_sort.compare_a_b( + return util.compare_a_b( presence.last_active_date(a.user_id) || 0, presence.last_active_date(b.user_id) || 0, ); diff --git a/web/src/user_group_components.ts b/web/src/user_group_components.ts index d3a774c76a..9ac44ef421 100644 --- a/web/src/user_group_components.ts +++ b/web/src/user_group_components.ts @@ -5,6 +5,7 @@ import * as people from "./people"; import type {User} from "./people"; import type {UserGroup} from "./user_groups"; import * as user_sort from "./user_sort"; +import * as util from "./util"; export let active_group_id: number | undefined; @@ -77,7 +78,7 @@ export function sort_group_member_email(a: User | UserGroup, b: User | UserGroup return 1; } - return user_sort.compare_a_b(a.name.toLowerCase(), b.name.toLowerCase()); + return util.compare_a_b(a.name.toLowerCase(), b.name.toLowerCase()); } export function sort_group_member_name(a: User | UserGroup, b: User | UserGroup): number { @@ -95,7 +96,7 @@ export function sort_group_member_name(a: User | UserGroup, b: User | UserGroup) b_name = b.name; } - return user_sort.compare_a_b(a_name.toLowerCase(), b_name.toLowerCase()); + return util.compare_a_b(a_name.toLowerCase(), b_name.toLowerCase()); } export function build_group_member_matcher(query: string): (member: User | UserGroup) => boolean { diff --git a/web/src/user_sort.ts b/web/src/user_sort.ts index 750b62ddb6..ecbf2f2dff 100644 --- a/web/src/user_sort.ts +++ b/web/src/user_sort.ts @@ -1,13 +1,5 @@ import type {User} from "./people"; - -export function compare_a_b(a: number | string, b: number | string): number { - if (a > b) { - return 1; - } else if (a === b) { - return 0; - } - return -1; -} +import {compare_a_b} from "./util"; export function sort_email(a: User, b: User): number { const email_a = a.delivery_email; diff --git a/web/src/util.ts b/web/src/util.ts index 78345526af..eaacb89808 100644 --- a/web/src/util.ts +++ b/web/src/util.ts @@ -488,3 +488,12 @@ export function the(items: T[] | JQuery): T { } return items[0]!; } + +export function compare_a_b(a: T, b: T): number { + if (a > b) { + return 1; + } else if (a === b) { + return 0; + } + return -1; +} diff --git a/web/tests/util.test.js b/web/tests/util.test.js index 9e7c082bcb..9744eb8e40 100644 --- a/web/tests/util.test.js +++ b/web/tests/util.test.js @@ -443,3 +443,29 @@ run_test("the", () => { // were previously typed wrong but not breaking the app. assert.equal(util.the([]), undefined); }); + +run_test("compare_a_b", () => { + const user1 = { + id: 1, + name: "sally", + }; + const user2 = { + id: 2, + name: "jenny", + }; + const user3 = { + id: 3, + name: "max", + }; + const user4 = { + id: 4, + name: "max", + }; + const unsorted = [user2, user1, user4, user3]; + + const sorted_by_id = [...unsorted].sort((a, b) => util.compare_a_b(a.id, b.id)); + assert.deepEqual(sorted_by_id, [user1, user2, user3, user4]); + + const sorted_by_name = [...unsorted].sort((a, b) => util.compare_a_b(a.name, b.name)); + assert.deepEqual(sorted_by_name, [user2, user4, user3, user1]); +}); From 7ca644b905b9eef32322363818ec32e847e2b9d0 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 4 Nov 2024 19:03:09 -0800 Subject: [PATCH 265/276] settings_users: Make separate variables for each section. This will help with the conversion to typescript. --- web/src/settings_users.js | 90 +++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/web/src/settings_users.js b/web/src/settings_users.js index b3e0b86f32..4a9cb20c7f 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.js @@ -30,26 +30,26 @@ export const deactivated_user_list_dropdown_widget_name = "deactivated_user_list let should_redraw_active_users_list = false; let should_redraw_deactivated_users_list = false; -const section = { - active: { - dropdown_widget_name: active_user_list_dropdown_widget_name, - filters: { - text_search: "", - // 0 role_code signifies All roles for our filter. - role_code: 0, - }, +const active_section = { + dropdown_widget_name: active_user_list_dropdown_widget_name, + filters: { + text_search: "", + // 0 role_code signifies All roles for our filter. + role_code: 0, }, - deactivated: { - dropdown_widget_name: deactivated_user_list_dropdown_widget_name, - filters: { - text_search: "", - // 0 role_code signifies All roles for our filter. - role_code: 0, - }, - }, - bots: {}, }; +const deactivated_section = { + dropdown_widget_name: deactivated_user_list_dropdown_widget_name, + filters: { + text_search: "", + // 0 role_code signifies All roles for our filter. + role_code: 0, + }, +}; + +const bots_section = {}; + function sort_bot_email(a, b) { function email(bot) { return (bot.display_email || "").toLowerCase(); @@ -141,10 +141,10 @@ function role_selected_handler(event, dropdown, widget) { event.stopPropagation(); const role_code = Number($(event.currentTarget).attr("data-unique-id")); - if (widget.widget_name === section.active.dropdown_widget_name) { - add_value_to_filters(section.active, "role_code", role_code); - } else if (widget.widget_name === section.deactivated.dropdown_widget_name) { - add_value_to_filters(section.deactivated, "role_code", role_code); + if (widget.widget_name === active_section.dropdown_widget_name) { + add_value_to_filters(active_section, "role_code", role_code); + } else if (widget.widget_name === deactivated_section.dropdown_widget_name) { + add_value_to_filters(deactivated_section, "role_code", role_code); } dropdown.hide(); @@ -185,10 +185,10 @@ function populate_users() { failed_listing_users(); } - section.active.create_table(active_user_ids); - section.deactivated.create_table(deactivated_user_ids); - create_role_filter_dropdown($("#admin-user-list"), section.active); - create_role_filter_dropdown($("#admin-deactivated-users-list"), section.deactivated); + active_section.create_table(active_user_ids); + deactivated_section.create_table(deactivated_user_ids); + create_role_filter_dropdown($("#admin-user-list"), active_section); + create_role_filter_dropdown($("#admin-deactivated-users-list"), deactivated_section); } function reset_scrollbar($sel) { @@ -297,7 +297,7 @@ function set_text_search_value($table, value) { let bot_list_widget; -section.bots.create_table = () => { +bots_section.create_table = () => { loading.make_indicator($("#admin_page_bots_loading_indicator"), { text: $t({defaultMessage: "Loading…"}), }); @@ -338,9 +338,9 @@ section.bots.create_table = () => { $bots_table.show(); }; -section.active.create_table = (active_users) => { +active_section.create_table = (active_users) => { const $users_table = $("#admin_users_table"); - section.active.list_widget = ListWidget.create($users_table, active_users, { + active_section.list_widget = ListWidget.create($users_table, active_users, { name: "users_table_list", get_item: people.get_by_user_id, modifier_html(item) { @@ -350,7 +350,7 @@ section.active.create_table = (active_users) => { }, filter: { predicate(person) { - return people.predicate_for_user_settings_filters(person, section.active.filters); + return people.predicate_for_user_settings_filters(person, active_section.filters); }, onupdate: reset_scrollbar($users_table), }, @@ -366,14 +366,14 @@ section.active.create_table = (active_users) => { $simplebar_container: $("#admin-active-users-list .progressive-table-wrapper"), }); - set_text_search_value($users_table, section.active.filters.text_search); + set_text_search_value($users_table, active_section.filters.text_search); loading.destroy_indicator($("#admin_page_users_loading_indicator")); $("#admin_users_table").show(); }; -section.deactivated.create_table = (deactivated_users) => { +deactivated_section.create_table = (deactivated_users) => { const $deactivated_users_table = $("#admin_deactivated_users_table"); - section.deactivated.list_widget = ListWidget.create( + deactivated_section.list_widget = ListWidget.create( $deactivated_users_table, deactivated_users, { @@ -388,7 +388,7 @@ section.deactivated.create_table = (deactivated_users) => { predicate(person) { return people.predicate_for_user_settings_filters( person, - section.deactivated.filters, + deactivated_section.filters, ); }, onupdate: reset_scrollbar($deactivated_users_table), @@ -405,7 +405,7 @@ section.deactivated.create_table = (deactivated_users) => { }, ); - set_text_search_value($deactivated_users_table, section.deactivated.filters.text_search); + set_text_search_value($deactivated_users_table, deactivated_section.filters.text_search); loading.destroy_indicator($("#admin_page_deactivated_users_loading_indicator")); $("#admin_deactivated_users_table").show(); }; @@ -460,7 +460,7 @@ export function redraw_deactivated_users_list() { return; } const deactivated_user_ids = people.get_non_active_human_ids(); - redraw_users_list(section.deactivated, deactivated_user_ids); + redraw_users_list(deactivated_section, deactivated_user_ids); should_redraw_deactivated_users_list = false; } @@ -469,7 +469,7 @@ export function redraw_active_users_list() { return; } const active_user_ids = people.get_realm_active_human_user_ids(); - redraw_users_list(section.active, active_user_ids); + redraw_users_list(active_section, active_user_ids); should_redraw_active_users_list = false; } @@ -589,25 +589,25 @@ function handle_filter_change($tbody, section) { }); } -section.active.handle_events = () => { +active_section.handle_events = () => { const $tbody = $("#admin_users_table").expectOne(); - handle_filter_change($tbody, section.active); + handle_filter_change($tbody, active_section); handle_deactivation($tbody); handle_reactivation($tbody); handle_edit_form($tbody); }; -section.deactivated.handle_events = () => { +deactivated_section.handle_events = () => { const $tbody = $("#admin_deactivated_users_table").expectOne(); - handle_filter_change($tbody, section.deactivated); + handle_filter_change($tbody, deactivated_section); handle_deactivation($tbody); handle_reactivation($tbody); handle_edit_form($tbody); }; -section.bots.handle_events = () => { +bots_section.handle_events = () => { const $tbody = $("#admin_bots_table").expectOne(); handle_bot_deactivation($tbody); @@ -617,14 +617,14 @@ section.bots.handle_events = () => { export function set_up_humans() { start_data_load(); - section.active.handle_events(); - section.deactivated.handle_events(); + active_section.handle_events(); + deactivated_section.handle_events(); setting_invites.set_up(); } export function set_up_bots() { - section.bots.handle_events(); - section.bots.create_table(); + bots_section.handle_events(); + bots_section.create_table(); $("#admin-bot-list .add-a-new-bot").on("click", (e) => { e.preventDefault(); From 7fbdefcfc0665dab8e85be01e7ac25c420e3be49 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 4 Nov 2024 19:14:27 -0800 Subject: [PATCH 266/276] settings_users: Initialize full section objects. Typescript likes objects to be fully initialized up front, instead of adding functions to them later. --- web/src/settings_users.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/web/src/settings_users.js b/web/src/settings_users.js index 4a9cb20c7f..05fc0bee6c 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.js @@ -37,6 +37,9 @@ const active_section = { // 0 role_code signifies All roles for our filter. role_code: 0, }, + handle_events: active_handle_events, + create_table: active_create_table, + list_widget: undefined, }; const deactivated_section = { @@ -46,9 +49,15 @@ const deactivated_section = { // 0 role_code signifies All roles for our filter. role_code: 0, }, + handle_events: deactivated_handle_events, + create_table: deactivated_create_table, + list_widget: undefined, }; -const bots_section = {}; +const bots_section = { + handle_events: bots_handle_events, + create_table: bots_create_table, +}; function sort_bot_email(a, b) { function email(bot) { @@ -297,7 +306,7 @@ function set_text_search_value($table, value) { let bot_list_widget; -bots_section.create_table = () => { +function bots_create_table() { loading.make_indicator($("#admin_page_bots_loading_indicator"), { text: $t({defaultMessage: "Loading…"}), }); @@ -336,9 +345,9 @@ bots_section.create_table = () => { loading.destroy_indicator($("#admin_page_bots_loading_indicator")); $bots_table.show(); -}; +} -active_section.create_table = (active_users) => { +function active_create_table(active_users) { const $users_table = $("#admin_users_table"); active_section.list_widget = ListWidget.create($users_table, active_users, { name: "users_table_list", @@ -369,9 +378,9 @@ active_section.create_table = (active_users) => { set_text_search_value($users_table, active_section.filters.text_search); loading.destroy_indicator($("#admin_page_users_loading_indicator")); $("#admin_users_table").show(); -}; +} -deactivated_section.create_table = (deactivated_users) => { +function deactivated_create_table(deactivated_users) { const $deactivated_users_table = $("#admin_deactivated_users_table"); deactivated_section.list_widget = ListWidget.create( $deactivated_users_table, @@ -408,7 +417,7 @@ deactivated_section.create_table = (deactivated_users) => { set_text_search_value($deactivated_users_table, deactivated_section.filters.text_search); loading.destroy_indicator($("#admin_page_deactivated_users_loading_indicator")); $("#admin_deactivated_users_table").show(); -}; +} export function update_bot_data(bot_user_id) { if (!bot_list_widget) { @@ -589,31 +598,31 @@ function handle_filter_change($tbody, section) { }); } -active_section.handle_events = () => { +function active_handle_events() { const $tbody = $("#admin_users_table").expectOne(); handle_filter_change($tbody, active_section); handle_deactivation($tbody); handle_reactivation($tbody); handle_edit_form($tbody); -}; +} -deactivated_section.handle_events = () => { +function deactivated_handle_events() { const $tbody = $("#admin_deactivated_users_table").expectOne(); handle_filter_change($tbody, deactivated_section); handle_deactivation($tbody); handle_reactivation($tbody); handle_edit_form($tbody); -}; +} -bots_section.handle_events = () => { +function bots_handle_events() { const $tbody = $("#admin_bots_table").expectOne(); handle_bot_deactivation($tbody); handle_reactivation($tbody); handle_edit_form($tbody); -}; +} export function set_up_humans() { start_data_load(); From 0e3374dc1b9e07a537e6a0796785e54c7979a582 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 4 Nov 2024 19:51:59 -0800 Subject: [PATCH 267/276] settings_users: Don't use `text()` with undefined value. `text(undefined)` is a noop, and also isn't validly typed. --- web/src/settings_users.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/settings_users.js b/web/src/settings_users.js index 05fc0bee6c..5e0f87cd0c 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.js @@ -440,7 +440,10 @@ export function update_user_data(user_id, new_data) { } if (new_data.role !== undefined) { - $user_row.find(".user_role").text(people.get_user_type(user_id)); + const user_type = people.get_user_type(user_id); + if (user_type) { + $user_row.find(".user_role").text(user_type); + } } } From 6aa08b9c50721d005ba61a92439622f0c2114ae7 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Mon, 4 Nov 2024 22:14:56 -0800 Subject: [PATCH 268/276] settings_users: Remove unnecessary bot user undefined check. Users are fetched from `people_by_user_id_dict`. `bot_info` is only called from: * `bots_create_table`, with ids from `get_bot_ids` which are from this `people_by_user_id_dict`. * `update_bot_data`, which is called in two places that check `is_valid_bot_user` on the bot id first. So we should always get a `bot_user` back, and we're already relying on the bot users being defined in several methods of the `bot_list_widget`. --- web/src/settings_users.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/web/src/settings_users.js b/web/src/settings_users.js index 5e0f87cd0c..6e5da0eef8 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.js @@ -220,12 +220,7 @@ function bot_owner_full_name(owner_id) { } function bot_info(bot_user_id) { - const bot_user = people.maybe_get_user_by_id(bot_user_id); - - if (!bot_user) { - return undefined; - } - + const bot_user = people.get_by_user_id(bot_user_id); const owner_id = bot_user.bot_owner_id; const info = {}; @@ -322,9 +317,6 @@ function bots_create_table() { filter: { $element: $bots_table.closest(".settings-section").find(".search"), predicate(item, value) { - if (!item) { - return false; - } return ( item.full_name.toLowerCase().includes(value) || item.display_email.toLowerCase().includes(value) From 174a4589281a807d8fa2dce33c93ff3acaffab09 Mon Sep 17 00:00:00 2001 From: evykassirer Date: Sun, 3 Nov 2024 19:16:43 -0800 Subject: [PATCH 269/276] settings_users: Convert module to typescript. --- tools/test-js-with-node | 2 +- .../{settings_users.js => settings_users.ts} | 337 +++++++++++------- web/src/user_sort.ts | 2 +- 3 files changed, 209 insertions(+), 132 deletions(-) rename web/src/{settings_users.js => settings_users.ts} (66%) diff --git a/tools/test-js-with-node b/tools/test-js-with-node index d14559aa0c..a6ab868b34 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -231,7 +231,7 @@ EXEMPT_FILES = make_set( "web/src/settings_toggle.js", "web/src/settings_ui.ts", "web/src/settings_user_topics.ts", - "web/src/settings_users.js", + "web/src/settings_users.ts", "web/src/setup.ts", "web/src/sidebar_ui.ts", "web/src/spectators.ts", diff --git a/web/src/settings_users.js b/web/src/settings_users.ts similarity index 66% rename from web/src/settings_users.js rename to web/src/settings_users.ts index 6e5da0eef8..55c8f5a602 100644 --- a/web/src/settings_users.js +++ b/web/src/settings_users.ts @@ -1,4 +1,6 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; +import type * as tippy from "tippy.js"; import render_admin_user_list from "../templates/settings/admin_user_list.hbs"; @@ -8,9 +10,11 @@ import * as channel from "./channel"; import * as dialog_widget from "./dialog_widget"; import * as dropdown_widget from "./dropdown_widget"; import {$t} from "./i18n"; +import type {ListWidget as ListWidgetType} from "./list_widget"; import * as ListWidget from "./list_widget"; import * as loading from "./loading"; import * as people from "./people"; +import type {User} from "./people"; import * as presence from "./presence"; import * as scroll_util from "./scroll_util"; import * as settings_bots from "./settings_bots"; @@ -30,7 +34,18 @@ export const deactivated_user_list_dropdown_widget_name = "deactivated_user_list let should_redraw_active_users_list = false; let should_redraw_deactivated_users_list = false; -const active_section = { +type UserSettingsSection = { + dropdown_widget_name: string; + filters: { + text_search: string; + role_code: number; + }; + handle_events: () => void; + create_table: (active_users: number[]) => void; + list_widget: ListWidgetType | undefined; +}; + +const active_section: UserSettingsSection = { dropdown_widget_name: active_user_list_dropdown_widget_name, filters: { text_search: "", @@ -42,7 +57,7 @@ const active_section = { list_widget: undefined, }; -const deactivated_section = { +const deactivated_section: UserSettingsSection = { dropdown_widget_name: deactivated_user_list_dropdown_widget_name, filters: { text_search: "", @@ -59,34 +74,34 @@ const bots_section = { create_table: bots_create_table, }; -function sort_bot_email(a, b) { - function email(bot) { - return (bot.display_email || "").toLowerCase(); +function sort_bot_email(a: BotInfo, b: BotInfo): number { + function email(bot: BotInfo): string { + return (bot.display_email ?? "").toLowerCase(); } return util.compare_a_b(email(a), email(b)); } -function sort_bot_owner(a, b) { - function owner_name(bot) { +function sort_bot_owner(a: BotInfo, b: BotInfo): number { + function owner_name(bot: BotInfo): string { return (bot.bot_owner_full_name || "").toLowerCase(); } return util.compare_a_b(owner_name(a), owner_name(b)); } -function sort_last_active(a, b) { +function sort_last_active(a: User, b: User): number { return util.compare_a_b( - presence.last_active_date(a.user_id) || 0, - presence.last_active_date(b.user_id) || 0, + presence.last_active_date(a.user_id) ?? 0, + presence.last_active_date(b.user_id) ?? 0, ); } -function get_user_info_row(user_id) { - return $(`tr.user_row[data-user-id='${CSS.escape(user_id)}']`); +function get_user_info_row(user_id: number): JQuery { + return $(`tr.user_row[data-user-id='${CSS.escape(user_id.toString())}']`); } -export function allow_sorting_deactivated_users_list_by_email() { +export function allow_sorting_deactivated_users_list_by_email(): boolean { const deactivated_users = people.get_non_active_realm_users(); const deactivated_humans_with_visible_email = deactivated_users.filter( (user) => !user.is_bot && user.delivery_email, @@ -95,7 +110,7 @@ export function allow_sorting_deactivated_users_list_by_email() { return deactivated_humans_with_visible_email.length !== 0; } -export function update_view_on_deactivate(user_id) { +export function update_view_on_deactivate(user_id: number): void { const $row = get_user_info_row(user_id); if ($row.length === 0) { return; @@ -114,7 +129,7 @@ export function update_view_on_deactivate(user_id) { should_redraw_deactivated_users_list = true; } -export function update_view_on_reactivate(user_id) { +export function update_view_on_reactivate(user_id: number): void { const $row = get_user_info_row(user_id); if ($row.length === 0) { return; @@ -132,24 +147,40 @@ export function update_view_on_reactivate(user_id) { should_redraw_deactivated_users_list = true; } -function failed_listing_users() { +function failed_listing_users(): void { loading.destroy_indicator($("#subs_page_loading_indicator")); const user_id = people.my_current_user_id(); blueslip.error("Error while listing users for user_id", {user_id}); } -function add_value_to_filters(section, key, value) { - section.filters[key] = value; +function add_value_to_filters( + section: UserSettingsSection, + key: "role_code" | "text_search", + value: number | string, +): void { + if (key === "role_code") { + assert(typeof value === "number"); + section.filters[key] = value; + } else { + assert(typeof value === "string"); + section.filters[key] = value; + } // This hard_redraw will rerun the relevant predicate function // and in turn apply the new filters. + assert(section.list_widget !== undefined); section.list_widget.hard_redraw(); } -function role_selected_handler(event, dropdown, widget) { +function role_selected_handler( + this: HTMLElement, + event: JQuery.ClickEvent, + dropdown: tippy.Instance, + widget: dropdown_widget.DropdownWidget, +): void { event.preventDefault(); event.stopPropagation(); - const role_code = Number($(event.currentTarget).attr("data-unique-id")); + const role_code = Number($(this).attr("data-unique-id")); if (widget.widget_name === active_section.dropdown_widget_name) { add_value_to_filters(active_section, "role_code", role_code); } else if (widget.widget_name === deactivated_section.dropdown_widget_name) { @@ -160,7 +191,10 @@ function role_selected_handler(event, dropdown, widget) { widget.render(); } -function get_roles() { +function get_roles(): { + unique_id: number; + name: string; +}[] { return [ {unique_id: 0, name: $t({defaultMessage: "All roles"})}, ...Object.values(settings_config.user_role_values) @@ -172,7 +206,10 @@ function get_roles() { ]; } -function create_role_filter_dropdown($events_container, section) { +function create_role_filter_dropdown( + $events_container: JQuery, + section: UserSettingsSection, +): void { new dropdown_widget.DropdownWidget({ widget_name: section.dropdown_widget_name, unique_id_type: dropdown_widget.DataTypes.NUMBER, @@ -186,7 +223,7 @@ function create_role_filter_dropdown($events_container, section) { }).setup(); } -function populate_users() { +function populate_users(): void { const active_user_ids = people.get_realm_active_human_user_ids(); const deactivated_user_ids = people.get_non_active_human_ids(); @@ -200,13 +237,13 @@ function populate_users() { create_role_filter_dropdown($("#admin-deactivated-users-list"), deactivated_section); } -function reset_scrollbar($sel) { +function reset_scrollbar($sel: JQuery): () => void { return function () { scroll_util.reset_scrollbar($sel); }; } -function bot_owner_full_name(owner_id) { +function bot_owner_full_name(owner_id: number | null): string | undefined { if (!owner_id) { return undefined; } @@ -219,48 +256,73 @@ function bot_owner_full_name(owner_id) { return bot_owner.full_name; } -function bot_info(bot_user_id) { +type BotInfo = { + is_bot: boolean; + role: number; + is_active: boolean; + user_id: number; + full_name: string; + user_role_text: string | undefined; + img_src: string; + bot_type: string | undefined; + bot_owner_full_name: string; + no_owner: boolean; + is_current_user: boolean; + can_modify: boolean; + cannot_deactivate: boolean; + cannot_edit: boolean; + display_email: string; +} & ( + | { + bot_owner_id: number; + is_bot_owner_active: boolean; + owner_img_src: string; + } + | { + bot_owner_id: null; + } +); + +function bot_info(bot_user_id: number): BotInfo { const bot_user = people.get_by_user_id(bot_user_id); + assert(bot_user.is_bot); + const owner_id = bot_user.bot_owner_id; + const owner_full_name = bot_owner_full_name(owner_id); - const info = {}; - - info.is_bot = true; - info.role = bot_user.role; - info.is_active = people.is_person_active(bot_user.user_id); - info.user_id = bot_user.user_id; - info.full_name = bot_user.full_name; - info.bot_owner_id = owner_id; - info.user_role_text = people.get_user_type(bot_user_id); - info.img_src = people.small_avatar_url_for_person(bot_user); - - // Convert bot type id to string for viewing to the users. - info.bot_type = settings_data.bot_type_id_to_string(bot_user.bot_type); - - info.bot_owner_full_name = bot_owner_full_name(owner_id); - - if (!info.bot_owner_full_name) { - info.no_owner = true; - info.bot_owner_full_name = $t({defaultMessage: "No owner"}); - } - - info.is_current_user = false; - info.can_modify = current_user.is_admin; - info.cannot_deactivate = bot_user.is_system_bot; - info.cannot_edit = bot_user.is_system_bot; - - // It's always safe to show the real email addresses for bot users - info.display_email = bot_user.email; - - if (owner_id) { - info.is_bot_owner_active = people.is_person_active(owner_id); - info.owner_img_src = people.small_avatar_url_for_person(people.get_by_user_id(owner_id)); - } - - return info; + return { + is_bot: true, + role: bot_user.role, + is_active: people.is_person_active(bot_user.user_id), + user_id: bot_user.user_id, + full_name: bot_user.full_name, + user_role_text: people.get_user_type(bot_user_id), + img_src: people.small_avatar_url_for_person(bot_user), + // Convert bot type id to string for viewing to the users. + bot_type: settings_data.bot_type_id_to_string(bot_user.bot_type), + bot_owner_full_name: owner_full_name ?? $t({defaultMessage: "No owner"}), + no_owner: !owner_full_name, + is_current_user: false, + can_modify: current_user.is_admin, + cannot_deactivate: bot_user.is_system_bot ?? false, + cannot_edit: bot_user.is_system_bot ?? false, + // It's always safe to show the real email addresses for bot users + display_email: bot_user.email, + ...(owner_id + ? { + bot_owner_id: owner_id, + is_bot_owner_active: people.is_person_active(owner_id), + owner_img_src: people.small_avatar_url_for_person( + people.get_by_user_id(owner_id), + ), + } + : { + bot_owner_id: null, + }), + }; } -function get_last_active(user) { +function get_last_active(user: User): string { const last_active_date = presence.last_active_date(user.user_id); if (!last_active_date) { @@ -269,39 +331,48 @@ function get_last_active(user) { return timerender.render_now(last_active_date).time_str; } -function human_info(person) { - const info = {}; - - info.is_bot = false; - info.user_role_text = people.get_user_type(person.user_id); - info.is_active = people.is_person_active(person.user_id); - info.user_id = person.user_id; - info.full_name = person.full_name; - info.bot_owner_id = person.bot_owner_id; - - info.can_modify = current_user.is_admin; - info.is_current_user = people.is_my_user_id(person.user_id); - info.cannot_deactivate = - person.is_owner && (!current_user.is_owner || people.is_current_user_only_owner()); - info.display_email = person.delivery_email; - info.img_src = people.small_avatar_url_for_person(person); - - // TODO: This is not shown in deactivated users table and it is - // controlled by `display_last_active_column` We might just want - // to show this for deactivated users, too, even though it might - // usually just be undefined. - info.last_active_date = get_last_active(person); - - return info; +function human_info(person: User): { + is_bot: false; + user_role_text: string | undefined; + is_active: boolean; + user_id: number; + full_name: string; + bot_owner_id: number | null; + can_modify: boolean; + is_current_user: boolean; + cannot_deactivate: boolean; + display_email: string | null; + img_src: string; + last_active_date: string; +} { + return { + is_bot: false, + user_role_text: people.get_user_type(person.user_id), + is_active: people.is_person_active(person.user_id), + user_id: person.user_id, + full_name: person.full_name, + bot_owner_id: person.is_bot ? person.bot_owner_id : null, + can_modify: current_user.is_admin, + is_current_user: people.is_my_user_id(person.user_id), + cannot_deactivate: + person.is_owner && (!current_user.is_owner || people.is_current_user_only_owner()), + display_email: person.delivery_email, + img_src: people.small_avatar_url_for_person(person), + // TODO: This is not shown in deactivated users table and it is + // controlled by `display_last_active_column` We might just want + // to show this for deactivated users, too, even though it might + // usually just be undefined. + last_active_date: get_last_active(person), + }; } -function set_text_search_value($table, value) { +function set_text_search_value($table: JQuery, value: string): void { $table.closest(".user-settings-section").find(".search").val(value); } -let bot_list_widget; +let bot_list_widget: ListWidgetType; -function bots_create_table() { +function bots_create_table(): void { loading.make_indicator($("#admin_page_bots_loading_indicator"), { text: $t({defaultMessage: "Loading…"}), }); @@ -313,7 +384,7 @@ function bots_create_table() { name: "admin_bot_list", get_item: bot_info, modifier_html: render_admin_user_list, - html_selector: (item) => $(`tr[data-user-id='${CSS.escape(item.user_id)}']`), + html_selector: (item) => $(`tr[data-user-id='${CSS.escape(item.user_id.toString())}']`), filter: { $element: $bots_table.closest(".settings-section").find(".search"), predicate(item, value) { @@ -339,15 +410,16 @@ function bots_create_table() { $bots_table.show(); } -function active_create_table(active_users) { +function active_create_table(active_users: number[]): void { const $users_table = $("#admin_users_table"); active_section.list_widget = ListWidget.create($users_table, active_users, { name: "users_table_list", get_item: people.get_by_user_id, modifier_html(item) { - const info = human_info(item); - info.display_last_active_column = true; - return render_admin_user_list(info); + return render_admin_user_list({ + ...human_info(item), + display_last_active_column: true, + }); }, filter: { predicate(person) { @@ -372,7 +444,7 @@ function active_create_table(active_users) { $("#admin_users_table").show(); } -function deactivated_create_table(deactivated_users) { +function deactivated_create_table(deactivated_users: number[]): void { const $deactivated_users_table = $("#admin_deactivated_users_table"); deactivated_section.list_widget = ListWidget.create( $deactivated_users_table, @@ -381,9 +453,10 @@ function deactivated_create_table(deactivated_users) { name: "deactivated_users_table_list", get_item: people.get_by_user_id, modifier_html(item) { - const info = human_info(item); - info.display_last_active_column = false; - return render_admin_user_list(info); + return render_admin_user_list({ + ...human_info(item), + display_last_active_column: false, + }); }, filter: { predicate(person) { @@ -411,7 +484,7 @@ function deactivated_create_table(deactivated_users) { $("#admin_deactivated_users_table").show(); } -export function update_bot_data(bot_user_id) { +export function update_bot_data(bot_user_id: number): void { if (!bot_list_widget) { return; } @@ -419,7 +492,10 @@ export function update_bot_data(bot_user_id) { bot_list_widget.render_item(bot_info(bot_user_id)); } -export function update_user_data(user_id, new_data) { +export function update_user_data( + user_id: number, + new_data: {full_name?: string; role?: number}, +): void { const $user_row = get_user_info_row(user_id); if ($user_row.length === 0) { @@ -439,7 +515,7 @@ export function update_user_data(user_id, new_data) { } } -export function redraw_bots_list() { +export function redraw_bots_list(): void { if (!bot_list_widget) { return; } @@ -451,7 +527,7 @@ export function redraw_bots_list() { bot_list_widget.replace_list_data(bot_user_ids); } -function redraw_users_list(user_section, user_list) { +function redraw_users_list(user_section: UserSettingsSection, user_list: number[]): void { if (!user_section.list_widget) { return; } @@ -459,7 +535,7 @@ function redraw_users_list(user_section, user_list) { user_section.list_widget.replace_list_data(user_list); } -export function redraw_deactivated_users_list() { +export function redraw_deactivated_users_list(): void { if (!should_redraw_deactivated_users_list) { return; } @@ -468,7 +544,7 @@ export function redraw_deactivated_users_list() { should_redraw_deactivated_users_list = false; } -export function redraw_active_users_list() { +export function redraw_active_users_list(): void { if (!should_redraw_active_users_list) { return; } @@ -477,7 +553,7 @@ export function redraw_active_users_list() { should_redraw_active_users_list = false; } -function start_data_load() { +function start_data_load(): void { loading.make_indicator($("#admin_page_users_loading_indicator"), { text: $t({defaultMessage: "Loading…"}), }); @@ -490,7 +566,7 @@ function start_data_load() { populate_users(); } -function handle_deactivation($tbody) { +function handle_deactivation($tbody: JQuery): void { $tbody.on("click", ".deactivate", (e) => { // This click event must not get propagated to parent container otherwise the modal // will not show up because of a call to `close_active` in `settings.js`. @@ -505,7 +581,7 @@ function handle_deactivation($tbody) { url = "/json/users/me"; } - function handle_confirm() { + function handle_confirm(): void { let data = {}; if ($(".send_email").is(":checked")) { data = { @@ -513,30 +589,31 @@ function handle_deactivation($tbody) { }; } - const opts = {}; if (user_id === current_user.user_id) { - opts.success_continuation = () => { - window.location.href = "/login/"; - }; + dialog_widget.submit_api_request(channel.del, url, data, { + success_continuation() { + window.location.href = "/login/"; + }, + }); + } else { + dialog_widget.submit_api_request(channel.del, url, data); } - - dialog_widget.submit_api_request(channel.del, url, data, opts); } user_deactivation_ui.confirm_deactivation(user_id, handle_confirm, true); }); } -function handle_bot_deactivation($tbody) { +function handle_bot_deactivation($tbody: JQuery): void { $tbody.on("click", ".deactivate", (e) => { e.preventDefault(); e.stopPropagation(); const $button_elem = $(e.target); const $row = $button_elem.closest(".user_row"); - const bot_id = Number.parseInt($row.attr("data-user-id"), 10); + const bot_id = Number.parseInt($row.attr("data-user-id")!, 10); - function handle_confirm() { + function handle_confirm(): void { const url = "/json/bots/" + encodeURIComponent(bot_id); dialog_widget.submit_api_request(channel.del, url, {}); } @@ -545,7 +622,7 @@ function handle_bot_deactivation($tbody) { }); } -function handle_reactivation($tbody) { +function handle_reactivation($tbody: JQuery): void { $tbody.on("click", ".reactivate", (e) => { e.preventDefault(); e.stopPropagation(); @@ -553,9 +630,9 @@ function handle_reactivation($tbody) { // Go up the tree until we find the user row, then grab the email element const $button_elem = $(e.target); const $row = $button_elem.closest(".user_row"); - const user_id = Number.parseInt($row.attr("data-user-id"), 10); + const user_id = Number.parseInt($row.attr("data-user-id")!, 10); - function handle_confirm() { + function handle_confirm(): void { const url = "/json/users/" + encodeURIComponent(user_id) + "/reactivate"; dialog_widget.submit_api_request(channel.post, url, {}); } @@ -564,12 +641,12 @@ function handle_reactivation($tbody) { }); } -function handle_edit_form($tbody) { - $tbody.on("click", ".open-user-form", (e) => { +function handle_edit_form($tbody: JQuery): void { + $tbody.on("click", ".open-user-form", function (this: HTMLElement, e) { e.stopPropagation(); e.preventDefault(); - const user_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); + const user_id = Number.parseInt($(this).attr("data-user-id")!, 10); if (people.is_my_user_id(user_id)) { browser_history.go_to_location("#settings/profile"); return; @@ -580,20 +657,20 @@ function handle_edit_form($tbody) { }); } -function handle_filter_change($tbody, section) { +function handle_filter_change($tbody: JQuery, section: UserSettingsSection): void { // This duplicates the built-in search filter live-update logic in // ListWidget for the input.list_widget_filter event type, but we // can't use that, because we're also filtering on Role with our // custom predicate. $tbody .closest(".user-settings-section") - .find(".search") - .on("input.list_widget_filter", function () { + .find(".search") + .on("input.list_widget_filter", function (this: HTMLInputElement) { add_value_to_filters(section, "text_search", this.value.toLocaleLowerCase()); }); } -function active_handle_events() { +function active_handle_events(): void { const $tbody = $("#admin_users_table").expectOne(); handle_filter_change($tbody, active_section); @@ -602,7 +679,7 @@ function active_handle_events() { handle_edit_form($tbody); } -function deactivated_handle_events() { +function deactivated_handle_events(): void { const $tbody = $("#admin_deactivated_users_table").expectOne(); handle_filter_change($tbody, deactivated_section); @@ -611,7 +688,7 @@ function deactivated_handle_events() { handle_edit_form($tbody); } -function bots_handle_events() { +function bots_handle_events(): void { const $tbody = $("#admin_bots_table").expectOne(); handle_bot_deactivation($tbody); @@ -619,14 +696,14 @@ function bots_handle_events() { handle_edit_form($tbody); } -export function set_up_humans() { +export function set_up_humans(): void { start_data_load(); active_section.handle_events(); deactivated_section.handle_events(); setting_invites.set_up(); } -export function set_up_bots() { +export function set_up_bots(): void { bots_section.handle_events(); bots_section.create_table(); diff --git a/web/src/user_sort.ts b/web/src/user_sort.ts index ecbf2f2dff..624d8bac12 100644 --- a/web/src/user_sort.ts +++ b/web/src/user_sort.ts @@ -21,7 +21,7 @@ export function sort_email(a: User, b: User): number { return compare_a_b(email_a.toLowerCase(), email_b.toLowerCase()); } -export function sort_role(a: User, b: User): number { +export function sort_role(a: T, b: T): number { return compare_a_b(a.role, b.role); } From 9c9866461a793a1b828bcc463775315c74fd80d4 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 4 Nov 2024 10:49:11 +0530 Subject: [PATCH 270/276] transaction: Add `durable=True` to the outermost db transactions. This commit adds `durable=True` to the outermost db transactions created in the following: * confirm_email_change * handle_upload_pre_finish_hook * deliver_scheduled_emails * restore_data_from_archive * do_change_realm_subdomain * do_create_realm * do_deactivate_realm * do_reactivate_realm * do_delete_user * do_delete_user_preserving_messages * create_stripe_customer * process_initial_upgrade * do_update_plan * request_sponsorship * upload_message_attachment * register_remote_server * do_soft_deactivate_users * maybe_send_batched_emails It helps to avoid creating unintended savepoints in the future. This is as a part of our plan to explicitly mark all the transaction.atomic calls with either 'savepoint=False' or 'durable=True' as required. * 'savepoint=True' is used in special cases. --- corporate/lib/stripe.py | 8 ++++---- zerver/actions/create_realm.py | 4 ++-- zerver/actions/realm_settings.py | 4 ++-- zerver/actions/users.py | 4 ++-- zerver/lib/retention.py | 2 +- zerver/lib/soft_deactivation.py | 2 +- zerver/lib/upload/__init__.py | 2 +- zerver/management/commands/deliver_scheduled_emails.py | 2 +- zerver/views/tusd.py | 2 +- zerver/views/user_settings.py | 2 +- zerver/worker/missedmessage_emails.py | 2 +- zilencer/views.py | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index bbf7264a1b..58c36422e4 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -1094,7 +1094,7 @@ class BillingSession(ABC): metadata=stripe_customer_data.metadata, ) event_time = timestamp_to_datetime(stripe_customer.created) - with transaction.atomic(): + with transaction.atomic(durable=True): self.write_to_audit_log(BillingSessionEventType.STRIPE_CUSTOMER_CREATED, event_time) customer = self.update_or_create_customer(stripe_customer.id) return customer @@ -1793,7 +1793,7 @@ class BillingSession(ABC): # TODO: The correctness of this relies on user creation, deactivation, etc being # in a transaction.atomic() with the relevant RealmAuditLog entries - with transaction.atomic(): + with transaction.atomic(durable=True): # We get the current license count here in case the number of billable # licenses has changed since the upgrade process began. current_licenses_count = self.get_billable_licenses_for_customer( @@ -2912,7 +2912,7 @@ class BillingSession(ABC): if status is not None: if status == CustomerPlan.ACTIVE: assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD - with transaction.atomic(): + with transaction.atomic(durable=True): # Switch to a different plan was cancelled. We end the next plan # and set the current one as active. if plan.status == CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END: @@ -3412,7 +3412,7 @@ class BillingSession(ABC): raise BillingError("Form validation error", message=message) request_context = self.get_sponsorship_request_session_specific_context() - with transaction.atomic(): + with transaction.atomic(durable=True): # Ensures customer is created first before updating sponsorship status. self.update_customer_sponsorship_status(True) sponsorship_request = ZulipSponsorshipRequest( diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index 08f69ad4e1..4d2d1c6f88 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -61,7 +61,7 @@ def do_change_realm_subdomain( # deleting, clear that state. realm.demo_organization_scheduled_deletion_date = None realm.string_id = new_subdomain - with transaction.atomic(): + with transaction.atomic(durable=True): realm.save(update_fields=["string_id", "demo_organization_scheduled_deletion_date"]) RealmAuditLog.objects.create( realm=realm, @@ -232,7 +232,7 @@ def do_create_realm( # is required to do so correctly. kwargs["push_notifications_enabled"] = sends_notifications_directly() - with transaction.atomic(): + with transaction.atomic(durable=True): realm = Realm(string_id=string_id, name=name, **kwargs) if is_demo_organization: realm.demo_organization_scheduled_deletion_date = realm.date_created + timedelta( diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index 7d06ab8257..f1e39332c4 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -516,7 +516,7 @@ def do_deactivate_realm( if settings.BILLING_ENABLED: from corporate.lib.stripe import RealmBillingSession - with transaction.atomic(): + with transaction.atomic(durable=True): realm.deactivated = True realm.save(update_fields=["deactivated"]) @@ -575,7 +575,7 @@ def do_reactivate_realm(realm: Realm) -> None: return realm.deactivated = False - with transaction.atomic(): + with transaction.atomic(durable=True): realm.save(update_fields=["deactivated"]) event_time = timezone_now() diff --git a/zerver/actions/users.py b/zerver/actions/users.py index ac8fb55c61..6079a36ffe 100644 --- a/zerver/actions/users.py +++ b/zerver/actions/users.py @@ -80,7 +80,7 @@ def do_delete_user(user_profile: UserProfile, *, acting_user: UserProfile | None date_joined = user_profile.date_joined personal_recipient = user_profile.recipient - with transaction.atomic(): + with transaction.atomic(durable=True): user_profile.delete() # Recipient objects don't get deleted through CASCADE, so we need to handle # the user's personal recipient manually. This will also delete all Messages pointing @@ -180,7 +180,7 @@ def do_delete_user_preserving_messages(user_profile: UserProfile) -> None: realm = user_profile.realm date_joined = user_profile.date_joined - with transaction.atomic(): + with transaction.atomic(durable=True): # The strategy is that before calling user_profile.delete(), we need to # reassign Messages sent by the user to a dummy user, so that they don't # get affected by CASCADE. We cannot yet create a dummy user with .id diff --git a/zerver/lib/retention.py b/zerver/lib/retention.py index 694f8318a8..f64a185606 100644 --- a/zerver/lib/retention.py +++ b/zerver/lib/retention.py @@ -596,7 +596,7 @@ def restore_data_from_archive(archive_transaction: ArchiveTransaction) -> int: # so that when we log "Finished", the process has indeed finished - and that happens only after # leaving the atomic block - Django does work committing the changes to the database when # the block ends. - with transaction.atomic(): + with transaction.atomic(durable=True): msg_ids = restore_messages_from_archive(archive_transaction.id) restore_models_with_message_key_from_archive(archive_transaction.id) restore_attachments_from_archive(archive_transaction.id) diff --git a/zerver/lib/soft_deactivation.py b/zerver/lib/soft_deactivation.py index 7dae6442e7..b8580d7d24 100644 --- a/zerver/lib/soft_deactivation.py +++ b/zerver/lib/soft_deactivation.py @@ -277,7 +277,7 @@ def do_soft_deactivate_users( (user_batch, users) = (users[0:BATCH_SIZE], users[BATCH_SIZE:]) if len(user_batch) == 0: break - with transaction.atomic(): + with transaction.atomic(durable=True): realm_logs = [] for user in user_batch: do_soft_deactivate_user(user) diff --git a/zerver/lib/upload/__init__.py b/zerver/lib/upload/__init__.py index 3557c3f601..84a61897a9 100644 --- a/zerver/lib/upload/__init__.py +++ b/zerver/lib/upload/__init__.py @@ -158,7 +158,7 @@ def upload_message_attachment( str(target_realm.id), sanitize_name(uploaded_file_name) ) - with transaction.atomic(): + with transaction.atomic(durable=True): upload_backend.upload_message_attachment( path_id, uploaded_file_name, diff --git a/zerver/management/commands/deliver_scheduled_emails.py b/zerver/management/commands/deliver_scheduled_emails.py index 66e1b44143..a7a3f2824d 100644 --- a/zerver/management/commands/deliver_scheduled_emails.py +++ b/zerver/management/commands/deliver_scheduled_emails.py @@ -37,7 +37,7 @@ Usage: ./manage.py deliver_scheduled_emails def handle(self, *args: Any, **options: Any) -> None: try: while True: - with transaction.atomic(): + with transaction.atomic(durable=True): job = ( ScheduledEmail.objects.filter(scheduled_timestamp__lte=timezone_now()) .prefetch_related("users") diff --git a/zerver/views/tusd.py b/zerver/views/tusd.py index cb13416a21..d053bf07ae 100644 --- a/zerver/views/tusd.py +++ b/zerver/views/tusd.py @@ -194,7 +194,7 @@ def handle_upload_pre_finish_hook( StorageClass=settings.S3_UPLOADS_STORAGE_CLASS, ) - with transaction.atomic(): + with transaction.atomic(durable=True): create_attachment( filename, path_id, diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py index eef6dbdbfe..5c1319536a 100644 --- a/zerver/views/user_settings.py +++ b/zerver/views/user_settings.py @@ -100,7 +100,7 @@ def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpRes assert isinstance(email_change_object, EmailChangeStatus) new_email = email_change_object.new_email old_email = email_change_object.old_email - with transaction.atomic(): + with transaction.atomic(durable=True): user_profile = UserProfile.objects.select_for_update().get( id=email_change_object.user_profile_id ) diff --git a/zerver/worker/missedmessage_emails.py b/zerver/worker/missedmessage_emails.py index 9f2b4c40f4..ef84ef23d5 100644 --- a/zerver/worker/missedmessage_emails.py +++ b/zerver/worker/missedmessage_emails.py @@ -216,7 +216,7 @@ class MissedMessageWorker(QueueProcessingWorker): def maybe_send_batched_emails(self) -> None: current_time = timezone_now() - with transaction.atomic(): + with transaction.atomic(durable=True): events_to_process = ScheduledMessageNotificationEmail.objects.filter( scheduled_timestamp__lte=current_time ).select_for_update() diff --git a/zilencer/views.py b/zilencer/views.py index 430f4bbb29..52083f1f76 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -211,7 +211,7 @@ def register_remote_server( _("A server with hostname {hostname} already exists").format(hostname=hostname) ) - with transaction.atomic(): + with transaction.atomic(durable=True): if remote_server is None: created = True remote_server = RemoteZulipServer.objects.create( From 0e67e4f1a144f9ceaf544b91b68592dc3574d890 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 4 Nov 2024 11:02:53 +0530 Subject: [PATCH 271/276] compose_views: Add `savepoint=False` to avoid creating savepoints. 'compose_views' is used inside an outer db transaction created in 'update_user_group_backend'. `transaction.atomic()` block in 'compose_views' resulted in savepoint creation. This commit adds `savepoint=False` to avoid that. --- zerver/tests/test_subs.py | 5 ++++- zerver/transaction_tests/test_user_groups.py | 1 - zerver/views/streams.py | 2 +- zerver/views/user_groups.py | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 8321bc651d..46c8eb6ef9 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -9,6 +9,7 @@ from unittest import mock import orjson from django.conf import settings from django.core.exceptions import ValidationError +from django.db import transaction from django.http import HttpResponse from django.utils.timezone import now as timezone_now from typing_extensions import override @@ -4016,7 +4017,9 @@ class SubscriptionRestApiTest(ZulipTestCase): def thunk2() -> HttpResponse: raise JsonableError("random failure") - with self.assertRaises(JsonableError): + with transaction.atomic(), self.assertRaises(JsonableError): + # The atomic() wrapper helps to avoid JsonableError breaking + # the test's transaction. compose_views([thunk1, thunk2]) user_profile = self.example_user("hamlet") diff --git a/zerver/transaction_tests/test_user_groups.py b/zerver/transaction_tests/test_user_groups.py index 3b5f6be325..1cdeff32ec 100644 --- a/zerver/transaction_tests/test_user_groups.py +++ b/zerver/transaction_tests/test_user_groups.py @@ -28,7 +28,6 @@ def dev_update_subgroups( assert BARRIER is not None try: with ( - transaction.atomic(), mock.patch("zerver.lib.user_groups.access_user_group_for_update") as m, ): diff --git a/zerver/views/streams.py b/zerver/views/streams.py index d618d473e5..6f0246cba5 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -468,7 +468,7 @@ def compose_views(thunks: list[Callable[[], HttpResponse]]) -> dict[str, Any]: """ json_dict: dict[str, Any] = {} - with transaction.atomic(): + with transaction.atomic(savepoint=False): for thunk in thunks: response = thunk() json_dict.update(orjson.loads(response.content)) diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index 2f44c2be18..98e0d0bf64 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -468,6 +468,7 @@ def remove_subgroups_from_group_backend( @require_member_or_admin @typed_endpoint +@transaction.atomic(durable=True) def update_subgroups_of_user_group( request: HttpRequest, user_profile: UserProfile, From ca4760a04c90f18bce3ed109043488416822f899 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 4 Nov 2024 11:15:56 +0530 Subject: [PATCH 272/276] recipients: Add `savepoint=False` to avoid creating savepoints. 'get_or_create_direct_message_group' is used inside an outer db transaction created in 'edit_scheduled_message'. `transaction.atomic()` block in 'get_or_create_direct_message_group' resulted in savepoint creation. This commit adds `savepoint=False` to avoid that. --- zerver/models/recipients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zerver/models/recipients.py b/zerver/models/recipients.py index 38a4bc5c8f..c49f5d774f 100644 --- a/zerver/models/recipients.py +++ b/zerver/models/recipients.py @@ -165,7 +165,7 @@ def get_or_create_direct_message_group(id_list: list[int]) -> DirectMessageGroup from zerver.models import Subscription, UserProfile direct_message_group_hash = get_direct_message_group_hash(id_list) - with transaction.atomic(): + with transaction.atomic(savepoint=False): (direct_message_group, created) = DirectMessageGroup.objects.get_or_create( huddle_hash=direct_message_group_hash, group_size=len(id_list), From 8c8cc8018319f2d043339f01309aae7e79eab83e Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 4 Nov 2024 13:02:13 +0530 Subject: [PATCH 273/276] create_user: Add `savepoint=False` to avoid creating savepoints. 'do_activate_mirror_dummy_user' is used inside an outer db transaction created in 'registration_helper'. `transaction.atomic()` block in 'do_activate_mirror_dummy_user' resulted in savepoint creation. This commit adds `savepoint=False` to avoid that. --- zerver/actions/create_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zerver/actions/create_user.py b/zerver/actions/create_user.py index b0b6cef415..297622dccf 100644 --- a/zerver/actions/create_user.py +++ b/zerver/actions/create_user.py @@ -659,7 +659,7 @@ def do_activate_mirror_dummy_user( if settings.BILLING_ENABLED: from corporate.lib.stripe import RealmBillingSession - with transaction.atomic(): + with transaction.atomic(savepoint=False): change_user_is_active(user_profile, True) user_profile.is_mirror_dummy = False user_profile.set_unusable_password() From 27eeb0845908abd869aac7039f056d978c27e1fe Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 4 Nov 2024 13:31:26 +0530 Subject: [PATCH 274/276] streams: Add `savepoint=False` to avoid creating savepoints. 'bulk_remove_subscriptions' is used inside an outer db transaction created in 'do_change_bot_owner'. `transaction.atomic()` block in 'bulk_remove_subscriptions' resulted in savepoint creation. This commit adds `savepoint=False` to avoid that. --- zerver/actions/streams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zerver/actions/streams.py b/zerver/actions/streams.py index 181a93c795..2271759614 100644 --- a/zerver/actions/streams.py +++ b/zerver/actions/streams.py @@ -979,7 +979,7 @@ def bulk_remove_subscriptions( streams_to_unsubscribe = [sub_info.stream for sub_info in subs_to_deactivate] # We do all the database changes in a transaction to ensure # RealmAuditLog entries are atomically created when making changes. - with transaction.atomic(): + with transaction.atomic(savepoint=False): Subscription.objects.filter( id__in=sub_ids_to_deactivate, ).update(active=False) From 86a909e7036c7775a8587301deb43f073d2f4557 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 4 Nov 2024 14:00:30 +0530 Subject: [PATCH 275/276] users: Add `savepoint=False` to avoid creating savepoints. 'do_deactivate_user' is used inside an outer db transaction created in 'sync_user_from_ldap'. `transaction.atomic()` block in 'do_deactivate_user' resulted in savepoint creation. This commit adds `savepoint=False` to avoid that. --- zerver/actions/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zerver/actions/users.py b/zerver/actions/users.py index 6079a36ffe..a215030f03 100644 --- a/zerver/actions/users.py +++ b/zerver/actions/users.py @@ -486,7 +486,7 @@ def do_deactivate_user( for profile in bot_profiles: do_deactivate_user(profile, _cascade=False, acting_user=acting_user) - with transaction.atomic(): + with transaction.atomic(savepoint=False): if user_profile.realm.is_zephyr_mirror_realm: # nocoverage # For zephyr mirror users, we need to make them a mirror dummy # again; otherwise, other users won't get the correct behavior From 75edce59c141eea9815dfeb44fa74acb66a63606 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 4 Nov 2024 15:51:21 +0530 Subject: [PATCH 276/276] process_downgrade: Add savepoint=False to avoid creating savepoint. 'process_downgrade' is used inside an outer db transaction created in 'remote_server_post_analytics'. `transaction.atomic()` block in 'process_downgrade' resulted in savepoint creation. This commit adds `savepoint=False` to avoid that. --- corporate/lib/stripe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 58c36422e4..671102a1b2 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -4540,7 +4540,7 @@ class RemoteRealmBillingSession(BillingSession): def process_downgrade( self, plan: CustomerPlan, background_update: bool = False ) -> None: # nocoverage - with transaction.atomic(): + with transaction.atomic(savepoint=False): old_plan_type = self.remote_realm.plan_type new_plan_type = RemoteRealm.PLAN_TYPE_SELF_MANAGED self.remote_realm.plan_type = new_plan_type @@ -4967,7 +4967,7 @@ class RemoteServerBillingSession(BillingSession): def process_downgrade( self, plan: CustomerPlan, background_update: bool = False ) -> None: # nocoverage - with transaction.atomic(): + with transaction.atomic(savepoint=False): old_plan_type = self.remote_server.plan_type new_plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED self.remote_server.plan_type = new_plan_type