mattermost-mobile-upstream/app/components/post_draft/index.ts
Harshil Sharma 4e2a508774
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
Access channel action (#10223)
* MM-68219 - Add redacted files placeholder and related functionality

* add feature flag control and e2e tests

* adjust placeholder design, polish code, fix permalink preview issue

* polish code and add missing test

* use import path with right path

* gate post refetch behind the feature flag, refetch the open thread and avoid observeCount

* restore filesInfo for the permalink files prop added upstream

* fix stale redaction on background servers, consume the flag on every fetch path and share the viewport emitter

* replace the in-memory redaction flag with a persisted per-post epoch so ABAC file decisions survive restarts and stale posts stay hidden

* raise the redaction epoch on every resync, capture the epoch counter, only invalidate channels on role changes, and store the newer epoch when a re-verified post is otherwise unchanged

* store superseded responses unverified instead of dropping them, keep the since-fetch when re-verifying, verify permalink embeds against the linked channel, and capture the websocket epoch on arrival

* nvalidate file decisions when a user attribute field changes or all its values are cleared

* batch channel invalidations, close file viewers only when affected, reset the epoch on unstamped redaction changes, and only  re-verify the newest page's attachment posts

* re-check older channel posts a page at a time instead of one request per post

* skip the unstamped epoch lookup for posts with no files, redacted count or permalink

* MM-70905 - disable and grey out the attachment button when an ABAC permission policy denies uploads in the channel

* First draft of updated read permission handling

* keep the attribute-view retry after later invalidations, retry linked posts unless refused for the current epoch, and stop older payloads from rolling back edits via the redaction check

* improved Data fetches

* fixes

* Fixed test

* Fixed archived channel issue

* Removed re-sync on team switch

* Define message enhancement

* Coderabbit fixes

* React to permission_policy_updated only for the actions it touched

The server now lists the rule actions a permission policy change touched.
Every connected client gets this event, so each step runs only when an
action it depends on changed:

- download_file_attachment raises the redaction epoch, which also
  revalidates the render decisions
- any other cached render-decision action only expires the decisions
- channel_read_access reconciles the channel list

An empty list (an auto-add toggle) does nothing, and a server that
predates the list is treated as having changed every action.

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

* Handled empty channel list

* Fixed channel access races and tablet kick fallback

Read stored channel IDs before the team channel list request so channels joined or left mid-request are not wrongly purged or restored, and set the fallback channel instead of the kicked one as current on a tablet when the server is not active.

* Minor fix

* Moved pending channel access checks to EphemeralStore

* Moved channel access revoked sheet helpers to navigation utils

* Gated channel access enforcement on the ChannelPermissionActions feature flag

* Reported a channel lost to read access on reconnect as revoked access

* Addressed review comments on channel read access

Jittered the channel list reconcile after a permission policy update, logged
non-request failures in channel access checks as errors, and set the channel
read access version to 12.2.0.

* Implemented write actions (#10224)

* Implemented write actions

* Lint fix

* Test that write and management policy changes only revalidate decisions

A permission_policy_updated event naming channel_write_access or
channel_management_access expires the cached render decisions those gates
read, without raising the redaction epoch or reconciling the channel list.

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

* Shared the channel access enforcement check

* Addressed review comments on channel write access

Hid pinning and the acknowledge button while a write access policy denies
the channel, as the server refuses both, and moved the read-only notice to
app/components since two screens share it.

---------

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

* Expect no Manage button on archived channel members

The members screen now renders the Manage header button only when the
user can manage and remove members, which is never the case for an
archived channel. MM-T1719_1 still expected the button to render and
failed on detox-ios and detox-android with "Got: was null". Assert that
neither the Manage button nor the remove controls exist instead.

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

---------

Co-authored-by: Pablo Andrés Vélez Vidal <pablovv2012@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-05 17:56:33 +05:30

97 lines
3.6 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import React from 'react';
import {combineLatest, of as of$} from 'rxjs';
import {filter, switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {RenderPermissionAction} from '@constants/access_control';
import {withServerUrl} from '@context/server';
import {observeChannel, observeIsReadOnlyChannel} from '@queries/servers/channel';
import {queryDraft, observeFirstDraft} from '@queries/servers/drafts';
import {observeChannelActionDenied} from '@queries/servers/render_permissions';
import {observePermissionForChannel} from '@queries/servers/role';
import {observeCurrentChannelId} from '@queries/servers/system';
import {observeCurrentUser, observeUser} from '@queries/servers/user';
import {getUserIdFromChannelName} from '@utils/user';
import PostDraft from './post_draft';
import type {WithDatabaseArgs} from '@typings/database/database';
type OwnProps = {
channelId: string;
channelIsArchived?: boolean;
rootId?: string;
serverUrl: string;
}
const enhanced = withObservables(['channelId', 'rootId', 'channelIsArchived'], (ownProps: WithDatabaseArgs & OwnProps) => {
const {database, rootId = '', serverUrl} = ownProps;
let channelId = of$(ownProps.channelId);
if (!ownProps.channelId) {
channelId = observeCurrentChannelId(database);
}
const draft = channelId.pipe(
switchMap((cId) => queryDraft(database, cId, rootId).observeWithColumns(['message', 'files', 'metadata']).pipe(
switchMap(observeFirstDraft),
)),
);
const files = draft.pipe(switchMap((d) => of$(d?.files)));
const message = draft.pipe(switchMap((d) => of$(d?.message)));
const currentUser = observeCurrentUser(database);
const channel = channelId.pipe(
switchMap((id) => observeChannel(database, id!)),
);
const canPost = combineLatest([channel, currentUser]).pipe(switchMap(([c, u]) => (c && u ? observePermissionForChannel(database, c, u, Permissions.CREATE_POST, true) : of$(true))));
const writeDenied = channelId.pipe(
switchMap((id) => observeChannelActionDenied(database, serverUrl, id, RenderPermissionAction.ChannelWriteAccess)),
);
// Filter transient `undefined` emissions so the archived banner doesn't
// flicker off during the archive-modal dismiss transition.
const channelIsArchived = channel.pipe(
filter((c): c is NonNullable<typeof c> => c != null),
switchMap((c) => (ownProps.channelIsArchived ? of$(true) : of$(c.deleteAt !== 0))),
);
const channelIsReadOnly = observeIsReadOnlyChannel(database, ownProps.channelId);
const deactivatedChannel = combineLatest([currentUser, channel]).pipe(
switchMap(([u, c]) => {
if (!u || !c) {
return of$(false);
}
if (c.type !== General.DM_CHANNEL) {
return of$(false);
}
const teammateId = getUserIdFromChannelName(u.id, c.name);
if (teammateId) {
return observeUser(database, teammateId).pipe(
switchMap((u2) => (u2 ? of$(Boolean(u2.deleteAt)) : of$(false))), // eslint-disable-line max-nested-callbacks
);
}
return of$(true);
}),
);
return {
canPost,
channelIsArchived,
channelIsReadOnly,
deactivatedChannel,
files,
message,
writeDenied,
};
});
export default React.memo(withDatabase(withServerUrl(enhanced(PostDraft))));