Zulip production suite / ${{ matrix.name }} (zulip/ci:bookworm, --test-custom-db, Debian 12 production install with custom db name and user, bookworm) (push) Blocked by required conditions
Zulip production suite / ${{ matrix.name }} (zulip/ci:jammy, , Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga, jammy) (push) Blocked by required conditions
The instructions said to decode the "URL-encoded" channel name, topic,
and message ID from a `#narrow/...` URL. That was wrong twice over: the
channel is just the leading numeric ID (which we already tell clients
to use as the operand, not a decoded name), and the topic isn't plain
URL-encoded — it's hash-encoded, `.`-escaped rather than `%`-escaped,
so it needs `decodeHashComponent`, not a plain URL-decode. The message
ID is a bare integer, handled as the anchor/`with` operand. Reword to
that effect, and fix the code block's language tag so it highlights.
The example built the request URL by dropping a raw JSON narrow
straight into the query string, which isn't a valid request — the
brackets, quotes, and any special characters all need to be
percent-encoded. Build the narrow with json.dumps and the whole query
with urlencode so the rendered example is a correct, copy-pasteable
request, and say plainly that the narrow value must be URL-encoded.
The tusd pre-finish hook opened a GetObject on the just-uploaded
object to sniff its charset, and then issued a self-CopyObject on
that same object (to fix up its Content-Type/Content-Disposition)
while that GetObject's response stream was still open and unread
for any content type other than text/plain.
S3 itself does not mind this, but S3-compatible backends that use
per-object locking for consistency (e.g. MinIO) hold a read lock
for the duration of an open GetObject; the concurrent CopyObject
then blocks until the idle GetObject is torn down by the backend's
write-deadline timeout, stalling every upload by tens of seconds.
Only open the object at all when we actually need to inspect its
bytes (charset sniffing only applies to text/plain), explicitly
close that read before the self-copy runs, and open a fresh,
unrelated read afterwards for create_attachment/maybe_thumbnail.
Fixes#39752.
Previously, search highlights used a hardcoded dark yellow background
in dark_theme.css, with no adjustment to the text color, resulting in
low-contrast light text on a dark yellow background.
Fix this by moving the background to a light-dark() variable and
adding --color-text-search-highlight to force dark text on the
highlight background in dark mode. Also add proper link colors and
hover states when highlights appear inside rendered-markdown links.
Fixes#12614.
Previously, alert words in dark mode used a dark orange background
that had poor contrast with the surrounding light-colored text.
Fix this by using a lighter orange background via light-dark(), and
adding --color-text-alert-word to force dark text on the background
in dark mode. Also add proper link colors and hover states when alert
words appear inside rendered-markdown links.
Fixes part of #12614.
Clicking a formatting-list button on a line that already had the
other list marker (e.g. clicking numbered on a "- item" line)
prepended the new marker without stripping the old one, producing
"1. - item". Repeated clicks stacked further ("- 1. - item", etc.)
instead of switching the marker.
Strip any existing bulleted or numbered marker before applying
the target marker (same as using Google Docs and Microsoft word),
so switching between list types replaces the marker cleanly.
PR #38855 made list formatting skip blank lines between items.
But `should_mark` still inspected every line, and blank lines
never satisfy `is_marked`. So any selection containing blank
lines was always classified as "needs marking", and each click
on the list button prepended another marker instead of toggling
off (e.g. "- item" became "- - item").
Exclude blank lines from `should_mark` so the toggle reflects
only the lines that actually carry a marker.
Clicking a compose banner's close button with the mouse left a
square focus outline around it, because the button is an
<a role="button" tabindex="0"> element, and the global focus rules
in zulip.css use plain :focus, which matches mouse-initiated focus.
Suppress the outline with :focus:not(:focus-visible), scoped to
compose box banners and the message edit form banners that share
the same templates; keyboard (Tab) focus still shows the outline
via :focus-visible.
The raw-SQL rewrite of this migration to de-duplicate emoji in the
database left two bugs on the code path that actually recreates an
orphaned emoji. Both were invisible in testing because that path
only runs when a realm has a missing RealmEmoji to restore; the
common empty case was the only one exercised.
First, `SELECT *` over the `distinct_emoji` CTE left-joined to
zerver_realmemoji returns every column of both tables, so unpacking
each row into a single variable raised "too many values to unpack";
we only want the emoji value, so select just that column.
Second, unlike the ORM's JSONField, a jsonb column read through a raw
cursor comes back as a string rather than a decoded dict, so the value
must be run through orjson.loads before it can be indexed.
The traceback that would be produced while running the original
migration, when trying to recreate an emoji:
```
Applying zerver.0770_recreate_missing_realmemoji...Traceback (most recent call last):
File "/home/zulip/deployments/2026-07-18-19-32-29/./manage.py", line 154, in <module>
execute_from_command_line(sys.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/home/zulip/deployments/2026-07-18-19-32-29/./manage.py", line 115, in execute_from_command_line
utility.execute()
~~~~~~~~~~~~~~~^^
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 436, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 420, in run_from_argv
self.execute(*args, **cmd_options)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 464, in execute
output = self.handle(*args, **options)
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/core/management/base.py", line 111, in wrapper
res = handle_func(*args, **kwargs)
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/core/management/commands/migrate.py", line 353, in handle
post_migrate_state = executor.migrate(
targets,
...<3 lines>...
fake_initial=fake_initial,
)
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/db/migrations/executor.py", line 135, in migrate
state = self._migrate_all_forwards(
state, plan, full_plan, fake=fake, fake_initial=fake_initial
)
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/db/migrations/executor.py", line 167, in _migrate_all_forwards
state = self.apply_migration(
state, migration, fake=fake, fake_initial=fake_initial
)
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/db/migrations/executor.py", line 255, in apply_migration
state = migration.apply(state, schema_editor)
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/db/migrations/migration.py", line 132, in apply
operation.database_forwards(
~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self.app_label, schema_editor, old_state, project_state
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/zulip/deployments/2026-07-18-19-32-29/.venv/lib/python3.13/site-packages/django/db/migrations/operations/special.py", line 196, in database_forwards
self.code(from_state.apps, schema_editor)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/zulip/deployments/2026-07-18-19-32-29/zerver/migrations/0770_recreate_missing_realmemoji.py", line 62, in recreate_missing_realmemoji
for (orphaned_emoji,) in cursor.fetchall():
^^^^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 1)
```
Co-authored-by: Mateusz Mandera <mateusz.mandera@zulip.com>
Zulip production suite / ${{ matrix.name }} (zulip/ci:bookworm, --test-custom-db, Debian 12 production install with custom db name and user, bookworm) (push) Has been cancelled
Zulip production suite / ${{ matrix.name }} (zulip/ci:jammy, , Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga, jammy) (push) Has been cancelled
If a user takes longer than REDIS_EXPIRATION_SECONDS to return from
the IdP, the relayed_params data stored in redis at the start of the
authentication attempt has expired, and the old
`assert relayed_params is not None` crashed with a 500.
If the state data is expired, we now log at the info level and return
None, which redirects the user back to the login page to retry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Django ORM rewrite of message fetch (commit c286f39d46) changed
limit_query_to_range to bound and order the before/after pagination
queries on zerver_message.id. But the scan that actually walks and
orders those rows is the zerver_usermessage (user_profile_id,
message_id) index, and PostgreSQL does not propagate an inequality on
zerver_message.id across the outer join onto that index. So the
driving scan lost its bound: fetching num_before/num_after messages
around an anchor far from the user's newest messages walked the user's
entire UserMessage history to fill a ~30-row limit.
The most common trigger is opening the combined feed at the
"first_unread" anchor for a user with a long, old unread backlog. On
chat.zulip.org this took ~98s (client timeout) for a user with ~880k
messages whose first unread was years old; the before-query walked all
~880k rows newest-first, discarding each as newer than the anchor,
before reaching the 30 older ones it needed.
Restore the pre-rewrite behavior by bounding and ordering the range on
zerver_usermessage.message_id, which equals zerver_message.id but lives
on the driving index. A non-selected alias carries it and reuses the
existing join, so no second join is added and the index seeks straight
to the anchor, reading ~30 rows instead of the whole history.
Recent anchors were unaffected, since the unbounded scan already
started at the correct (newest) end and stopped after 30 rows; that is
why this escaped tests and everyday use. The golden-SQL tests are
updated to assert the bound and ordering are on the usermessage column.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a "Mobile push notifications enabled by default" checkbox to the
channel settings General tab (between channel folder and email address)
for existing channels. In the channel creation form, the checkbox
appears at the end of the Subscription management section and is
only shown to organization administrators.
When subscribing to a stream with default_push_notifications=True,
the new subscription gets push_notifications=True. Live stream
update events keep the setting in sync without a reload.
Document the setting with a new help center article, and cross-link it
in relevant help center articles.
Fixes#23873.
Rename the "Subscription permissions" section to "Subscription
management" in the channel creation form and the channel settings
Permissions tab, since it covers both subscription and subscriber
management.
When default_push_notifications is True on a stream, new
subscriptions get push_notifications=True instead of inheriting
from the account-level default. Re-subscribing a previously
unsubscribed user preserves their stored preference.
Realm administrators can set this via PATCH /streams/{stream_id},
POST /users/me/subscriptions, and POST /channels/create.
Fixes part of #23873.
Boolean field (default False) on the Stream model that controls
whether new subscribers get push_notifications=True instead of
inheriting from their account-level default.
Adds the field to all channel API response objects (BasicChannelBase,
Subscription, GET /streams, GET /events, POST /register).
update_func was passed to typeahead setup functions to trigger a
callback on pill creation. Update callers to register these
callbacks via onPillCreate instead, consistent with how
onPillRemove is already used.
This is behavior-preserving. custom_profile_fields_ui was the only
caller passing update_func; the other set_up_* callers never used
it, so dropping the option from those signatures changes nothing
for them. The onPillCreate callback is registered after the loop
that pre-fills existing field values, so — like update_func before
it — it fires only for user-initiated additions, not for the pills
restored on load.
Extract the construction of a UserPill data object out of
append_person into a standalone exported function, with no
behavior change.
This is groundwork for the in-place pill editing work in #38345,
where the edit path builds the pill data to update an existing
pill's contents in place, rather than appending a new pill to the
container as append_person does.
The right sidebar toggle has the same issue: hide-right-sidebar
changes the feed's max-width, rewrapping text and jumping the
feed on expand.
Use the toggle_sidebar_preserving_selected_row_offset helper
added in the previous commit. Observed in a narrower
~1200-1300px range.
Toggling the hide-left-sidebar class changes the feed's
max-width, which rewraps text and shifts row positions. The
browser preserves scrollTop on hide but not on show, so the
feed jumps on expand.
Add a toggle_sidebar_preserving_selected_row_offset helper
that captures the selected row's viewport offset, toggles the
given body class, then restores the offset via
view.set_message_offset. Use it in the left sidebar handler.
Drop the previous scroll_to_selected() call, which only ran
below the xl breakpoint but never at xl widths where the
layout also shifts (~1200-1600px). It also recentered on the
selected row rather than preserving the user's reading
position.
The left-sidebar dragstart handler calls e.target.blur() to drop the
focus outline Chrome draws when a drag begins. But e.target is the
innermost node where the drag started, which need not be an element
with a blur() method: dragging a text selection (e.g. an accidentally
selected channel name) makes it a Text node, throwing "e.target.blur
is not a function".
Only call blur() when e.target is an HTMLElement. A Text node can
never hold focus or show a focus outline, so there is nothing to clear
in the skipped case; focusable sidebar controls are all HTMLElements
and still get blurred.
https://chat.zulip.org/#narrow/channel/464-kandra-js-errors/topic/TypeError.3A.20e.2Etarget.2Eblur.20is.20not.20a.20function/near/2471801
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes this error in
`convert_mattermost_data --combine-into-one-realm` when there are users
with `"team": null`:
```
Traceback (most recent call last):
File "/home/elev3n/laboratory/zulip/zulip/./manage.py", line 154, in <module>
execute_from_command_line(sys.argv)
File "/home/elev3n/laboratory/zulip/zulip/./manage.py", line 115, in execute_from_command_line
utility.execute()
File "/home/elev3n/laboratory/zulip/zulip/.venv/lib/python3.10/site-packages/django/core/management/__init__.py", line 436, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/elev3n/laboratory/zulip/zulip/.venv/lib/python3.10/site-packages/django/core/management/base.py", line 420, in run_from_argv
self.execute(*args, **cmd_options)
File "/home/elev3n/laboratory/zulip/zulip/zerver/lib/management.py", line 133, in execute
super().execute(*args, **options)
File "/home/elev3n/laboratory/zulip/zulip/.venv/lib/python3.10/site-packages/django/core/management/base.py", line 464, in execute
output = self.handle(*args, **options)
File "/home/elev3n/laboratory/zulip/zulip/zerver/management/commands/convert_mattermost_data.py", line 78, in handle
do_convert_data(
File "/home/elev3n/laboratory/zulip/zulip/zerver/data_import/mattermost.py", line 1130, in do_convert_data
mattermost_data = mattermost_data_file_to_dict(import_jsonl_file, combine_into_one_realm)
File "/home/elev3n/laboratory/zulip/zulip/zerver/data_import/mattermost.py", line 1071, in mattermost_data_file_to_dict
for team in row[data_type]["teams"]:
TypeError: 'NoneType' object is not iterable
```
Zulip production suite / ${{ matrix.name }} (zulip/ci:bookworm, --test-custom-db, Debian 12 production install with custom db name and user, bookworm) (push) Has been cancelled
Zulip production suite / ${{ matrix.name }} (zulip/ci:jammy, , Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga, jammy) (push) Has been cancelled
`near` and `with` are pure navigation operators that don't filter
messages on the client (build_term_predicate returns null for both,
alongside `date`), so negating them is meaningless. `id` does have
a filter predicate, but `-id:N` (every message except one specific
one) isn't a useful operation either. Treat all three as invalid
when negated, matching the existing handling of `-date:`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deleting the last message in a direct message conversation left a
stale row in the left sidebar until the next reload, unlike topics,
where deleting the last message removes the topic.
The client can't always tell from local state whether a deletion
emptied a conversation, since it typically holds only a recent window
of each conversation's history. We mirror the approach
stream_topic_history already uses for topics: track a count of locally
known messages per conversation, and decrement it on a deletion. If
it reaches zero the conversation may now be empty, so we optimistically
remove it from the sidebar and ask the server to confirm, re-adding it
if any messages actually remain (which also covers a new message
arriving while the request is in flight).
Only delivered messages are counted, so a locally echoed message isn't
double-counted once the server confirms it. As with topics, a sent
message is counted when the server acks it, in echo.reify_message_id.
A conversation with an unacked local echo (a pending or failed send)
is kept in the sidebar even when we know of no delivered messages,
since the echo is still visible to the user, again mirroring how
stream_topic_history surfaces locally-echoed topics.
Direct message delete_message events don't identify the conversation,
so we derive it from a deleted message in the local cache before it is
removed.
Fixes#29275.
RecentDirectMessages.insert built the conversation key from the
recipient user ids with its own inline logic: substituting our own
id for a self-DM, then sorting and joining.
people.pm_lookup_key_from_user_ids already computes exactly this key,
so call it instead of duplicating the normalization. A later commit
needs the same key when counting and removing conversations, and
reuses this helper there too.
This is a no-op refactor.
Both stream_topic_history_util and pm_conversations_util need to ask
the server for the newest message in a narrow to decide whether a
topic or direct message conversation is really empty after a local
deletion. The request, response parsing, and error handling were
identical, so extract them into a shared get_last_message_id_in_narrow
helper in message_util and have update_topic_last_message_id use it.
A later commit reuses it for direct messages.
Previously, get_mentioned_user_group looped over every message's
mentioned_user_group_id without deduplicating, so a group mentioned
across N messages in the same batch triggered N redundant NamedUserGroup
lookups and N redundant recursive membership queries for an answer
that cannot change within a single call.
Deduplicate the mentioned group IDs with dict.fromkeys (preserving
message order, so ties in group size still resolve the same way as
before) and fetch them in a single NamedUserGroup.objects.filter(id__in=...)
query instead of querying once per message.
Verified via CaptureQueriesContext that a batch of 5 messages
mentioning the same group dropped from 10 queries to 2
with identical output.
After an upload failed, its error banner (like "file too large")
stayed on screen. Starting a new, valid upload would then succeed
while the old error was still showing, which looked confusing.
Now we remove any leftover upload error banner when a new upload
starts, so only the current upload's status is shown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An upload error banner (such as "file too large") would flash and
immediately disappear when the compose box was empty. Removing the
failed upload's placeholder empties the textarea, which triggers
compose validation; that calls clear_errors(), which removed every
".error" banner -- including the upload error banner, whose
lifecycle is managed by the upload flow.
Split clear_errors() into clear_validation_errors() and
clear_upload_errors(). Validation now calls
clear_validation_errors(), which excludes ".upload_banner", so it
only clears the banners it owns and re-derives on each run.
Changing the compose recipient relied on validation's broad
clear_errors() to remove upload error banners; keep that behavior
by calling clear_upload_errors() explicitly in
update_on_recipient_change().
With text already in the textarea, validation did not re-run on
placeholder removal, which is why the bug only appeared with an
empty compose box.
https://chat.zulip.org/#narrow/channel/9-issues/topic/error.20message.20for.20.27attachment.20too.20large.27.20is.20inconsistent/with/2491153
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zulip production suite / ${{ matrix.name }} (zulip/ci:bookworm, --test-custom-db, Debian 12 production install with custom db name and user, bookworm) (push) Has been cancelled
Zulip production suite / ${{ matrix.name }} (zulip/ci:jammy, , Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga, jammy) (push) Has been cancelled
The formatting-bar scroller arrows hide when focus is within the
formatting buttons.Clicking a button briefly puts focus inside the
formatting bar before returning it to the textarea, which caused
the arrows to blink off and back on during the click.
Use :focus-visible instead of :focus-within so the hide rule
only triggers for keyboard focus, not for click-induced focus.
This generalizes to all clickable buttons in the bar (formatting,
upload, etc.) without enumerating each class.
Pass the is_sending_saving flag through format_draft into the
FormattedDraft type. The flag already exists on LocalStorageDraft;
this makes it available to templates without changing any behavior.
Fixes part of #32999.
Wrap the drafts list in a .drafts-tab-pane container and update the
selectors that walk the drafts list. Add the min-height/height rules
that keep the list scrolling correctly inside the new wrapper. This
is a no-op refactor that prepares the overlay for an outbox tab living
in a sibling .outbox-tab-pane container.
Large realm imports can a long time serially uploading attachments
to S3, one blocking HTTP round-trip at a time. Wrap the transfer
loop in run_parallel_queue so workers can upload concurrently: each
worker calls _init_upload_worker at startup to construct its own
UploadDestination (a boto3 Bucket isn't fork-safe to share), and
_execute_upload pulls it from a ContextVar per plan.
Planning (which populates path_maps and resolves S3 metadata via
ID_MAP and get_user_profile_by_id) stays serial in the main process,
so downstream code sees a fully populated path_maps before it runs
and workers don't need the import's in-memory state.
The per-record transfer loop branched on s3_uploads for each file,
computing its S3-specific metadata inline and picking between
key.upload_file and shutil.copy. Split that out so the
destination-specific I/O lives on a small UploadDestination type,
with one S3Destination and one LocalDestination subclass, and the
loop just calls destination.execute(plan) for each record.
build_plan always populates content_type and metadata; LocalDestination
ignores them, which keeps UploadPlan self-consistent rather than
having destination-dependent required fields.
This is preparation for parallelizing the transfer phase; the only
behavior change is that local-filesystem imports now do the same
per-record get_user_profile_by_id lookup that S3 imports did before,
which is cheap against Django's cache.
missing_thumbnails asserted that ImageAttachment.content_type is set.
That holds for anything maybe_thumbnail creates, but not for rows
migration 0660 backfilled from Attachment.content_type: uploads from
before we recorded a content-type left it null, and a blank ?mimetype=
parameter (before we guarded against it) left it empty.
The assertion fires on two paths for such an image: the thumbnail
queue worker (e.g. when `manage.py thumbnail` re-thumbnails old
images), and the thumbnail-status endpoint in zerver/views/thumbnail.py,
where an unhandled AssertionError becomes a user-facing 500.
Use needs_transcoded_format, which judges a null or empty content-type
as renderable inline (no transcoded format), exactly as
get_transcoded_format does when serving. Sharing the helper keeps the
generate and serve sides from disagreeing, which would otherwise
produce a transcoded format that is never served, or vice versa.
This stands on its own: it fixes the crash on every deployment,
including self-hosted installs using local uploads, where no S3-side
content-type backfill runs. Backfilling the underlying data is a
separate change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the predicate from the negated "content_type is None or the
bare type is inline" to the equivalent positive "has a content-type
and the bare type is not inline". Besides reading better under the
function's name, testing the content-type for truthiness first makes
the treatment of a missing content-type explicit -- null or empty is
judged inline -- rather than leaning on the non-obvious fact that
bare_content_type("") is "text/plain", which is itself inline.
No behavior change: for every content-type, including null and empty,
the two forms agree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_transcoded_format and missing_thumbnails both decide, from an
image's content-type, whether it needs a transcoded web-safe format;
the two must agree, or we generate a transcoded format that is never
served, or skip one that is needed. Extract the check so they can
share it.
This commit is a pure refactor: the helper is the exact negation of
the condition it replaces in get_transcoded_format, so behavior is
unchanged. The next commit rewrites it into a clearer positive form,
and a later commit puts missing_thumbnails on the shared helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zulip production suite / ${{ matrix.name }} (zulip/ci:bookworm, --test-custom-db, Debian 12 production install with custom db name and user, bookworm) (push) Has been cancelled
Zulip production suite / ${{ matrix.name }} (zulip/ci:jammy, , Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga, jammy) (push) Has been cancelled
There can be cases where realm.plan_type is set to one of the
paid plans but there are no CustomerPlan or Customer objects
for that realm.
This happens when support staff have manually set plan_type.
We want to allow such realms to be able to configure
two tier billing before they start paying, so this commit
updates realm_eligible_for_non_workplace_pricing and
realm_on_discounted_cloud_plan such that UI is enabled for
the setting.
build_person_matcher lets a search query match a user by id, but
compared with startsWith, so typing part of an id (e.g. "30")
matched every user whose id begins with those digits. That floods
results with unrelated users and collides with full names that start
with digits, which can then be crowded out of a capped result list.
User id lookup, added in 6c355f6532 (people: Allow user to lookup
users using user id.), only needs to find the user whose id was typed
in full, so compare against the whole id instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes https://chat.zulip.org/#narrow/channel/9-issues/topic/Search.20typeahead.20matches.20get.20slashed.20sometimes/with/2479827
When typing a person operator like `dm:`, `get_suggestions`
canonicalizes the term, resolving a unique full name to that user's id
(e.g. `dm:John` becomes `dm:50`). `make_people_getter` then queried
people by that id instead of the typed text, collapsing the dropdown to
the single exactly-named user `dm:joh` listed both "John" and "John
Doe", but `dm:John` listed only "John".
Now, we track the originally typed text on the term and use it for the
people query, so suggestions match what the user typed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
208c0c3 fixed `do_delete_user` to stop generating dummy users with
invalid addresses of the form deleteduser@http://zulip.example.com,
and `0439_fix_deleteduser_email` repaired the `delivery_email`
field of users deleted before that fix. However, on realms whose
`email_address_visibility` setting was "everyone", the `email` field
held a copy of the same invalid address; such values still break
realm import.
Since `delivery_email` is guaranteed by migration `0439` to have
already been repaired on every affected row, and the `email` field
of the affected rows is by definition a copy of `delivery_email`,
we restore the invariant by copying the repaired value.
Fixes#28622.
During tests, `fixture_to_headers` generates the `HTTP_X_GITLAB_EVENT`
header by splitting the fixture name on `__` and taking the left half.
For Confidential events the header should start with `Confidential`
but the fixture names `issue_hook__confidential_issue_*` and
`note_hook__confidential_issue_note` produced headers without one.
This was harmless in practice because Confidential Hooks and Normal Hooks
are handled by the same event handler, so they rendered identical messages
either way.
failed_message_success and message_send_error looked up the message
with a non-null assertion (message_store.get(id)!) before writing
failed_request, throwing "Cannot set properties of undefined (setting
'failed_request')" when the id is no longer in the store.
This happens when a send appears to fail on the client but actually
reached the server. The user clicks resend; if the original's
get-events delivery arrives first, it reifies the local id to the real
server id and re-keys the store. The resend's success handler then
reifies the already-consumed local id to its own (duplicate) server
id, which early-returns, so that id is never stored, and
failed_message_success dereferenced the missing entry. The error
handler is exposed the same way if the message is removed from the
store while a resend is in flight.
Guard both handlers: if the message is gone it has already been
reconciled or removed, so there is nothing to mark.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resend_message drives the manual retry of a previously failed send.
It was module-private and had no direct test coverage. Export it and
add node tests for its success and error callbacks while the message
is still in the store: the success path clears failed_request and the
error path sets it.
This is preparatory for the next commit, which guards those callbacks
against a message that is no longer in the store. The tests added here
pin the existing behavior when the message is present, so the guard
does not silently regress it.
No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously, clicking inside the flatpickr calendar (e.g., on month or
year) would hide the scheduling popover. Prevent this by ensuring
interactions within flatpickr are not treated as outside clicks,
so the popover remains open while selecting a custom time.
Fixes: #35464
The Slack webhook view code read payload.get("event", {}) with an
empty-dict default, but the next line immediately expected a key of
"type" with a string value. When a payload arrived without an event
object, this raised a ValidationError, which returned as errors in
our logs.
A well-formed Slack "event_callback" request always contains an
event object.
There are two documented event payloads where there is no event
object. The "url_verification" payloads are already handled in the
Slack webhook view code. The "app_rate_limited" payloads now return
UnsupportedWebhookEventTypeError.
Otherwise, if the event object is missing from the payload, then an
AnomalousWebhookPayloadError is now returned.