Commit Graph

11692 Commits

Author SHA1 Message Date
sathwikshetty33
f93299c79a bots: Simplify service type definition and initialization.
The prior type of `service` hid that `services?.[0]` is
undefined for embedded bots without stored `config_data`,
forcing a defensive trailing `assert(service && ...)` at the
outgoing render. It also duplicated the union already
exported from `bot_data`.

Co-authored-by: Aditya Kasaudhan <akasaudhan02@gmail.com>
Co-authored-by: Satyam Bansal <sbansal1999@gmail.com>
2026-05-29 18:39:39 +02:00
sathwikshetty33
8334046113 bots: Replace if-else statements with switch-case for bot type.
Co-authored-by: Aditya Kasaudhan <akasaudhan02@gmail.com>
Co-authored-by: Mukul Goyal <96649866+Mukul1235@users.noreply.github.com>
2026-05-29 18:39:39 +02:00
sathwikshetty33
66ac1f53a5 bots: Rename INCOMING_WEBHOOK_BOT_TYPE to INCOMING_WEBHOOK_BOT_TYPE_INT.
This matches the naming convention of the existing
OUTGOING_WEBHOOK_BOT_TYPE_INT constant, since both hold integer values.

Co-authored-by: Aditya Kasaudhan <akasaudhan02@gmail.com>
Co-authored-by: Mukul Goyal <96649866+Mukul1235@users.noreply.github.com>
2026-05-29 18:39:39 +02:00
apoorva
55ddde8411 copy_paste: Preserve timestamps across copy-paste.
When a user partially selects text inside a rendered <time> element,
Chrome's clipboard serializer strips the <time> wrapper from the
paste HTML along with the .timestamp-content-wrapper span, losing
the `datetime` attribute needed to reconstruct the `<time:ISO>`
markdown.

We expand the selection range to cover the full <time> element (same
trick the KaTeX path uses to keep its annotation), and at copy time
wrap the localized date text in a fresh `<span data-datetime="...">`.

The paste rule recovers the markdown via the surviving `<time>` tag
(cross-element selections) or the wrapping `<span data-datetime>`
(in-time selections, where Chrome has stripped the <time>).

Fixes: https://chat.zulip.org/#narrow/channel/138-user-questions/topic/Copy-paste-ability.20of.20global.20times/with/2462937
2026-05-29 16:14:29 +05:30
Lauryn Menard
350d7c5d2d message-list-view: Remove hack for undefined StreamSubscription.
In message_list_view.populate_group_from_message, the tutorial
referred to in the comment for the case when a stream message's
`stream_id` returns an undefined, instead of a StreamSubscription
object, was removed in commit be7f6db854.

Since we now can assert that a StreamSubscription object is
returned for the message in question, we can populate the
MessageGroup fields via the fields in that object, instead of
calling various functions that check for the same object.

Removes the undefined case from stream_data.can_resolve_topics
as well.
2026-05-29 13:45:51 +05:30
Evy Kassirer
514ac66a86 stream_list: Specify which stream is being narrowed to zoom_in.
zoom_in previously inferred its stream from
topic_list.active_stream_id(), which only works when the topic list
is already active for the target stream. Pass the stream_id in
explicitly so the caller controls which stream is zoomed, and narrow
to that channel's feed first when it isn't already the current
narrow.

