23754 Commits
Author SHA1 Message Date
Felipe Martin 2b3fb21fe3 Add KVListWithOptions for server-side prefix filtering in plugin KV store (#35441)
* Add KVListWithOptions to push prefix filtering into SQL

Add server-side prefix filtering for plugin KV store key listing via
KVListWithOptions across the full stack: store interface, SQL
implementation, platform service, app layer, plugin API, pluginapi
helper, and memory store.

Previously, WithPrefix in pluginapi.ListKeys performed client-side
filtering after fetching all keys. This pushes the LIKE filter into
the SQL query for efficiency, using sanitizeSearchTerm to escape
SQL wildcards.

* Add KVListWithOptions backward-compat fallback for old servers

Manually implement the KVListWithOptions RPC glue instead of generating
it so the client can detect "rpc: can't find method" errors from
pre-11.5 servers and return a proper AppError with StatusNotImplemented.
The generated version silently returns (nil, nil) on RPC errors, which
is indistinguishable from an empty result due to encoding/gob conflating
nil and empty slices. The pluginapi helper already checks for
StatusNotImplemented and falls back to KVList with client-side prefix
filtering.

* Address review findings on KVListWithOptions

- Escape backslashes in the SQL prefix instead of stripping them.
- Return an AppError for every RPC failure in the KVListWithOptions client.
- AND multiple WithPrefix options again; extra prefixes become checkers.
- Make the old-server fallback page over prefix-filtered keys and trigger
  only on StatusNotImplemented.
- Set the minimum server version to 12.2 and fix the WithChecker doc.
- Remove the unused PlatformService.KVListWithOptions.
- Route List and ListPluginKeys through their WithOptions variants.

* Tighten KV list tests from CodeRabbit nitpicks

- Assert exact keys per page in the prefix pagination store test.
- Drop unused pgHost/pgPort lookups in the KV integration test.
- Include compiler output in the plugin build failure message.

* Use snake_case mlog field key in ListPluginKeysWithOptions

* Move KVListWithOptions integration tests into regular test suites

- Cover the plugin-to-SQL path with a test_kv_list_with_options plugin run by TestBasicAPIPlugins.
- Cover the old-server RPC error mapping with a unit test against an RPC server that lacks the method.
- Remove server/integration/pluginapi and the testcontainers dependency it pulled into server/go.mod.

* Combine WithPrefix prefixes so all prefix filtering stays server-side

A second prefix ran as a checker after page and count were applied, so
pages came back short. Nested prefixes now collapse to the longest one,
and prefixes that cannot both match return no keys.

* Use a fixed batch size for the ListKeys prefix fallback scan

The scan read the caller's count keys per KVList call, so a small count
made many RPCs. It now reads 1000 keys per call, independent of count.

* Use an explicit ESCAPE '*' for the plugin KV prefix LIKE

Matches the other LIKE queries in the store and does not depend on the
default backslash escape of Postgres.

* Rename the KVListWithOptions RPC error ids to plugin.api.kv_list_with_options.*

Matches the naming of the other plugin API error ids.

* Add a benchmark for listing plugin keys with a prefix

Compares paging through KVList and filtering in Go with KVListWithOptions,
for 10k, 100k and 1M keys.

* Document KVListWithOptions and prefix listing for plugins

Expands the doc comments that generate the server plugin reference, and
adds a best-practices section on listing KV store keys with a prefix.
2026-11-01 09:37:48 +01:00
f619e1e3e0 Remove dead self-serve purchase code (#39528)
* Remove dead self-serve purchase code

Remove the unused ChangeSubscription, GetSelfHostedProducts and
InvalidateCaches methods from CloudInterface, the always-501
/api/v4/hosted_customer/signup_available endpoint, the unused
NotifySelfHostedSignupProgress app method, the hosted purchase screening
job, and Client4.GetSelfHostedProducts. Deprecate the related public
model types and constants.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Remove hosted_customer/signup_available from the API reference

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 18:40:48 -04:00
18fb0968a4 [MM-71476] Fix timeout when deleting the last reply in a thread on large Posts tables (#39579)
* [MM-71476] Compute thread reply stats in one query to avoid full index scan on reply delete

Deleting the last live reply in a thread could time out on large Posts
tables: the MAX-only LastReplyAt subquery let Postgres use a backward scan
of idx_posts_create_at, which walks the entire index when no live reply
remains. Selecting COUNT and MAX together prevents that rewrite, so the
query reads the thread through idx_posts_root_id_delete_at.

Also return the thread cleanup error before updating the root post's
UpdateAt, and return an error from that update instead of only logging it,
so the real cause is no longer masked by "driver: bad connection".

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Address PR feedback: 0 answered, 1 resolved, 0 declined

Add a Postgres EXPLAIN test for the thread reply stats query, rendered from a
shared threadReplyStatsQuery helper, with a MAX-only control case.

* Address PR feedback: 0 answered, 1 resolved, 0 declined

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
2026-10-30 21:51:48 +00:00
Nick MisasiandCursor Agent 3c0d58fcb6 Remove unused archived file UI (#39532)
Removes the archived file tooltip, preview and attachment states, the
archived flag on FileInfo, their strings, and the Cypress Cloud file
limits spec.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 17:42:05 -04:00
bd81219306 Remove Cloud file storage limit (#39526)
* Remove Cloud file storage limit

The Cloud product limits no longer define a file storage limit, so the
last accessible file job, filtering and header are inert. Add a one-time
migration that clears any leftover LastAccessibleFileTime marker.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Mock the last accessible file time migration key in the test store

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Delete leftover last accessible file jobs in the cleanup migration

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Retry the cleanup migration when deleting a leftover job fails

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Remove the last accessible file time cleanup migration

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 17:41:54 -04:00
f9f65b7604 MM-71482: Update login token handling (#39578)
* Add CWS login test cases

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Update token cleanup and CWS login handling

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Extend CWS login test cases

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-10-30 21:46:12 +02:00
Nick MisasiandCursor Agent 89f34ef2ad Remove unused billing history page (#39524)
Remove the admin console Billing History page, its invoice table and
modals, the cloud invoice preview, the invoice redux state and the
Client4 invoice methods, plus the unreachable invoice view in
BillingSummary.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 18:58:39 +00:00
Nick MisasiandCursor Agent 7605721bd7 Remove unused Cloud email templates (#39517)
Delete the Cloud arrears, payment failed, upgrade confirmation and renewal
notification email templates, which are no longer sent by the server, along
with the mjml partials that were only used by them.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 14:55:24 -04:00
Jesse Hallam 9aa8140f86 Fix openApiSync gaps for nested, helper, and multi-method routes (#39577)
* Fix openApiSync gaps for nested and multi-method routes

* Follow nested init helpers in openApiSync
2026-10-30 14:43:28 -04:00
Nick MisasiandCursor Agent b4fb9d16bd Remove Cloud Starter billing UI (#39529)
Removes the Cloud trial ended announcement bar, which only rendered for
the Cloud Starter plan, and the Starter-only cases on the billing plan
details, feature list and contact sales card.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 14:06:38 -04:00
4df6bbc764 Remove unused cloud invoice endpoints (#39523)
* Remove unused cloud invoice endpoints

Remove GET /api/v4/cloud/subscription/invoices and
/invoices/{invoice_id}/pdf, their CloudInterface methods and the
Client4.GetInvoicesForSubscription method. Mark the invoice model types
as deprecated since server/public is a published module.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Clarify deprecation notes on cloud invoice model types

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 12:20:38 -04:00
Nick MisasiandCursor Agent a29df1bb0f Remove cloud customer update endpoints (#39519)
Removes the unused PUT /api/v4/cloud/customer and PUT /api/v4/cloud/customer/address
routes, their CloudInterface methods, Client4 methods and API documentation.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 10:36:11 -04:00
414ca16c11 Remove company information edit page (#39518)
* Remove company information edit page

Removes the unused Company Information edit page from the System Console,
its route, the add/edit links on the read-only page, the related redux
actions and Client4 methods, orphaned strings and Cypress edit cases.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Leave after_subscription_spec.js to the stale billing specs removal

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Remove MM-T4165 company information edit Cypress case

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 10:35:42 -04:00
Scott Bishel a8f8a6b523 Documentation updates for some missed areas. (#39511) 2026-10-30 07:29:44 -04:00
e8d166d8fc Compact the websocket channel membership structures (#38993)
* Make WebConn.ShouldSendEvent allocation-free with an immutable session bundle

WebConn.ShouldSendEvent runs once per connection for every event the hub
delivers. With channel-scoped iteration off (the default) that is every
connection on the node for every channel-scoped event. Each call
allocated 336 bytes in two places unrelated to the decision itself:

- IsMFAAuthenticated built a fresh request.Context from the session on
  every call, and the clone escaped into the MFARequired interface call
  (320 bytes).
- The session's roles string was split with strings.Fields on every
  call, whether or not a permission check followed (16 bytes).

At 15k connections per node that is about 5 MB of garbage per
channel-scoped event.

Store the session as an immutable bundle behind the existing atomic
pointer: a deep copy of the session, its roles parsed once, and a
request context built once. SetSession builds the bundle in one store;
GetSession keeps its signature. The hot path reads the bundle and passes
the cached context to MFARequired. The cached context is shared
read-only by the hub and read-pump goroutines; comments on the bundle,
the session getter, and the roles accessor say so. The trade is about
320 bytes resident per connection.

No decision changes.

Measured end to end with BenchmarkHubChannelBroadcast (one posted event
through the production hubs, one per CPU, to 5000 connections with 200
memberships each; benchstat, n=10, interleaved), against master:

  iteration  members  time per event    p99 latency       allocs per event
  off        50       780us -> 717us    2.32ms -> 798us   10349 -> 348
  off        5000     1.11ms -> 1.05ms  3.20ms -> 1.19ms  10349 -> 348
  on         50       37.2us -> 34.2us  60us -> 53us      472 -> 372
  on         5000     1.12ms -> 1.05ms  2.70ms -> 1.20ms  10372 -> 372
  on, churn  50       unchanged         unchanged         894 -> 802

Bytes per event drop from 1.6 MB to 19 KB. With the work spread over one
hub per CPU the mean time gains 6 to 8%; the tail gains most, p99 down
56 to 66% where every connection is checked, because the collector no
longer has 1.6 MB of garbage per event to chase. The allocations left
are the per-hub event preparation (29 per hub). The bundle adds about
400 bytes per connection (live heap +1.4 to 1.5%, 1.9 MiB at 5000
connections).
The churn case is bound by membership reloads on the hub goroutine,
which this change does not touch.

Per call (BenchmarkShouldSendEvent, 200 memberships, mock suite; the
real App suite also measures zero allocations on the channel and
user-scoped paths): 102 to 149 ns and 2 allocations before, 37 to 83 ns
and none after. Events flagged as sanitized or sensitive still allocate
inside RolesGrantPermission, which is out of scope here.

Tests: TestWebConnShouldSendEventDecisions pins 23 decision cases with a
denying mock suite; TestWebConnShouldSendEventAllocs asserts zero
allocations for six cases and is stable under the race detector;
reintroducing either removed allocation fails all six.

Release Note:
NONE

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Use an admin session for the sensitive-data allocation case

The platform test helper's suite grants every permission, so the case passed
with a plain user session, but a reader has to know that to see why. Give the
session the admin role so the case reads as what it is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Hold small channel sets in the hub index as slices

With EnableWebHubChannelIteration on, each hub keeps, per channel, the
set of its connections in that channel as a Go map. Most channels on a
hub have only a few connections there: every DM and small group is its
own channel. A map costs about 165 bytes even when it holds one
connection, so with many small channels the index costs far more than
the connections it holds.

The set is now a slice while a channel has at most 64 connections on the
hub, and a map beyond that. Adding appends; removing scans the slice and
moves the last element into the gap; iterating walks the slice from the
end, so the hub can still remove the connection it is visiting when its
send queue is full. A set that empties is deleted.

BenchmarkHubChannelBroadcast with the setting on, 5000 connections in
200 channels each, where h is how many connections on the hub share each
channel on average (set with the benchmark's sharing knob); live heap
of the index on top of the setting-off baseline, n=6:

  h     before     after
  2     172 MB     50 MB
  3     165 MB     50 MB
  5     152 MB     50 MB
  10    112 MB     22 MB
  50     10 MB    -14 MB

Live objects drop about 30% and a full collection about 15 to 30%.
Delivery time is unchanged within noise.

Tests: TestConnSet covers duplicate adds, the switch to a map past 64
connections, and removing members while iterating.

Release Note:
NONE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Key the channel membership structures by the decoded channel ID

A channel ID is 16 random bytes that model.NewId writes as 26 base32
characters, and the server keeps it as that string everywhere. The
websocket membership structures hold one copy per membership: the
per-connection cache with the setting off, and the hub index's channel
lists and sets with it on. Each copy is a 16-byte string header plus
its own 32-byte allocation, and the collector marks every one.

model.DecodeId turns an ID back into its 16 bytes and rejects any
string NewId could not have produced; the decoder alone would accept
four strings per ID, since it ignores the last character's two unused
bits. The per-connection cache and the index now key by those bytes,
stored inline with no pointers, and keep any ID that does not decode in
a string-keyed side map, which stays empty in practice. The hub decodes
each event's channel once per broadcast and passes the key to
ShouldSendEvent, which now takes it as a parameter; its only callers in
this repository are the hub and tests.

BenchmarkHubChannelBroadcast, 5000 connections in 200 channels each,
live heap once memberships are loaded, where h is how many connections
on the hub share each channel on average, n=6:

  h    setting off: before -> after    setting on: before -> after
  2    136 MB -> 65 MB                 186 MB -> 153 MB
  5    143 MB -> 72 MB                 193 MB -> 160 MB
  10   145 MB -> 74 MB                 167 MB -> 134 MB
  50   146 MB -> 75 MB                 132 MB -> 98 MB

Live objects with the setting off drop from about 2.1 million to under
100 thousand: each connection's cache is now one pointer-free map.
Delivery time is unchanged within noise.

Tests: TestDecodeId (the unused-bits string the round-trip check
rejects), TestChannelSet (keys, undecodable IDs, the zero key) and
TestHubConnIndexChannelsThatDoNotDecode.

Release Note:
NONE

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Let the hub broadcast benchmark vary how widely channels are shared

The benchmark draws each user's 200 channels from a pool of 20000, so
each channel is held by about 50 of the 5000 connections. Memory
results depend on that number: DMs and small groups are held by one or
two connections on a node, large channels by hundreds.

MM_BENCH_CHANNEL_SHARING=h sizes the pool so each channel is held by
about h connections. Unset, the pool stays at 20000 and the results are
unchanged.

Release Note:
NONE

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Walk small channel sets with a plain loop

The linter asked for slices.Backward here. That is a second closure on
every channel event, on top of the iterator connSet.all returns, and
BenchmarkHubChannelBroadcast counts it: 408 allocations per event
instead of 372 with the plain loop, about 1.2 KB more per event. Back
to the plain loop, with the reason beside the nolint directive.

Release Note:
NONE

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Move the cached request context lookup into a helper

getRCtx returns the request context built in SetSession, or an empty one when
there is no session, so IsMFAAuthenticated reads like getSessionRoles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* PR feedback: Build the connections inside each TestConnSet subtest

Each subtest now makes its own connections, so the subtests are independent
and could run in parallel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* PR feedback: Return an explicit zero value from every DecodeId failure

The length check returned the named result, which is zero there, while the two
later returns spell out a zero value because Decode may have written part of b
before failing. Make the first one explicit too and say why.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* PR feedback: Remove from a small connection set with slices.Delete

The slice is walked from the end, so shifting down the elements after the
removed one only moves connections that were already visited.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* PR feedback: Drop the side maps for channel IDs that do not decode

Every channel ID the server makes decodes: NewId has produced them since the
first release, and the store refuses a caller-supplied ID unless the channel
is shared, so only a shared-channel invite from a plugin remote could store
one that does not. Such an ID is now skipped, with a warning, when a
connection's membership loads, and the hub drops a broadcast whose channel ID
does not decode, since no connection can be a member of it. Before, those IDs
were kept in string-keyed side maps next to the decoded keys.

channelSet and channelList are plain decoded keys, ForChannel and has take
the key alone, and the decodeChannelID helper lives only in the tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-10-30 10:40:55 +01:00
Ben SchumacherandClaude Opus 5.5 4bf16a6a10 MM-71424: Make Elasticsearch/OpenSearch TestConfig side-effect-free (#39512)
* MM-71424: Make Elasticsearch/OpenSearch TestConfig side-effect-free

TestConfig marked a stopped engine ready and swapped in a new client,
skipping Start(): no index templates, no bulk processors, no healthy
flag. The watcher then never called Start(), and SyncBulkIndexChannels
could hit a nil sync bulk processor in an unrecovered goroutine. It
also overwrote the running engine's version and plugins, without the
mutex, from a possibly unsaved candidate config.

- TestConfig now only builds a client and checks the version.
- App.TestElasticsearch wakes the search engine watcher after a
  successful test on an inactive engine, so it runs a full Start()
  right away instead of waiting out its backoff.
- SyncBulkIndexChannels returns an error instead of panicking when the
  sync bulk processor is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpCfx3XMX5sfS3hGN7Ln1M

* MM-71424: Only wake the watcher when the tested connection is the saved one

The watcher starts the engine with the saved config, and each wake
resets its backoff. Testing an unsaved candidate config (for example a
different URL) must not make it retry the saved cluster immediately.

Also fold the duplicated watcher wake subtest into a loop over both
wake functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpCfx3XMX5sfS3hGN7Ln1M

* MM-71424: Simplify the TestConfig follow-up

- Move the "wake the watcher after a successful test" decision into
  PlatformService.SearchEngineConnectionTested, next to the watcher,
  instead of exposing a raw reevaluate signal to the app layer.
- Close the probe client in TestConfig. It is now always discarded, and
  with sniffing on it would leave a discovery goroutine running.
- Drop the syncBulkProcessor nil guards: with TestConfig no longer
  setting ready, ready implies both processors exist.
- Restore the original watcher wake subtest and test the new platform
  method directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpCfx3XMX5sfS3hGN7Ln1M

* MM-71424: Use the caller's context for the TestConfig version probe

Lets callers that carry a deadline or cancellation stop the version
request. The probe client is still closed with a background context so
cleanup runs even when the caller's context is already done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpCfx3XMX5sfS3hGN7Ln1M

* MM-71424: Add watcher lifecycle tests for both search engines

Adapted from #37931 as requested in review:
- TestTestConfigThenSavingConfigStartsEngine: TestConfig leaves a
  stopped engine alone; saving the tested config wakes the watcher,
  which runs the real Start() and creates the sync bulk processor.
- TestTestElasticsearchStartsEngineWithoutWaitingForBackoff: the saved
  cluster comes back while the watcher waits out its backoff, and a
  successful App.TestElasticsearch starts the engine right away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpCfx3XMX5sfS3hGN7Ln1M

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-30 07:58:29 +01:00
yasser khan cdf79fef30 ci: update the test-system-io actions to 18a0baa (#39564) 2026-10-30 03:02:43 +00:00
Matthew Birtch f7f54c0968 Use compass-ui Icon in Button icon slots and fix follow-ups from the Button migration (#39559) 2026-10-29 20:37:41 -04:00
Max BartenevandMattermost Build caaec8cda5 Add a button to quote text selected in a message (#39237)
* Add a button to quote text selected in a message

Selecting text in a post in the center channel, RHS or Threads view shows a
floating "Quote selection" button at the end of the selection. Clicking it
appends the selection as a Markdown blockquote to the message box of the same
channel or thread and leaves the caret on an empty line below it, in both the
plain text and the WYSIWYG editors.

Fixes https://github.com/mattermost/mattermost/issues/39236

* Address review feedback on quoting selected text

- Determine the selection direction from its boundary points so a left to
  right selection that ends on an element containing its start isn't treated
  as right to left.
- Keep a quote queued while the message box is busy rewriting the draft and
  add it once it's done, but still drop it when the message box can't be used.

* Add E2E tests for quoting selected text

Cover quoting a selection into the channel message box and posting it with a
reply, a right to left selection added after an existing draft, quoting from
the RHS into the thread reply box, and typing the reply below the quote in
the WYSIWYG editor. Text is selected by dragging the mouse like a user does.

* Address UX review feedback on quoting selected text

- Keep the Markdown formatting of the quoted text (headings, lists, emphasis,
  code blocks, links, mentions, hashtags) by converting the selected HTML back
  to Markdown, in both the plain text and WYSIWYG editors.
- Place the quote button next to the first selected character instead of the
  caret, which browsers report at the end of the previous line when a
  selection starts at the beginning of a wrapped line.
- Don't offer to quote when the target message box can't be used: archived
  channels, DMs with deactivated users, threads whose root was deleted, or
  without permission to post.
- Make the button reachable with Tab and dismissable with Escape, returning
  the focus where it was, and show it for selections made without the mouse.
- Mix the hover color with the background instead of using transparency.

* Update the button visibility ref in a layout effect

Writing a ref during render isn't allowed, since React can discard a render without rolling the write back.

* Use the shared Turndown converter for quotes

Convert quoted selections with utils/turndown.ts, which already handles the
markup of rendered messages for pasting, instead of a separate converter, so
quoting and copy-pasting a message produce the same Markdown.

- Add a rule for user mentions to the shared converter so that they keep the
  username rather than the display name, for pasting too.
- Drop the data-language attribute on code blocks. When only part of a code
  block is selected, its language label is copied along with the selection
  instead.

* Address dev review feedback on quoting selected text

- Don't offer to quote burn-on-read posts, which can't be copied either, so
  that they aren't kept around after they're deleted.
- Leave the user's selection as is instead of narrowing it to the message
  text, so a selection running into the author name, attachments or reactions
  can still be copied as a whole. Only the message text is quoted.
- Support quoting in channel and thread pop-out windows.

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-10-29 20:10:17 -04:00
Nick MisasiandCursor Agent 926a4c8a83 Remove Cloud team limit checks on team create and restore (#39525)
The Cloud product limits no longer define a teams limit, so these checks
never block anything; they only fail the request when fetching limits fails.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 16:35:31 -04:00
9a78a71c28 Remove Cypress specs for removed billing flows (#39516)
* Remove Cypress specs for removed billing flows

Delete specs for the pricing modal, purchase and payment flows, downgrade
feedback, invoice and card details, self-hosted purchase and workspace
deletion, and drop the purchase modal cases from the subscriptions spec.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Remove Cypress billing helpers that are no longer used

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

* Remove MM-T4122 from subscriptions spec

It exercises the removed purchase modal.

Co-authored-by: Nick Misasi <nick13misasi@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 16:31:35 -04:00
Asaad MahmoodandClaude Opus 5.5 68b173ab79 Keep the team sidebar and current team in global products with the product rail (#39552)
* Keep the team sidebar and a selected team in global products

With the rail on screen, products that hide the team sidebar (Agents)
keep it, and loading one of their pages restores the user's last team, so
the rail and its team menu don't disappear on a refresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restore a team only where the rail shows, and survive blocked storage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Leave the product rail out of backstage pages

Integrations and Custom Emoji take the whole page with their own way
back, so the rail stays off there, and the header and team sidebar
behave as they do without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remember a team picked inside a global product

Switching teams on a global product's page selects the team without a
team URL, so the team controller never recorded it and a refresh
restored the earlier one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Record the last team only where the product rail shows

With the flag off or on mobile, switching teams in a global product
leaves user_prev_team as before. The blocked-storage test now has a
current user, so it reaches the storage write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Avoid an import cycle when recording the last team

team_actions importing the product rail selectors closed a cycle
through utils/utils, which broke channel_header_title.test.tsx. Check
the flag and mobile view directly instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test when the team sidebar shows alongside the product rail

Cover global products with and without the rail, the hidden sidebar,
the hide button, and backstage pages bringing the sidebar back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 15:57:50 -04:00
Asaad MahmoodandClaude Opus 5.5 e93c098703 Plugin API additions for Boards: Create new board modal, Channel Settings tab, tooltips (#39457)
* Plugin API additions for Boards: Create new board modal, Channel Settings tab, tooltips

Ports mattermost/mattermost#39108 from the Boards core branch
(cursor/boards-plugin-core-changes-eade) to master. It applied cleanly.

* Let plugins open the new channel modal for their own channel types

- registerSidebarBrowseOrAddChannelMenuAction takes an optional placement;
  'afterCreateChannel' lists the item right below "Create new channel".
- registerChannelTypeOption takes showInNewChannelModal; false keeps the
  option out of "Create a new channel".
- new_channel_modal is a published modal. With channelTypePluginId it offers
  only that plugin's channel types, without Public and Private Channel, with
  the first selected, and takes wording for that kind of channel (title, name
  label and placeholder, purpose label and help text, category help text).

Boards uses these for "Create new board".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let plugin sidebars add their own header buttons

registerRightHandSidebarComponent takes headerControls, rendered first among
the sidebar header's buttons, before expand and close, as the thread header
shows Follow. Boards uses it for Follow on a card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Expose WithTooltip to plugins on window.Components

So plugin UI can show core's tooltips (Boards uses it for the attribute lock
and the card's header buttons).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Revert "Let plugin sidebars add their own header buttons"

This reverts commit 1cc38cf296. Plugins can already put buttons at
the end of their sidebar's title, which fills the header up to expand
and close, so the Boards plugin does that instead and the extra API
is not needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let plugins open Channel Settings on a tab, including their own

Publishes the channel settings modal to plugins as channel_settings
(pinned open, like team_settings), and gives it an initialTab: a
built-in tab or the id registerChannelSettingsTab returned for a
plugin's tab. A tab that isn't shown for the channel falls back to the
first one, as a selection that becomes unavailable does. The Boards
plugin opens a board's Attributes tab this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Address review: publish WithTooltip properly, load the channel for channel_settings, reset the Create gate

- WithTooltip moves from window.Components, which is frozen legacy, to a
  new published namespace, window.WebappUtils.components. It has a contract
  (PublishedComponents) and a build-time drift check, like modals and editor.
- The published channel_settings modal loads a channel the client hasn't
  loaded before rendering it, and closes if the channel can't be loaded.
  Before, it threw reading the missing channel.
- Opened for a plugin, the new channel modal resets the plugin's Create gate
  when the selected option goes away and it selects the first one, as a
  type change does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:19:27 +00:00
Nick MisasiandCursor Agent 51e826eae1 Remove AdjustInProductLimits and the Cloud usage API (#39527)
The Cloud product limits no longer define a teams limit, so
AdjustInProductLimits never archives or restores teams. The
/api/v4/usage endpoints only served the removed Cloud limits UI.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 13:24:46 -04:00
Nick MisasiandCursor Agent 718b00b337 Remove unused Cloud usage limits UI (#39530)
Removes the Cloud usage modal, file limit sticky banner, product switcher
limits footer, admin limit-reached effects, the usage-delta hooks and HOC,
the teams-limit gating that read usage deltas, the limits branch of the
cloud_subscription_changed websocket handler, and their strings.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 13:24:21 -04:00
c0b8b4348d drip: use errors.AsType for sidebar category store errors (#39553)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jesse <jesse@mattermost.com>
2026-10-29 14:06:47 -03:00
57002e487c drip: AppError -> error: login attempts (#39539)
Migrate checkUserLoginAttempts to return error so a nil result cannot become a non-nil error interface at the caller.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jesse <jesse@mattermost.com>
2026-10-29 14:03:13 -03:00
Nick MisasiandCursor Agent 19cd4917f6 Remove unused hosted customer state (#39520)
Drop the hostedCustomer reducer, action types, state type, unused cloud
action types, the hosted customer signup websocket event and the unused
Client4 hosted customer route.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 12:39:31 -04:00
Nick MisasiandCursor Agent 540a5d7b5c Add post priority and acknowledgements to bulk import and export (#39551)
* Add post priority and acknowledgements to bulk import data and validation

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Import post priority and acknowledgements

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Export post priority and acknowledgements

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 12:11:43 -04:00
Nick MisasiandCursor Agent 49a923f5c2 Remove orphaned billing and purchase components (#39522)
Delete Cloud billing/purchase components, hooks, selectors, utils, modal
identifiers, assets and i18n strings that are no longer referenced.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 15:26:58 +00:00
Nick MisasiandCursor Agent 1cf0caf2d9 Remove unused payment announcement bar (#39521)
Drop the Cloud payment-failed / card-expired banner, which links to a
console page that no longer exists, along with its helpers, strings and
the unused announcement bar placeholders.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-29 10:44:15 -04:00
3525e8faa8 [MM-70033] Replace property permission mechanisms with a typed permissions model (#39081)
* Replace property permission mechanisms with a typed permissions model

Convert every PSAv2 property field to a permissions object at startup and
on every create, and remove the legacy fallback, so the permissions
object is the only rule. The property hook covers every PSAv2 and v3
group, the access control group moves to version 3, the sync lock is
restored as a grant, and the legacy permission columns are still written
so the change can be rolled back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Require a system admin to submit permissions on an access_control create

The access_control group's hook pins field.write and option.write to
sysadmin on every create, but a submitted permissions object is stored as
sent and decides alone, so it slipped past that pin. Only a system
administrator may now submit one there; anyone else gets a 403, and can
still create the field without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Convert fields missing a permissions object on every startup

The backfill ran once, behind a System key. A release that predates the
permissions object writes fields without one, from an old node during a
rolling upgrade or while rolled back, and the hook refuses such a field to
everyone, so those fields could never be read, written or deleted again.
The backfill now runs on every startup. It only writes fields still missing
an object, so a startup with nothing to convert writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Report a linked field masked when its template can't be read

effectiveAccessModeUsing fell back to a linked field's own access mode when
the template read failed. A linked field never carries masking of its own, so
that mode is usually public, and the callers that strip attribute values or
shield fields from a channel admin then let the field through. A failed or
missing template read now reports shared_only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cover plugin access to builtin groups outside access_control

The conversion now gives fields outside access_control a wildcard plugin
grant, so a plugin keeps the access it had before the hook covered every v2
group. Add a test that a plugin can still read a boards field's options and
write a value, and update the tests and the comment that expected no grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fit the lower PRs' newer tests to the cutover

Two tests added below this PR after it was cut ran here for the first time.
The masking fixtures updated with the rows a create or update returned,
which still hold the legacy columns as submitted, so the cutover read the
update as a column change and kept the stored permissions; they now re-read
the stored rows. A member's create without a permissions object now gets
one converted from the default levels rather than none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Skip and log a field the permissions backfill can't convert

Any error converting a field stopped the backfill, and with it the server,
since app migrations are fatal. A template switched to shared_only over
member-writable linked fields, a soft-deleted or missing template, or a
concurrent write could each leave a deployment unable to start. A field that
fails is now logged by ID and left without a permissions object, which the
hook already treats as refusing everyone, and the run carries on. A group
batch that fails is retried one field at a time, so one bad field doesn't
hold back the others. The backfill runs on every startup, so a skipped field
is retried on the next one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Return a field's legacy columns as stored from create and update

The store writes the Protected and Permission* columns from the permissions
object but returned the field with the columns as submitted. The hook
defaults those columns before the save, so a caller's own stricter object
could come back next to a looser column: a CPA field created with value.write
none returned PermissionValues member. A plugin that renamed that copy sent
the stale column back, which an update reads as a deliberate legacy edit, and
value.write became member. Create and Update now return the columns as stored,
the same view a read gives, so sending a returned field back changes only what
the caller changed. The masking fixtures no longer need to re-read the rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Expect the stored legacy column from create and update in two tests

Two tests read PermissionValues off the field a create or update returned
and expected the column as submitted. Those now return it as stored: an
unset column on an unmanaged group's field is stored as value.write none,
and an owner-managed field's value.write is none, left to the owner's grant,
whatever the hook pinned the column to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Correct two doc comments in the permissions backfill tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Refuse an update that changes both permissions and a legacy permission key

Translating a legacy column or attr change rebuilds the permissions
object from the stored one, so a changed object submitted in the same
update was silently dropped, keeping any grant or wider tier it had
just removed. Neither shape says which of the two should win, so the
update is now a 400. An echoed object, or none, translates as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cover an unowned field's v2 read having no owners attr

Clients lock any field whose attrs.owners is non-empty, so a field an admin
creates must reach users without one, through both the CPA and the generic
endpoints, and users must still be able to set their own value. Also drops
the plugin wildcard from two comments that described it as projected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Write the permissions column in the masking e2e DB helpers

The server now decides a field's access from its permissions column alone,
so setting access_mode and protected in attrs no longer masked anything and
every spec that relied on it saw plain values. The helpers now write the
permissions object the conversion would produce for each mode, and keep the
grants index in step, alongside the legacy keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fit two owner tests to the unprojected plugin wildcard

Removing a field's last owner now reads as no owners, with the plugin
wildcard kept as a stored grant. The max-owners echo test relied on the
wildcard as the synthetic owner past the limit, so it uses a sync lock's
grant instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Follow the model's ApplySyncLock and attr-protected projection

ApplySyncLock now takes the field it locks rather than its sync source,
since the grant it gives the sync depends on the field's owners,
protection and link. And ProjectLegacyPermissions now reports an
attr-protected access_control field with the Protected column unset and
the protection in the attr, as master stores it. The link step read only
the column, so a linked field of such a template would no longer copy
its protection and access mode for an older release to read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Leave the template's sync grant off its linked fields

A synced template now carries a grant for its sync over its options,
which the sync provisions there. A linked field copied every template
grant but option.read, so a channel field linked to a synced template
took a sync grant over option.write. The conversion of the same field
from its own attrs gives it none, and the v2 projection reported it as
an owner, which locks the field to people in the webapp.

The link step now leaves the template's sync grant out. A linked user
field still gets the sync's grant from the sync lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fit the bulk value delete tests to the field and value write check

Deleting all of a field's values now needs field.write and value.write,
and fails closed on a field with no permissions object. Five tests
assumed the old check: a human caller always passed, and a plugin
needed value.write alone. They now delete as a caller holding both, or
expect the refusal, and create the linked fixture directly so its
template's grants don't gate the link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fit tests to the conversion's owner, source plugin and sync grants

The model now converts a user or role owner to no grant, gives the
source plugin a grant only on a protected field (and only its reads
once the field has owners), leaves derived grants out of the projected
owners, and gives the sync the reads and option writes master gives
it. Tests that assumed the old grants now check the new ones:

- the sync's grant on a linked user field holds every action but
  option.read;
- a user owner added through a v2 update stays in the v2 view without
  a grant;
- a field with the most owners allowed projects no synthetic owner, so
  an unchanged echo needs no allowance for one;
- the linked-field backfill test uses a plugin owner, which still
  converts to a grant;
- an unprotected field a plugin creates carries only the plugin
  wildcard;
- adding an owner to an owned, protected field is done by its listed
  owner, and the source plugin keeps its reads;
- the v2 view test reads the legacy access_mode attr, since
  GetAccessMode now reports the mode from the permissions object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fit master's newer sync and bot attribute tests to the cutover

Three tests master added since the last rebase assumed access was
decided from attrs and the legacy hook:

- the linked sync attrs test seeded its template with no permissions
  object, which the link step now refuses; it seeds a converted one, as
  the tests around it do;
- the syncer's broadcast test restricted its field through the
  access_mode attr alone; it restricts the permissions object too;
- the bot attribute lock test expected the hook's protected and sync
  lock errors, but the API's value.write check now refuses first, as for
  every other locked field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Convert a linked field's own grants on create

A linked field created without a permissions object took its template's
grants wholesale and never converted its own settings. A field with
owners its template lacked was then writable by any plugin, and showed
no owners in the v2 view, where master lets only those owners and the
sync write it. A field without owners on a template that had them could
be written only by the template's owner, where master lets any plugin
write it.

The restrictions still come from the template, as master copied the
template's permission levels. The grants are now converted from the
field's own attrs, as the startup backfill converts a linked field, so
create and backfill give the same grants for the same row. The
conversion already gives a synced user field the sync's grant and takes
value.write off every other grant, so the sync lock applied afterwards
is dropped: on an owned synced field it also took value.write from the
owners, which master and the backfill let write.

A template's explicit grants, such as a role grant authored through v3,
no longer reach a linked field created without a permissions object.
Grants are per field; a caller that wants them sends its own object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let the sync follow a late sync link on a linked user field's values

A user field linked to its template before the template got its sync
link carries no copy of the link, so it has neither the sync lock nor
the sync's grant, and the hook refused the sync its values. Master
leaves such a field open to every machine caller, the sync included,
and its syncer follows the template's link for it.

The sync is now let at such a field's values, reads and writes, when
the template is synced from its own source and the field's catch-all
plugin grant allows the action, which is where master lets any machine
caller in. An owned or protected field has no such grant for a value
write, and only a user field takes a sync link, so the sync is still
refused wherever master refuses it, and no other writer is locked out.
The reads are needed too: the syncer compares what it reads with what
it last wrote, and a hidden value never gets cleared.

isDefinitionSync moves to the property service so both hooks share the
template lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tell a sync its template was unlinked before judging its grants

When an admin unlinks a template from the sync while a sync run is in
progress, the sync's next option change on that template has to fail
with the 409 sync_source_unlinked, which tells the syncer to keep the
user's stored value and stop syncing the field. Unlinking also takes
the sync's option grant off the template, and the access control hook
runs before the validation hook that raises the 409, so the sync got a
403 instead. The syncer treats that as an ordinary failure and clears
the stored value.

The access control hook now gives a sync changing a field's options the
validation hook's answers first, in the same order: its own field goes
on to the grants, a field synced from another source gets the 403, and
a field synced from nothing gets the 409. Only the options endpoints
answer this way, as on master, which never refuses a value write or a
field update with the 409.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Narrow a linked field's restrictions by its own locks on create

A linked field created without a permissions object took its
restrictions wholesale from its template. A plugin that created a
protected linked field on an unprotected template left sysadmins able
to edit and delete it, and one that also set source_only left its
values readable by anyone, where master lets only the source plugin do
either.

The template's restrictions are now narrowed by the field's own
protection and access mode, never widened: a protected field takes the
converted field.write (and in access_control option.write), and a
non-public access mode takes the converted reads where they are
narrower. Create and backfill now give the same restrictions for these
rows. A shared_only mode the field sets for itself still can't be
expressed, since a linked field carries no masking of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read a late-linked field's template once per hook call

Letting the sync at a late-linked user field's values reads the
field's template from the master database to see whether it is synced
from the caller's source, and the read and write paths asked once per
value. A sync run reads every user's values on each field it maps, so
that was one master read per user per field.

The answer is now kept in the call's masking context, per template and
sync source, so one call reads each template once however many values
it judges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Allow unlinking a field whose template does not exist

Unlinking reads the stored field's template to work out the legacy view
a caller was shown, both in the access control hook and when
reconverting legacy permission keys. When the template row is gone
entirely, those reads failed with its not-found error, which the API
reports as a 500, so the holdings guard's allowance for a missing
template was never reached. The read path already projects such a
field with no template, so these reads now do the same for a template
that does not exist. Any other read error still fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Describe the legacy permission update error strings for translators

Every en.json entry carries a description for translators, and the two
errors refusing a legacy-shaped update that would lose permission
settings had none. They now say when each is shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Convert system-owned fields on every page of a group

ConvertSystemOwnedFields read a single page of the group's fields, so
in a group with more fields than one page holds, a system-owned field
past the first page was left with no permissions object and without
its subsystem's grant. It now pages through the whole group with a
cursor, the way the permissions backfill does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Name the Local-session denial cases after their unconverted fixtures

These cases deny a Local session only because their field is left with
no permissions object, a state the backfill does not leave behind. Once
a field is converted, a Local session may edit it, protected or not.
The names now say the field is unconverted, so they no longer read as
saying a Local session is refused on such a field in general.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fit two tests to the field.write floor under the cutover

The access_control create test submitted a policy at field.write none
as a system admin and expected it stored as sent. The floor now refuses
that through the API, so the test checks the refusal and then submits
the same policy at field.write sysadmin.

The lockout test that keeps a partial restrictions object's meaning
for callers outside the HTTP API used an uninstalled plugin, which the
cutover's enforcement hook refuses. The LDAP sync, granted field.write,
stands in for it, since it too reaches the app layer without coming
through the handlers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Judge a permissions patch's lockout checks by its effect

The patch handler gates a permissions object on what it changes, so an
unchanged echo riding along on an options patch is judged on
option.write alone. The floor and the anti-lockout check were keyed on
the key being present, so that same echo was refused: with a 403 when
the caller holds option.write but not field.write, and with a 400 when
the field is one a plugin keeps at field.write none. Both now run only
when the permissions actually change, which takes nothing away from
either check, since an unchanged object can lock out nobody new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read null grants and the served [] as the same when comparing permissions

A field created without grants stores them as null, and the API serves
them as [], so every caller sees the same shape. A client that sends
the served permissions back alongside an options change then looked,
to both the patch handler's echo check and the enforcement hook's
options-only comparison, as if it had changed the grants. A caller who
holds option.write but not field.write got a 403 on such a field,
which is common: linked fields and fields created without grants.

Both layers now compare with one helper, model.PermissionsEqual, which
is reflect.DeepEqual except that a nil and an empty list are the same
list in grants, a grant's scopes and allow, and masking's except. Those
are the lists the wire cannot keep apart, and they mean the same thing
wherever a policy is read. Comparing this way rather than normalizing
the stored object at one point also covers fields already stored with
null grants. Any other difference still counts, masking absent against
masking {} included, so a real grant change is still held to
field.write.

The handler's resolution of the permissions patch, including the drop
of a linked field's served masking: {}, moves into a small helper so
the test that checks the handler and the hook agree runs the same code,
and gains cases for a null grants echo, an added grant and a linked
field's served permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bump the managed category group before converting its fields

An install that still carries the legacy "true" marker has its managed
category group at version 1, and its field has no permissions object. The
setup registered the group and converted the field before bumping the
version, so the converted field failed the group version check and server
startup aborted, and the service kept the version 1 group cached.

Bump the version and save the marker first, as before, then register the
group so the cache holds the bumped row, then convert. A conversion that
fails after the marker is saved is retried on the next boot without a
second bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Leave fields in unversioned property groups out of the permissions backfill

A group that existed on 11.7 is still at version 1 after the upgrade, but a
plugin could have put fields with an ObjectType in it. The backfill converted
those, the write failed the group version check, and every startup logged them
as unconverted fields that stay inaccessible. They don't: the access control
hook and the API only enforce version 2 and 3 groups, so these fields behave as
they did before. Skip them instead.

ConvertSystemOwnedFields now skips a version 1 group too. Without that, a
managed category group left at version 1 with no setup flag, by a server that
stopped between creating the field and saving the flag, failed startup; now
that boot saves the flag and the next one bumps the group and converts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reconvert a field's permissions when its OpenID Connect sync link changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cover the OpenID Connect sync's service grant on value writes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Name every sync source in a comment and an error description

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test that local mode can't write sync- or owner-locked values

Check local mode against LDAP- and SAML-synced and owner-managed CPA
fields through the production ladder, where the conversion gives them a
value.write tier of none, and restore the empty and local-mode callers
to the OpenID sync case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Describe local mode's value-write rule by the field's locks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Report a linked field masked in the v2 view when its template can't be read

A linked field never carries masking of its own, so falling back to its
own access mode told a v2 reader a masked linked field was public. Report
shared_only instead, as the v3 masking presence and effectiveAccessModeUsing
already do; a field whose own tiers say source_only keeps that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tag the plugin as caller when a plugin creates a channel

PluginAPI.CreateChannel passed the plugin API's untagged context, so the
access control hook refused the managed category write and the channel came
back without its category. Add a test helper that builds the plugin API on
the context a real plugin gets, and use it in the managed category test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tag local-mode sessions as the local admin on post and field reads

Post property value hydration and the access control field autocomplete
tagged the session's UserId, which is empty for a local-mode session, so
local mode got posts without property values and fields without options.
Tag such a session as the local admin, as the other property routes do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Warn when property access control refuses a call with no caller ID

The access control hook logs a warning, once per hook call, naming the
operation and the group and field IDs, when a call on an enforced group
carries no caller ID and has a write refused or values hidden. A field
read that only hides options is not logged. App tests install an
observer on the hook that sees every such call, field reads included,
whatever logger the context carries, and fail unless they opt out with
AllowUntaggedPropertyCaller, so an internal call that forgets to tag its
caller shows up in a test instead of failing silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tag the caller on channel creation, policy masking and redaction reads

Channel creation's attribute field lookup, the policy simulation's CPA
redaction, the field autocomplete's linked-template reads, and the policy
masking calls ran untagged or with a local-mode session's empty UserId,
so the access control hook hid options from them. Tag the session user,
or the local admin for local mode. The plugin API's policy read, policy
save and access evaluation passed the plugin API's untagged context; tag
the plugin. Expose the hook's no-caller observer on Server so api4 tests
can check the calls they make are tagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Report a refused linked-field create with no caller ID

A linked field create that its template refuses returns an AppError
rather than ErrAccessDenied, so the hook's deferred report never saw it,
and a caller that named nobody was refused without a warning or a call
to the test observer. Report it where the refusal is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Withhold broadcast values that not everyone can read

Websocket broadcasts carried a property value, an option list or an
attribute whenever the field's access mode said public, which a read tier
between everyone and none still reports, and which a linked field reports
for itself whatever its template's masking. Add
model.PropertyFieldReadableByEveryone: a permissions object is present,
nothing masks the field (the template's masking, for a linked field), and
the read tier is everyone. Value broadcasts, option stripping on field
broadcasts, the channel admin redaction set and the members sidebar
attribute filter all decide on it. The redaction set and the sidebar
filter show option names for a field with options, so for those fields
they require both value.read and option.read to be readable by everyone.

Each path now gates only on the tier of what it exposes: value.read for
values, option.read for options, and both for option-type fields on the
redaction set and the sidebar filter. A none tier on the other action no
longer withholds, which matches the hook's HTTP read; backfilled fields
always carry equal read tiers, so none of them is affected.

model.BroadcastValue keeps its signature but now withholds more: it
withholds the value of any field with no permissions object, so for
fields of plugin-registered v1 groups it always withholds, and it
withholds a linked field's value, having no template to read. The core
callers use the new model.BroadcastValueWithTemplate through
App.PropertyValueBroadcaster, which reads each template once per
broadcast and withholds on a failed read; the property syncer uses the
template it already loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fall back to the single linked user field for editor masking

When a channel field's template names no mask_by_field_id, policy masking
now takes the caller's holdings from the one user field linked to the same
template, as the editor did before mask_by_field_id existed. Two or more
such fields fail the reference closed with ambiguous_sibling, as before.
A declared mask_by_field_id still wins. The property read hook is
unchanged and keeps resolving holdings from mask_by_field_id only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Backfill only fields with no permissions object

The permissions backfill runs on every startup and paged through every
property field to find the ones it converts. Add an internal store search,
GetFieldsMissingPermissions, that returns only live PSAv2 fields in version
2 and 3 groups whose Permissions column is NULL, paged by (CreateAt, Id),
and use it in MigrateBackfillPropertyPermissions. A startup with nothing to
convert now reads one empty page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Skip a backfilled field that changed since the scan

The backfill converted the copy of each field its page read, but
updatePropertyFields took the expected UpdateAt from its own re-read, so a
legacy edit from an old node landing in between passed the check and was
overwritten. Add updatePropertyFieldsIfUnchanged, which takes the UpdateAt
each field was read at and returns store.ErrConflict if the row has moved
on, and use it in the backfill with the UpdateAts its page read. A field
that conflicts is logged, counted as skipped and left unconverted for the
next startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Report the stricter of a linked field's own and template access mode

EffectiveAccessMode returned a linked field's template mode alone, so a
master shared_only field linked to an unmasked template, which the
conversion narrows to none reads instead of masking, reported public.
Policy masking then showed every literal and the v2 view reported the
field public. A linked field now reports the stricter of its own mode and
its template's, and a linked field whose template can't be read keeps its
own source_only rather than dropping to shared_only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Apply golangci-lint v2.15.0 modernize fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pin the managed category group at V2 instead of bumping it

The managed category setup now registers the group, pins it at V2 with
ensureManagedCategoryGroupVersion, and only then converts its fields, on
every startup whether or not the setup flag is saved. A converted field
doesn't validate against a version 1 group, and the pin re-caches the
group that registering has just cached. This replaces the increment that
ran only for the legacy marker, which the store no longer offers, and also
repairs a group that drifted past V2.

An install that stopped between creating the field and saving the flag
now finishes in one boot and saves the current flag, rather than saving
the legacy marker and converting on the next boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Describe the managed category group as pinned, not bumped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 10:36:29 -04:00
LevandClaude Fable 5.1 18b992d329 Make WebConn.ShouldSendEvent allocation-free with an immutable session bundle (#38992)
* Make WebConn.ShouldSendEvent allocation-free with an immutable session bundle

WebConn.ShouldSendEvent runs once per connection for every event the hub
delivers. With channel-scoped iteration off (the default) that is every
connection on the node for every channel-scoped event. Each call
allocated 336 bytes in two places unrelated to the decision itself:

- IsMFAAuthenticated built a fresh request.Context from the session on
  every call, and the clone escaped into the MFARequired interface call
  (320 bytes).
- The session's roles string was split with strings.Fields on every
  call, whether or not a permission check followed (16 bytes).

At 15k connections per node that is about 5 MB of garbage per
channel-scoped event.

Store the session as an immutable bundle behind the existing atomic
pointer: a deep copy of the session, its roles parsed once, and a
request context built once. SetSession builds the bundle in one store;
GetSession keeps its signature. The hot path reads the bundle and passes
the cached context to MFARequired. The cached context is shared
read-only by the hub and read-pump goroutines; comments on the bundle,
the session getter, and the roles accessor say so. The trade is about
320 bytes resident per connection.

No decision changes.

Measured end to end with BenchmarkHubChannelBroadcast (one posted event
through the production hubs, one per CPU, to 5000 connections with 200
memberships each; benchstat, n=10, interleaved), against master:

  iteration  members  time per event    p99 latency       allocs per event
  off        50       780us -> 717us    2.32ms -> 798us   10349 -> 348
  off        5000     1.11ms -> 1.05ms  3.20ms -> 1.19ms  10349 -> 348
  on         50       37.2us -> 34.2us  60us -> 53us      472 -> 372
  on         5000     1.12ms -> 1.05ms  2.70ms -> 1.20ms  10372 -> 372
  on, churn  50       unchanged         unchanged         894 -> 802

Bytes per event drop from 1.6 MB to 19 KB. With the work spread over one
hub per CPU the mean time gains 6 to 8%; the tail gains most, p99 down
56 to 66% where every connection is checked, because the collector no
longer has 1.6 MB of garbage per event to chase. The allocations left
are the per-hub event preparation (29 per hub). The bundle adds about
400 bytes per connection (live heap +1.4 to 1.5%, 1.9 MiB at 5000
connections).
The churn case is bound by membership reloads on the hub goroutine,
which this change does not touch.

Per call (BenchmarkShouldSendEvent, 200 memberships, mock suite; the
real App suite also measures zero allocations on the channel and
user-scoped paths): 102 to 149 ns and 2 allocations before, 37 to 83 ns
and none after. Events flagged as sanitized or sensitive still allocate
inside RolesGrantPermission, which is out of scope here.

Tests: TestWebConnShouldSendEventDecisions pins 23 decision cases with a
denying mock suite; TestWebConnShouldSendEventAllocs asserts zero
allocations for six cases and is stable under the race detector;
reintroducing either removed allocation fails all six.

Release Note:
NONE

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Use an admin session for the sensitive-data allocation case

The platform test helper's suite grants every permission, so the case passed
with a plain user session, but a reader has to know that to see why. Give the
session the admin role so the case reads as what it is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Move the cached request context lookup into a helper

getRCtx returns the request context built in SetSession, or an empty one when
there is no session, so IsMFAAuthenticated reads like getSessionRoles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-29 11:54:57 +00:00
Eva SarafianouandCursor 0cfb204f04 Bump github.com/andybalholm/brotli to v1.2.7 (#39543)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 10:41:41 +00:00
cursor[bot] 0eb2eb7ebc drip: AppError -> error: login attempts (#39539) 2026-10-29 07:36:31 -03:00
Nuno Simões ea90a1c2d5 docs: add Ubuntu 26.04 LTS to supported APT install targets (#39498)
Mattermost .deb packages are now published to the `resolute` suite of the APT repository. List Ubuntu 26.04 LTS alongside 22.04 and 24.04 on the Ubuntu/Debian deployment pages. Scope is host APT installs only; the Docker image base is unchanged.
2026-10-29 10:49:45 +01:00
Felipe MartinandClaude Opus 5.5 7823260826 Let plugin shared channel receive APIs run on any cluster node (#39504)
The shared channel service is only active on the cluster leader, but
plugins run on every node. ReceiveSharedChannelSyncMsg,
ReceiveSharedChannelAttachmentSyncMsg and
ReceiveSharedChannelProfileImageSyncMsg required an active service, so a
plugin on a follower node got a service_inactive error and could not
ingest inbound data.

These APIs now only require the service to exist, the same as the HTTP
remote cluster receive path, which has no leader check. Received content
is written to the database on the receiving node. Posts, reactions and
acknowledgements reach other remotes through cluster websocket events
handled by the leader. Sync tasks queued as a side effect on a follower
(membership changes, statuses) wait in that node's inactive queue.
Outbound APIs such as SyncSharedChannel stay leader-only.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:12:41 +01:00
e22bcdd331 fix(e2e): wait for a search-engine answer in the Elasticsearch/OpenSearch search specs (#39445)
postAndFindMessage polled one search and asserted on a second. The server
answers from the database whenever the engine errors (a just-purged index
has no create_at mapping for a few hundred ms), so the poll could be
satisfied by the fallback while the next search, answered by Elasticsearch,
returned nothing.

Engine answers carry highlights in `matches`; database answers don't. With
fromSearchEngine the poll now waits for an engine answer containing the
post, and the assertions run on that same response.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: yasser.khan <yasser.khan@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-10-29 13:12:17 +05:30
fcafb9b89c fix(e2e): repair deletion-report.spec.ts on master (#39376)
* fix(e2e): stop the deletion report and removal notice racing on reviewer threads

Removing a flagged post posted the deletion report and the "removed by"
notice from two goroutines at once. Both are often the first replies on a
reviewer's thread, and two first replies saved concurrently both try to
insert the Threads row: one fails on threads_pkey and its post is dropped.
That made deletion-report.spec.ts flaky on master, and reviewers could
silently miss either message.

The removal notice now waits for the deletion report to be posted.

Co-authored-by: yasser.khan <yasser.khan@mattermost.com>

* Fix shadowed variable lint in deletion report test

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* fix(app): bound deletion-report delivery in PermanentDeleteFlaggedPost

The removal notification waits for the deletion report to be delivered first, but report delivery uploads a file through a filestore write that takes no context. A blocked write never closed deletionReportSent, leaking the notification goroutine and dropping the notice. Give delivery a one-minute context deadline, and close the signal on timeout even if the write ignores the context, logging the timeout.

Co-authored-by: yasser khan <attitude3cena.yf@gmail.com>

* fix(app): do not post the deletion report after its context is done

A file write that resumes after the delivery deadline would otherwise still reach CreatePost in postReviewerMessage, racing the removal notification that the timeout released. Abort the reviewer post when the request context is done before starting the save.

Co-authored-by: yasser khan <attitude3cena.yf@gmail.com>

* basic improvement

* restored accidently altered files

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
Co-authored-by: yasser khan <attitude3cena.yf@gmail.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
2026-10-29 09:45:50 +05:30
Harshil Sharma b9fd23e542 MM-70703: Align file attachment permission checks (#39413) 2026-10-29 07:44:34 +05:30
Wayne WollesenandClaude Opus 5.5 8a53d645de MM-71401: Rebuild the scrambled authentication settings tables (#39482)
* MM-71401: Rebuild the email domain restriction settings table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the MFA settings tables

The Enforce table's System Console path was written as Markdown bold
inside HTML, so it rendered with literal asterisks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the AD/LDAP guest filter and name attribute tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the SAML enable and guest attribute tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the Google OAuth 2.0 settings tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the GitLab OpenID preferred username table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the Google OpenID settings tables

The discovery endpoint table called the setting a string input with
Google's URL as its default. Its config default is empty; the System
Console fills in Google's URL in a read-only field. A blockquote above
the Enable table repeated its true and false values, and is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Remove the Google OpenID preferred username setting

GoogleSettings.UsePreferredUsername exists only because GoogleSettings
shares the SSOSettings struct with GitLab and OpenID. The System Console
offers the setting for GitLab, Entra ID and other OpenID providers, but
not Google, and the Google provider ignores it: GetUserFromJSON discards
the SSO settings and always derives the username from the email address.
Nothing links to the section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the Entra ID OpenID settings tables

The discovery endpoint table gave the common endpoint as the default.
The System Console builds the endpoint from the Directory (tenant) ID in
a read-only field, and only falls back to the common endpoint when no
directory ID is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the other OpenID Connect provider settings tables

The preferred username table was already two columns but kept stray grid
characters in its config cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:23:57 -05:00
Wayne WollesenandClaude Opus 5.5 2515ee2b33 MM-71401: Rebuild the scrambled site settings tables (#39483)
* MM-71401: Rebuild the custom brand text and minimum desktop version tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the max users per team table

The table said the limit counts activated and deactivated users. It
counts active team members only: GetActiveMemberCount skips users
removed from the team and deactivated users. The wording follows the
MM-71123 docs update held for its release, minus the parts that only
apply once that ships, so that update reduces to its own changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the threaded discussions table

Name the two "Enabled" options as the System Console does, say which
ones users can change, and give each option's config.json value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the message link previews table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the LaTeX rendering tables

EnableInlineLatex defaults to true, as it has since it was added, not
false. The web and desktop apps render inline LaTeX only when EnableLatex
is also true, and EnableLatex defaults to false, so inline LaTeX is off
by default anyway. The old text implied the reverse dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the public file links table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Rebuild the admin and end user notices tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MM-71401: Qualify the EnableLatex default by deployment age

Config.SetDefaults gives EnableLatex true when the saved config already
has a SiteURL but no EnableLatex, as a config from before the setting
existed does, and false otherwise. Label the defaults the way the
session revocation setting labels its own isUpdate-dependent default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:23:27 -05:00
Wayne WollesenandClaude Opus 5.5 cf7496d135 Fix the secure file preview and PDF link navigation settings tables (#39475)
* Fix the secure file preview and PDF link navigation settings tables

The "Enable secure file preview on mobile" table came out of the RST
conversion as a single scrambled cell, with the config path, config.json
key and environment variable spliced into the description. Rebuild it as
the usual two-column table on both pages that document it.

Its config details, and those of "Allow PDF link navigation on mobile",
were also wrong. Both settings have lived in NativeAppSettings on the
Environment > Mobile Security page since they were added in v10.11, not
in FileSettings under Site Configuration > File sharing and downloads,
and MobileAllowPdfLinkNavigation defaults to false, not true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Point the Site Configuration copies to Mobile Security

Both settings live under Environment > Mobile Security, yet the Site
Configuration page documented them in full under a section that says its
settings are in Site Configuration > File Sharing and Downloads. Replace
the duplicated tables there with a pointer to the Mobile security
entries, and keep the headings so existing links to these anchors still
land on the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:21:56 -05:00
Wayne WollesenandClaude Opus 5.5 5e9feb818b Mark mobile file upload and download settings as Enterprise (#39473)
The System Console shows "Allow file uploads on mobile" and "Allow file
downloads on mobile" only when the license includes the Compliance
feature, and the server only sends their client config values under that
license. Add the Enterprise and Enterprise Advanced plan badge to both
settings, as "Enable secure file preview on mobile" already has for its
plan.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:21:21 -05:00
Asaad MahmoodandClaude Opus 5.5 1bd58a1746 Add an experimental product rail with Home, Channels and Boards views (#39383)
* Add a product rail beside the team sidebar

A Slack-style navigation rail, behind the ProductRail feature flag, for
switching between Channels, Threads and registered products. The
channel sidebar drops its Threads link and the product switcher drops
the items the rail now shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Split the product rail into Home, Channels and Boards, and fold in the team sidebar

Builds on the product rail:

- Home shows the whole channel sidebar; Channels and Boards narrow it to
  channels (and DMs) or to boards, the channels a plugin registers with
  registerChannelKind. Categories left empty by the split are hidden.
- Each rail item returns to where the user last was in it, per team and
  across reloads: the last channel, board, thread or product page.
- The team sidebar can be hidden from its bottom; the current team then
  tops the rail as a stack that opens a team menu.
- Threads takes the place of the channel sidebar, in its colors.
- Docs is shown as a placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Name the site instead of the product in the header while the product rail is on

The rail already shows which product is open, so the product menu button
shows the site name and a chevron instead of "Channels". The menu holds
site-wide items, and the label keeps the button easy to hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the chevron after the site name in the header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Center the site name on the grid icon in the header

Metropolis sits its capitals about 1px above the middle of its line box,
so the site name read high next to the grid icon. Nudge it down to line
them up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add the ProductRail flag to the generated defaults and support packet golden

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read board channel kinds without depending on the Boards plugin API

The Home/Boards split relies on plugins registering board channels with
registerChannelKind, which comes with the Boards work and isn't on master
yet. Read those registrations directly instead: until the API lands there
are none, and the rail shows Channels, Threads and products without the
split; once it lands, Home and Boards appear with no further change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address review: menu fallback, saved paths, and dragging in filtered sections

- Keep Channels and the products in the product menu when the rail can't
  show for want of a current team.
- Accept only a plain object of team-to-path strings from localStorage.
- While Channels or Boards hides categories, number the shown ones without
  gaps for dragging, and translate category and channel drop indices from
  the shown list to the full one. The hidden categories come from one
  selector the list and each category share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep no-op drops in place, and the unread filter in the rail's section

- A drop at the item's own visible index keeps its place in the full list
  instead of moving it past hidden neighbours.
- With the unread filter on, the current channel is added only when it
  belongs in the product rail's current section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop Home and the rail dividers; let the filter menu show boards in Channels

- The rail lists Channels, Threads, Boards, Docs and products, with no
  Home view and no dividers.
- While the rail splits boards from channels, the filter button above the
  channel list opens a menu: Unreads only, and in Channels, Show boards (a
  sidebar_settings preference). With boards shown, Channels lists them too,
  stays put when a board is opened, and counts their mentions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Even out the space around the team stack at the top of the rail

With the divider gone, give the team stack 12px above and below, rather
than 8px above and 16px below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let the rail sit on the header background, with more room below the team

The rail no longer has a background of its own, so it reads as one with
the team sidebar; badges ring themselves in the header colour. The team
stack gets 24px below it, clearing the Channels badge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Join the rail to the main panel beside the team sidebar; fix thread tags

- With the team sidebar showing, the rail takes the sidebar background
  and the panel's outer border and corners, with a divider before the
  channel list, so the two read as one panel.
- In Threads, remap --semantic-color-general too, so the channel tags in
  the thread list take the sidebar's text colour instead of the light
  theme's dark grey.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Square the thread list's left corners in Threads

With the rail on, the thread list takes the channel sidebar's place against
the panel's left edge, so its corners there are square, like the sidebar's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Round the thread pane, and restyle the active thread-list tab

- Beside the thread list, round the thread pane like the center channel
  beside the channel sidebar, with the sidebar colour behind its corners.
- Give the active thread-list tab the sidebar's colours, so it reads on
  the list's background instead of as blue on blue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Turn the rail off on mobile; use Compass checkboxes in the filter menu

- On mobile the rail is hidden, so treat it as off there: no Channels or
  Boards split, the team sidebar shows whatever the preference, and the
  product menu keeps Channels and the products.
- The filter menu's checkboxes are Compass Checkboxes, shown for the
  item's state only (out of the tab order and accessibility tree, and not
  taking clicks), instead of grey icon glyphs that read as disabled.
- Lighten the active thread-list tab's background to 8%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Lead the rail with Threads; widen and mark the thread list

- Threads is the first item in the rail, ahead of Channels.
- Beside the rail, the thread list is a fixed 368px wide, and the open
  thread is marked with the sidebar text colour at 10%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tighten the space below the team stack to 8px

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let Show boards only toggle what Channels lists; plain unread toggle in Boards

- Turning Show boards off while a board is open no longer moves the rail
  to Boards (and so drops the option from the menu): the setting only
  changes what Channels lists. A channel or board opened still moves the
  rail to its section.
- In Boards, the filter button is the one-click unread toggle again; the
  menu, with Show boards, is only in Channels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the default products at the top of the rail, others at the bottom

The rail leads with Threads, Channels, Boards, Agents and Playbooks; any
other product a plugin registers is pinned to the bottom. Drop the Docs
placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reset the product rail's state on logout

Logging out doesn't reload the page, so the next user to log in on it
would inherit the previous user's last paths and save them as their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tie the rail's side effects to the rail being on screen

The header's site name, the hidden Threads link and the rail's layout
CSS now apply only when the rail is shown (a team, desktop width), and
adding a channel to a category no longer translates its index as if it
were a drop into a filtered list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hide the team sidebar's hide button where the rail isn't shown

On mobile the button could save the preference, which then hid the team
sidebar on returning to desktop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:16:22 -04:00
bfc9583942 drip: use errors.AsType for OAuth store errors (#39479)
Rewrite errors.As switches in app/oauth.go to errors.AsType where each target stays inside its own branch, and cover those branches with a focused test.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jesse <jesse@mattermost.com>
2026-10-28 14:52:22 -03:00
M-ZubairAhmed 76c56ef772 Replace @deanwhillier/jest-matchmedia-mock with a local matchMedia mock (#39496) 2026-10-28 23:11:48 +05:30
Felipe Martin 25f5f13a63 Bump github.com/mattermost/logr/v2 to v2.0.25 (#39486)
v2.0.25 tags the commit that the server already pins as a pseudo-version.
2026-10-28 17:18:24 +01:00
Ben SchumacherandClaude Opus 5.5 0629253243 [MM-71391] Read server logs in blocks and stream log downloads (#39477)
* [MM-71390] Tolerate failed nodes in plain-text logs and fix log viewer filters

Server.GetLogs now gets other nodes' logs through ClusterInterface.QueryLogs,
so a node that can't be reached gets its usual separator followed by the
failed-node error line instead of failing the whole request. Remote logs are
only requested in a licensed, enabled cluster, matching QueryLogs, and
ClusterInterface.GetLogs is removed. The failed-node entry now carries
"failed_node": true, which the structured viewer keys off instead of the
message text. queryLogs returns 400 rather than 500 for a filter body that
can't be decoded or is null.

In the structured viewer, an active time range now counts as a filter for the
empty state, "Clear filters" also clears it, and changing it resets the page,
expanded row and selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpCfx5vEzDnjuW2q3TTD5t

* [MM-71391] Read server logs in blocks and stream log downloads

GetLogsSkipSend scanned mattermost.log backwards one byte per ReadAt, so a
default request cost millions of syscalls per node, and a selective filter
read to the start of the file. It now reads 64 KiB blocks from the end and
splits lines from the buffer, returning the same lines as before. With
date_from set, the scan stops after 100 consecutive entries older than it,
so a single out-of-order entry does not cut it short. A line decoding to
JSON null is now kept as unparseable instead of panicking under a filter.

downloadLogs streams the open log file through WriteFileResponse instead of
loading it into memory; the support packet still uses GetLogFile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpCfx5vEzDnjuW2q3TTD5t

* [MM-71391] Use < for the log page size loop bound

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 16:17:56 +00:00
Katie WiersgallaandEric Sethna 40575a0b58 Update maximize-microsoft-investments.mdx (#39434)
* Update maximize-microsoft-investments.mdx

* Update maximize-microsoft-investments.mdx

* Update maximize-microsoft-investments.mdx

* Remove redundant note on connected workspaces

Removed note about connected workspaces and their limitations with Microsoft Teams.

* Update sign-in instructions for Mattermost responders

---------

Co-authored-by: Eric Sethna <14333569+esethna@users.noreply.github.com>
2026-10-28 08:45:07 -07:00