Preparation for filtering topics across all channels from the left
sidebar, where zoom-in may target a stream that isn't currently
narrowed.
2026-05-29 10:59:24 +05:30
Evy Kassirer
a0171a7c15 topic_list: Clarify topic search term function to only be for zoomed.
get_zoomed_topic_search_term reads the zoomed-in topic filter input
(#topic_filter_query), which exists only in the more-topics modal.
Rename it to make that explicit, differentiate it from the upcoming
top-level left sidebar topic search, and assert that it's only
called while zoomed.

filter_topics_left_sidebar runs for both the zoomed topic list and
the inline topic lists under streams, so read its search term from
the zoomed input when zoomed and the left sidebar search box
otherwise, preserving existing behavior. The remaining callers are
all zoomed-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:59:24 +05:30
Evy Kassirer
acc850a3e1 unread: Mark all narrow-matching messages as read from banner.
The "Mark as read" banner click handler previously only marked
messages currently loaded in the message list as read, missing
unreads outside the locally fetched range. For example, in the
DM feed (is:dm), older DM conversations with unreads would not
get marked as read.

Fix this by using the server-side narrow API
(bulk_update_read_flags_for_narrow) to mark every message
matching the current narrow as read, the same pattern used by
mark_stream_as_read and mark_topic_as_read.

This banner can appear in any non-conversation feed view
(channel feed, DM feed, combined feed, mentions, starred,
search, etc.), so the fix applies broadly.

The narrow API was added in b1ca1fd606, after the BUG comment
in 6afdf2410d flagged this limitation.

Reported at: https://chat.zulip.org/#narrow/channel/9-issues/topic/DM.20mark.20all.20as.20read.20only.20marking.20some.20unreads/with/2463479

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:51:02 +05:30
Amit Patil
852e481f50 message: Apply edit/move button classes on re-render.
When a message is re-rendered, we currently don't apply the edit/move
button classes to the newly created DOM element.

In this commit, when a message is re-rendered, the edit/move button
classes are applied.
2026-05-28 14:14:32 +05:30
Evy Kassirer
20cd33fcab channel_settings: Apply folder filter regardless of left tab.
The empty-right branch of change_state previously set the folder
filter dropdown only when left_side_tab === "all". This came from the
old structure where the folder filter set was buried inside the
section === "all" branch, but it caused a preexisting bug: clicking
"View channels" from a folder popover as a guest passed
left_side_tab = "subscribed" (since guests can't use the "All" tab),
which skipped the folder-filter setup — so guests saw all their
subscribed channels rather than the folder they clicked.

Drop the left_side_tab guard. The folder filter is just a dropdown
value; applying it whenever folder_id is supplied makes the filter
work across all left tabs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:05:37 +05:30
Evy Kassirer
227293abe5 channel_settings: Split right-panel state from left tab in change_state.
URL slot 1 (`#channels/<slot1>/...`) was overloaded as a left-panel
tab, the create-channel sentinel ("new"), or a channel ID, all passed
to `change_state` as one `section: string`. Parse it at the hashchange
boundary into `right_panel: "new" | number | undefined` and a separate
`left_side_tab`, and dispatch `change_state` on `right_panel` in an
if/else over the three cases.

`right_side_tab` is now strictly the in-channel tab (general /
personal / subscribers / permissions), no longer overloaded with
"new". Opening the create form preserves the user's current left tab
instead of resetting it.

Fixes #27730.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:05:37 +05:30
Evy Kassirer
a3947e7c4b channel_settings: Rename toggler key "all-streams" to "all".
The left-panel toggler had keys "subscribed", "available", and
"all-streams"; the URL hash sections are "subscribed", "available",
and "all". The "all" vs "all-streams" mismatch required a one-line
mapping in URL parsing. Rename the toggler key to "all" so the
toggler and URL hash sections use the same vocabulary.

No functional changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:05:37 +05:30
Evy Kassirer
2b3adff517 channel_settings: Pass undefined, not "", for unused right_side_tab.
The folder-create flow at #channels/folders/<id>/new calls change_state
and launch with right_side_tab = "". The create-form branch in
change_state never reads right_side_tab, so this empty string was
vestigial. Pass undefined to match the parameter's `string | undefined`
type and signal "no value" explicitly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:05:37 +05:30
Arun-kushwaha007
142db947e7 settings: Improve Code Playground language typeahead alias matching.
Improve alias matching in the Code Playground settings typeahead.

Typing an alias such as py, py3, or python3 now surfaces the canonical
language option instead of suggesting a custom language entry.

This ensures canonical languages are prioritized when matched via aliases.

Fixes #24045.
2026-05-28 14:03:15 +05:30
Evy Kassirer
a718205f98 left_sidebar: Add search-topics button to expanded channel row.
Filtering across all of a channel's topics (rather than just the
cached ones) requires clicking "Show all topics," which is not
discoverable. This adds a search icon in the expanded channel's
row, to the left of the "+ new topic" button, that opens the
"show all topics" view with the filter input focused — giving
users a clear visual affordance for searching topics within a
channel.

The icon only appears on the expanded channel row, mirroring the
zoom-in mechanism, which only operates on the active channel.
The grid container for the left-sidebar controls now flows in
columns so multiple icons sit side-by-side rather than stacking.

Discussion:
https://chat.zulip.org/#narrow/channel/101-design/topic/Filter.20topics.20within.20channel.20button/with/2369643

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:33:17 +05:30
Pratik Chanda
d17d623b3d search: Remove dead openInputFieldOnKeyUp code.
`openInputFieldOnKeyUp` in the search typeahead was used to open the
search input when the user started typing while the search box was
closed.
In commit 75dab825fe, we removed the
ability to type into closed search box, making this code path
unreachable.
We remove this option altogether from bootstrap_typeahead
module.
2026-05-27 15:13:12 -07:00
PieterCK
bed2c6bf57 stream_settings: Focus on an edit modal field when opened.
Focus on the "Channel name" field if opened from the title-adjacent
button and "Description" field if opened from the description
field-adjacent button.

Fixes:
https://chat.zulip.org/#narrow/channel/9-issues/topic/missing.20focus.20on.20edit.20channel.2Fgroup.20name.2Fdescription.20modal/with/2447561
2026-05-27 20:59:27 +05:30
PieterCK
d7fc8a689c user_group_create: Focus on name field on rename deactivated group modal. 2026-05-27 20:59:27 +05:30
PieterCK
a2871c9dda user_group_settings: Focus on an edit modal field when opened.
Focus on "User group name" field if opened from the title-adjacent
button and "User group description" field if opened from the
description-adjacent button.

Fixes:
https://chat.zulip.org/#narrow/channel/9-issues/topic/missing.20focus.20on.20edit.20channel.2Fgroup.20name.2Fdescription.20modal/with/2447561
2026-05-27 20:59:27 +05:30
PieterCK
db4328236b ui_utils: Update place_caret_at_end to properly handle text area input.
Previously the function doesn't work for HTMLTextAreaElement, it'll only
focus but doesn't place the cursor at the end.

setSelectionRange is a valid method for HTMLTextAreaElement:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement/setSelectionRange
2026-05-27 20:59:27 +05:30
Evy Kassirer
792d65b616 tippy: Destroy instance or fall back to body when reference is detached.
Tippy schedules show() via setTimeout to honor the hover delay. With
our default appendTo: "parent", the mount path resolves "parent" to
reference.parentNode. If the reference was removed from the DOM
during the delay (e.g., the user hovers a recipient-bar icon, then
narrows away before the 100ms delay elapses, causing the feed to
re-render and detach the icon), parentNode is null and tippy
crashes on parentNode.contains(popper).

Fix this with two complementary changes to the default config:

1. A default onShow that destroys the instance and cancels the show
   when the reference is no longer in the DOM. This keeps the DOM
   clean: no popper is mounted at all.

2. An appendTo function that falls back to document.body when
   reference.parentElement is null. This acts as a structural
   backstop, since tippy's onShow is replaced (not composed with) by
   per-instance onShow handlers; any tooltip overriding onShow would
   otherwise lose the isConnected check.

Reported in Sentry as ~50 events from 41 users over 90 days; the
stack trace lands in the setTimeout callback at the
parentNode.contains line, consistent with this scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:48:18 +05:30
Alya Abbott
9b358322e8 inbox, recent: Match message feed's 5px row focus-ring radius.
The 10px radius read overly rounded next to the message feed's
selection box, which uses 5px. Bring `.inbox-row`, `.inbox-header`,
the inbox icon-action focus rings, and `.recent-view-body-row` into
visual consistency with that established pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
8649c92a3c inbox, recent: Extra-light reveal for keyboard-only row selection.
When a row is keyboard-selected but not mouse-hovered, the default-
state action buttons (three-dots menu, default visibility marker) sit
at the tertiary/secondary tier's full reveal opacity, which is louder
than necessary for keyboard navigation: the user has selected the row
but is just orienting, not yet trying to act on it. Drop these to 0.2
in that state so the buttons remain perceivable but recede.

For inbox, the rule scopes to `:focus-within:not(:focus)` (the
row owns focus through children) so mouse hover restores the louder
reveal as soon as the cursor lands. The target elements also carry
`:not(:focus-visible)` so the row-scoped rule yields to a button's
own `:focus-visible` styling once the user arrows into the button —
otherwise the row-scoped selector's higher specificity would clamp
the focused button at 0.2 instead of letting the direct-interaction
rule paint at 0.7.

For recent-view, the rule scopes to `:focus:not(:hover)` rather
than `:focus-visible:not(:hover)`: the existing
`.no-visible-focus-outlines` machinery (see focus_outline_util.ts)
already gates the focus reveal until the first keyboard navigation
key, and `:focus-visible` is fragile for programmatic focus calls
in setTimeout (e.g., the `.trigger("focus")` in
`recent_view_ui.set_table_focus`). Mirror the wrapper-focus (0.7)
and body-row keyboard-focus (0.2) tier rules inside the
`.no-visible-focus-outlines` scope, suppressing icon opacity to 0
while the class is present, so programmatic focus on view entry
doesn't reveal the icons.

Recent-view: the tier selectors match both the natively-rendered
`.recent-view-row-topic-menu` and the JS-swapped vdots (signaled by
`data-vdots-original-icon-class` set in `recent_view_ui`), so
adjacent rows with and without a status marker land at the same
opacity in each interaction state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
dbc2ed027b inbox, recent: Apply tiered opacity for action menu and state markers.
Topic state markers (follow/mute/unmute) communicate state and so are
secondary information; the three-dots topic menu communicates no state
and is a tertiary action. Replace the previous opacity values — which
left focus dimmer than hover for keyboard users (0 idle → 0.4 row
hover → 0.2 focus on the menu button) — with a consistent tier shared
by both inbox and recent-view:

- Tertiary action (three-dots menu): 0 idle, 0.45 on row reveal,
  0.7 on direct hover or focus.
- Secondary state (follow/mute/unmute markers): 0.45 idle,
  0.6 on row hover/focus, 0.7 on direct hover.

0.45 — rather than the more obvious 0.4 — is the lowest opacity that
clears WCAG 1.4.11's 3:1 minimum non-text contrast ratio against both
the light (~3.4:1) and dark (~3.9:1) row backgrounds; 0.4 produces
~2.85:1 and fails. The visual difference is imperceptible.

Hover/focus on the icon itself overrides `.recipient_bar_icon`'s
`!important` from `message_header.css`. The inherit-marker icon (the
no-explicit-policy default in inbox) is normalized to the same
opacity values as the explicit-policy markers so all three states
render identically in each tier.

Recent-view: drop the topic-menu wrapper's `outline-offset` from 5px
to 3px: the wrapper sits hard against the recent-view table's right
edge (the table has `overflow: hidden`), so a 5px offset clipped the
outline's right side. The clipping was effectively invisible while
the focused icon was at 0.2 opacity but became apparent once the menu
button moved up to direct-interaction opacity. 3px gives comfortable
clearance while still matching the inbox's 22×22 effective ring size
around a 16×16 icon.

Inbox: several existing rules selected on a parent that doesn't
actually match (e.g., a focus pseudo on the row instead of the
wrapper); rewrite them so they apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
968f2e9362 recent_view: Reserve space for unread badge focus ring.
The focused unread badge's `::before` ring extends `5px`
outside the badge edges, so when the topic name is long
enough to truncate with `...` the ellipsis ends right against
the badge column and the ring's left side paints over the
ellipsis text.

Add `7px` of left padding on
`.recent-view-unread-mention-and-count-wrapper` (5px ring
outset + 1.5px ring stroke + ~1px clearance) so the topic-name
column shrinks just enough to keep `...` clear of the ring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
8e705caf8e inbox: Tighten unread focus ring around the badge.
Move the focus ring on `.unread-count-focus-outline` from a
wrapper-sized `outline` to a pseudo-element on the inner
`.unread_count` badge, matching `recent-view-table-unread-count`'s
pattern: `5px` of breathing room outside the badge with a
`3px` corner radius on the ring. The wrapper-extending outline
left a wide gap between the ring and the badge fill that read
as a second concentric outline; recent's bigger gap reads as
intentional padding around a single badge instead, so the
inbox focus indicator now matches the recent-view treatment
exactly. The badge's inner `.normal-count` `inset` `box-shadow`
is also suppressed on focus so it doesn't sit as a third,
narrower outline inside the ring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
2951db1fb1 recent_view: Swap status-marker to vdots on hover or keyboard focus.
Replace the existing CSS-only `background-image`-based hover swap on
`.visibility-status-icon` with a JavaScript class swap that fires on
hover, keyboard focus of the focusable wrapper, or while the topic
popover is open. The wider trigger set gives keyboard users the same
affordance — "this opens the topic-actions popover" — that mouse
users get on hover, and the sticky popover-open case keeps the icon
linked to the open menu after hover/focus moves into the popover.

The swap toggles the icon-font class itself (e.g.,
`zulip-icon-follow` → `zulip-icon-more-vertical`) and remembers the
original on a data attribute so leave events restore it. Doing this
in JS rather than via `mask-image: url(...)` matters for visual
fidelity: the icon font normalizes `more-vertical.svg` into a
shared em-square that renders the glyph at smaller dimensions than
a direct `mask-size: contain` paint of the same SVG, so a CSS
swap produced visibly different vdots from the row's natively
rendered `zulip-icon-more-vertical` siblings. Routing both states
through the icon font guarantees identical rendering.

The focus trigger gates on the recent-view's own
`no-visible-focus-outlines` flag — the same one that suppresses
focus rings until the first keyboard navigation key (see
`focus_outline_util`). Browser-native `:focus-visible` can match
on programmatic focus that inherits keyboard mode from a prior
element (sidebar tab-then-click, scroll-driven row re-focus on
view entry), and we don't want a surprising vdots reveal in those
cases — the row is focused but the user hasn't acted on the
keyboard yet. `change_focused_element` re-evaluates the swap
state once the flag clears so the first keyboard nav reveals the
glyph alongside the focus ring.

Drop the now-obsolete `filter: invert(1)` hover rule from
`dark_theme.css`: it existed solely to recolor a previously
hard-coded-black `background-image` glyph for dark theme. The
icon font already honors `color` and renders white-on-dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Aman Agrawal
110eb4ad54 inbox, recent_view: Restyle unread-count focus ring.
Replace the inbox `.unread-count-focus-outline` 2px outline with a
1.5px outline insetted 2px and rounded at 5px — matching the
action button and state-marker focus rings (also -2px on a 30px
wrapper) so all four inbox focus indicators read at the same 26px
outer height. Suppress the inner badge's `inset` `box-shadow`
(from `.normal-count`) so it doesn't read as a second, narrower
outline inside the focus ring.

For recent-view, draw the badge focus ring as a `::before`
pseudo-element with inset `-5px` and a 5px radius, matching the
breathing room the inbox wrapper-extending outline gets from its
`padding: 0 5px`. Scope the rule via
`#recent_view:not(.no-visible-focus-outlines)` so the badge ring
stays suppressed alongside the wrapper outline until the first
keyboard navigation key removes the class (see focus_outline_util.ts),
and so the rule has high enough specificity (1,2,1) to beat the
`#recent_view .recent_view_focusable:focus` wrapper rule (1,1,1).
Without that specificity, both rings would paint.

A subsequent commit tightens the inbox ring further so it hugs
the inner badge instead of the row-tall wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
c3ea180d20 inbox: Unify action button and visibility indicator focus rings.
The three icon-style row controls — `.channel-visibility-policy-indicator`,
`.visibility-policy-indicator`, and `.inbox-action-button` — had three
different sizes, click areas, focus-ring colors, and corner radii.
Unify them as 30×30 (row-tall) flex wrappers around their 16×16 icons,
with `cursor: pointer` carried by the wrapper so the entire hit area
feels clickable, and a single shared focus-ring style.

The focus ring lives on the wrapper at full opacity to satisfy
WCAG 1.4.11's 3:1 contrast requirement for focus indicators (an
earlier draft drew the outline on the inner icon inside a 0.7-opacity
wrapper, which dimmed the stroke via opacity inheritance to ~2.7:1 in
light / ~2.4:1 in dark). Negative `outline-offset: -2px` keeps the
1.5px stroke inside the wrapper's 30×30 bounds so it doesn't clip
against the row's `overflow: hidden` edge, and a 10px border-radius
matches the row outline so the button ring visibly rounds at 1× DPR
rather than reading as square. The inner icon dims separately at 0.7
on focus so the button still reads as subordinate to row content.

Icon color is unified across all three wrappers as well: action button
icons inherit the same color as the state-marker icons rather than
their previous distinct shade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
a8e1f1cb87 inbox: Consolidate row and header focus-visible blocks.
The previous commit gave `.inbox-row` and `.inbox-header` identical
`:focus-visible` outline rules. Merge them into one combined
`.inbox-row, .inbox-header` block, alongside the existing icon
visibility rule that already targeted both selectors. The
header-only `.collapsible-button` rules move to a separate
`.inbox-header:focus-visible` block.

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
131299bc83 inbox: Match recent-view row focus outline style.
Replace the 2px inner-border focus mechanism on `.inbox-focus-border`
with a 1.5px outline on `.inbox-row` and `.inbox-header` themselves,
matching `.recent-view-body-row`. Unlike recent-view rows, inbox
rows are tighter (30px), so the outline sits flush with the row
edges rather than insetting 4px. Drop the DM row's separate inset
box-shadow since the unified outline works for multi-line rows
too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
6791e1ca3d inbox, recent_view: Trim filter button focus outline to 1.5px.
Bring `#inbox-filter_widget` and `#recent_view_folder_filter_button`'s
focus outlines from 2px down to 1.5px, matching the row focus
outline width used elsewhere in the same views.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Aman Agrawal
be7c303189 css: Extract focus-ring outline shorthand to a variable.
Pull the `var(--focus-ring-width-in-views) solid
var(--color-outline-focus)` shorthand into a single variable
`--focus-ring-outline` so a future width or color change touches
one place. Convert the one pre-existing usage on
`.recent_view_focusable:focus`; upcoming commits in this branch
introduce six more usages (five `outline:` shorthands and one
`border:` on the `.recent-view-table-unread-count::before`
pseudo-element) that use the variable directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
d47e1395bf css: Extract inbox/recent focus-ring width to a variable.
Pull the 1.5px focus-ring stroke width used by the inbox and
recent views into `--focus-ring-width-in-views` so a future tweak
touches one place. Convert the existing `outline-width: 1.5px`
declarations on the unread-badge hover-outline animation in both
views (and on the inbox row's hover-forwarded badge), the
pre-existing `.recent_view_focusable:focus` outline, and prepare
for upcoming commits that introduce additional 1.5px focus rings.

Scoped to the row-list views: focus widths elsewhere in the
codebase vary (1px in popovers/modals, 2px in the left sidebar
and message feed), so the variable name carries `-in-views` to
signal it isn't a global focus width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
65ef7ff67a recent_view: Suppress 'Mark as read' tooltip on keyboard focus.
The unread-count badge in recent view is keyboard-focusable, and
its Tippy tooltip fires on both mouse hover and keyboard focus by
default. The latter pops a redundant 'Mark as read' bubble every
time the user arrows into the badge, which competes with the
focus ring as the visual cue.

Add `data-tippy-trigger="mouseenter"` so the tooltip only
appears on hover. Inbox unread badges don't have this issue
because their focusable wrapper is the parent of the tooltipped
badge, so keyboard focus on the wrapper doesn't trigger the
inner badge's tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
dead3a4dd7 inbox, recent: Keep action buttons visible while menu open.
When the 3-dots topic menu or stream/visibility-policy popover
is open, the trigger button should stay in the direct-interaction
tier (0.7) rather than fading back to hidden/quiet when the
cursor leaves it. Otherwise there's no visual link between the
open popover and the row it belongs to.

Inbox: add `.visibility-policy-popover-visible` and
`.topic-popover-visible` opacity rules for the state-marker
wrappers and the action button. The former class is already
toggled by user_topic_popover.ts; extend topic_popover.ts to
toggle the latter on `.inbox-action-button` too (it previously
only tagged `.recent_view_focusable`, so inbox got nothing), and
extend stream_popover.ts to do the same for inbox channel
headers, where the trigger is also an `.inbox-action-button`.

Recent: the existing topic-popover-visible rule is already in
its final form on the wrapper from the prior commit, so this
commit only adds the JS plumbing for inbox.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
e46c0c3d9d inbox: Expand unread-count click target to the row-tall column.
Move the `#inbox-list` unread click handler from the inner
`.unread_count` badge to the row-tall `.unread-count-focus-outline`
wrapper, so clicking anywhere in the unread-count column marks
the conversation as read — not just the small grey pill. Data
attributes still live on the inner badge; the handler reads them
via `.find()`. Merges the previous `.on_hover_dm_read` and
`.on_hover_topic_read` handlers into a single one.

Carry `cursor: pointer` and forward the wrapper's `:hover` to
the badge's hover-outline animation so hovering anywhere in the
new click target previews the click affordance, not just hovering
directly over the inner pill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Alya Abbott
29356d29dd inbox, recent: Use a long hover delay for the 'Mark as read' tooltip.
Both views previously showed the 'Mark as read' tooltip on the
unread-count badge with tippy's default zero delay: hover the
badge for any duration and the bubble appears instantly,
covering nearby UI on every casual mouse-over.

Bring both into line with the project's existing
`LONG_HOVER_DELAY` (750 ms) used for similar
hover-to-explain tooltips:

- Inbox templates switch from `tippy-zulip-tooltip` to
  `tippy-zulip-delayed-tooltip` on the unread badges, picking
  up the delegate at `tippyjs.ts:156` that already configures
  `delay: LONG_HOVER_DELAY`.
- Recent's per-row `tippy.delegate` adds `delay: LONG_HOVER_DELAY`
  alongside its existing per-row placement logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:24:58 +05:30
Pratik Chanda
1c0d386d1b templates: Show archived icon for channels in channel_list_item.hbs.
We pass entire stream object context when rendering channel_list_item.hbs
which is then used by stream_privacy and renders the archived
icon correctly.
2026-05-26 16:23:52 -07:00
Pratik Chanda
20a355682c channel_folder_ui: Sort streams by non-archived and archived status.
Currently, all streams in the add channel to folder widget list in
channel folder ui modal are sorted alphabetically, including archived
streams. Instead, first sort non-archived streams alphabetically and
then place archived streams, also sorted alphabetically, at the bottom
of the list.
2026-05-26 16:23:52 -07:00
Pratik Chanda
e371c914ec user_profile: Sort streams by non-archived and archived status.
Currently, all streams in the user channel subscribe widget list in
user profile modal are sorted alphabetically, including archived
streams. Instead, first sort non-archived streams alphabetically and
then place archived streams, also sorted alphabetically, at the bottom
of the list.

We define a helper function compare_stream_by_name in util.ts to
sort the streams.
2026-05-26 16:23:52 -07:00
Pratik Chanda
c4842fc956 user_profile: Use strcmp to sort in user channel subscribe widget.
We use the locale-aware strcmp (backed by Intl.Collator) to sort
channels in the user channel subscribe widget, matching how channels
are sorted elsewhere in the codebase.

The previous sort lowercased both names before comparing, making it
case-insensitive. strcmp uses Intl.Collator's default sensitivity, so
sort order may occasionally differ for channel names that differ only
in case or contain non-ASCII characters.
2026-05-26 16:23:52 -07:00
apoorvapendse
a16f84c409 navbar: Add alt text for realm logo.
Signed-off-by: apoorvapendse <apoorvavpendse@gmail.com>
2026-05-26 15:51:33 -05:00
apoorvapendse
4a81d72c0f navbar: Add aria-labels for navbar icon buttons and links.
Signed-off-by: apoorvapendse <apoorvavpendse@gmail.com>
2026-05-26 15:51:33 -05:00
Evy Kassirer
1a932f7137 channel_settings: Add breathing room around channel name focus ring.
Add `padding-right` to `.selected-stream`/`.selected-group` so the
inset focus ring no longer hugs the channel/group name and so it
sits clear of the adjacent edit button's focus ring. Also drop
the underline on `.selected-stream:focus-visible`, since the
outline already indicates focus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:22:00 -05:00
Evy Kassirer
89e7dcd35b channel_settings: Fix clipped, off-color focus outlines in title row.
The keyboard focus outlines on the channel-name link and the
title-row action buttons (edit, preview, archive, subscribe) in
the channel and user-group settings overlay were being clipped at
the top and bottom, and rendered in the black/white
`--color-outline-button-focus` rather than the regular focus blue.

The clipping is caused by `overflow: hidden` on the .display-type
and .stream-info-title ancestors, which clip the outside-drawn
outlines on `.selected-stream`, `.icon-button`, and
`.action-button`. Inset the outlines with `outline-offset: -2px`
and recolor them to `--color-outline-focus` so they sit fully
within the focusable elements and match the focus styling used
elsewhere in the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:22:00 -05:00
Tim Abbott
e74eda0c39 bootstrap: Remove h1-h4 element resets from bootstrap.app.css.
Drop the universal h1, h2, h3, h4 selectors carried over from
Bootstrap 2.3.2. Earlier commits in this series made every scoped
heading rule that depended on these resets self-sufficient, so this
deletion is purely a no-op for visible behavior.

Removed properties were:

* margin: 10px 0 (now set explicitly on .overlay-messages-header h1,
  .settings-header h1, .settings-section h3,
  .drafts-list h2 / .message-edit-history-list h2,
  .empty-feed-notice-title, .poll-question-header /
  .todo-task-list-title-header, and
  .group-assigned-permissions .group-permissions-section > h3)
* line-height: 20px / var(--header-line-height) (now set explicitly
  on the same rules where it was load-bearing — overlay headers,
  user-profile modal h3s, folder-channels-list-header, etc.)
* font-size: 38.5px/31.5px/24.5px/17.5px (overridden in every place
  the app actually rendered these headings; only one bare instance
  per overlay needed an explicit replacement, captured above)
* font-weight: bold (matches the user-agent default for h1-h6 — no
  replacement needed; .light's 'font-weight: 300' override now
  competes only with the UA default, preserving its current effect)
* color: inherit, font-family: inherit, text-rendering:
  optimizelegibility (no-ops vs. UA defaults; no replacement needed)

The .alert h4 rules at the bottom of the file are part of the alert
component, not the heading reset, and remain in place.

The --header-line-height variable in app_variables.css is still used
by modal.css (height: calc(100% - var(--header-line-height))) and by
several of the explicit replacements above, so it stays.

Verified with a Puppeteer-based computed-style sweep: settings
overlay (Account, Notifications, Preferences, Org permissions, Org
profile), drafts and scheduled-messages overlay headers, and the
empty-feed notice all render identical font-size, line-height,
margin, font-weight, and bounding-box dimensions before and after
this series.
2026-05-26 13:00:58 +05:30
Sahil Batra
4b9033ee01 subscriptions: Set explicit margin on h4 subsection titles.
The <h4 class="stream_setting_subsection_title"> and
<h4 class="user_group_setting_subsection_title"> headings in
channel and group settings rely on the universal h1-h4 reset in
bootstrap.app.css for 'margin: 10px 0'. With display: inline-block,
vertical margins apply, and the bootstrap-supplied 10px is
visible spacing above/below these subsection titles.

Set 'margin: 10px 0' explicitly on the combined rule so both
classes keep their top and bottom margin once the reset is
removed. The 'margin-bottom: 5px' override for the stream
variant is preserved by moving it below the combined rule, so it
wins by source order.

No visual change.
2026-05-26 13:00:58 +05:30
Sahil Batra
9a40c091f0 left-sidebar: Set explicit line-height on .left-sidebar-title.
The <h4 class="left-sidebar-title"> elements in the left sidebar
(rendered from left_sidebar.hbs and stream_list_section_container.hbs)
rely on the universal h1-h4 reset in bootstrap.app.css for
'line-height: 20px'. The existing rule overrides margin, font-size,
and font-weight explicitly, but not line-height — so once the
reset is removed, the h4 would inherit its line-height from the
surrounding grid cell, which may not match.

Set 'line-height: 20px' explicitly so the title's vertical metrics
survive the bootstrap reset's removal.

No visual change.
2026-05-26 13:00:58 +05:30
Tim Abbott
468ff7fce1 subscriptions: Style bare h3 section titles in group permissions tab.
The "Permissions" tab of a user group's settings (rendered from
group_permission_settings.hbs) has three bare <h3> section titles —
"Organization permissions", "Channel permissions", "User group
permissions" — that sit directly inside .realm-group-permissions /
.channel-group-permissions / .user-group-permissions (all of which
also carry the shared .group-permissions-section class). They
currently rely on the universal h1-h4 reset in bootstrap.app.css for
font-size, line-height, and margin.

Add an explicit rule scoped to '.group-assigned-permissions
.group-permissions-section > h3' so these titles keep their current
appearance once the reset is removed. font-weight is left at the
user-agent default (bold) — same as bootstrap was supplying.

Note that the new font-size is expressed in em (1.5313em ≈ 24.5px
at the 16px base font) rather than the absolute 24.5px that the
bootstrap reset supplied. This means the section title now scales
with the user's chosen information-density font size rather than
staying fixed at 24.5px regardless. At the default 16px base font
the rendered size is identical; at non-default font densities it
shifts proportionally. A small deliberate improvement, not a
strict "no visual change" preservation.

The pre-existing .subsection-header h3 rule (line 458) covers the
per-subsection titles but did not set margin, relying on the
bootstrap reset's 'margin: 10px 0'. Since .subsection-header is
'display: flex', the h3 inside is a flex item with blockified
display, so those vertical margins were active — removing the
bootstrap reset would have collapsed the subsection header
vertically. Add 'margin: 10px 0' explicitly to that rule as well.
2026-05-26 13:00:58 +05:30