mattermost-mobile-upstream/app/database/models/server/post.ts
Pablo Vélez 7df4a0ab97
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
Mm 68219 show placeholder for redacted files (#9719)
* 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

* 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

* MM-70905 - disable and grey out the attachment button when an ABAC pe… (#10195)

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

* only back off render permission fetches when the server request fails, and log other errors without retrying

* remove the disableServer helper and have removeServer do both steps

* split defaultAllowed into allowWhenUnenforced and allowWhenNotEvaluated, and treat render permissions as unenforced  on servers without the decisions API

* MM-69627: Add playbook task requirements support on mobile (#10035)

* MM-69627: Add playbook task requirements support on mobile

Allow filling required task fields when checking off checklist items during
a run, gated by the same Playbooks beta setting used on web.

Co-authored-by: Cursor <cursoragent@cursor.com>

* MM-69627: Address PR review feedback for task requirements

Propagate settings write failures, harden fill-requirements save/title handling, and add missing unit coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* MM-69627: Fix TypeScript errors in new unit tests

Co-authored-by: Cursor <cursoragent@cursor.com>

* MM-69627: Use defineMessages for fill requirements titles

Keep i18n extraction able to find the header titles after moving them to the route.

Co-authored-by: Cursor <cursoragent@cursor.com>

* MM-69627: Address review feedback for task requirements

Update checklist items locally when saving requirement values instead of
refetching the run, add missing query/navigation tests, and apply small
callback/reduce cleanups from review.

Co-authored-by: Cursor <cursoragent@cursor.com>

* MM-69627: Fix task requirements review follow-ups

Propagate local checklist write failures, clear a stale task-requirements
flag when playbooks is unsupported, and cap fill values at the desktop/
server max length.

Co-authored-by: Cursor <cursoragent@cursor.com>

* MM-69627: Move requirement field into its own component

Keep one component per file on the fill-requirements screen.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* MM-69122: Add file element support to interactive dialogs (#9898)

* Implement Interactive Dialog - file upload

* update demo plugin version for e2etests

* fix and add e2etests

* review fixes

* Fix version bump checkout when a tag shares the branch name (#10229)

* Fix Android crashes caused by animated custom emoji (#10030)

* Route GIF decoding through Glide

- Keep APNG4Android for APNG, animated WebP, and animated AVIF support.
- Prepend Glide's GIF decoders to avoid APNG4Android's native GIF overflow.

* Clear Expo image resources with Glide targets

- Detach drawables when Glide clears their custom target.
- Prevent recycled GIF frame resources from remaining attached to Expo image views.

* Preserve foreground image during GIF target recycling

---------

Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com>
Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com>

* detox - fix clobbering of feature flags that cause failures in other suites.  Also small fix of a incorrectly set test-id

---------

Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com>
Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com>
Co-authored-by: Nate Schlossberg <nate.schlossberg@mattermost.com>
2026-10-15 15:59:13 -07:00

191 lines
7.7 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q, Query, Relation} from '@nozbe/watermelondb';
import {children, field, immutableRelation, json, lazy} from '@nozbe/watermelondb/decorators';
import Model, {type Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import {safeParseJSON} from '@utils/helpers';
import type ChannelModel from '@typings/database/models/servers/channel';
import type DraftModel from '@typings/database/models/servers/draft';
import type FileModel from '@typings/database/models/servers/file';
import type PostModelInterface from '@typings/database/models/servers/post';
import type PostInThreadModel from '@typings/database/models/servers/posts_in_thread';
import type ReactionModel from '@typings/database/models/servers/reaction';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
const {CHANNEL, DRAFT, FILE, POST, POSTS_IN_THREAD, REACTION, THREAD, USER} = MM_TABLES.SERVER;
/**
* The Post model is the building block of communication in the Mattermost app.
*/
export default class PostModel extends Model implements PostModelInterface {
/** table (name) : Post */
static table = POST;
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A CHANNEL can have multiple POST. (relationship is 1:N) */
[CHANNEL]: {type: 'belongs_to', key: 'channel_id'},
/** A POST can have multiple DRAFT. (relationship is 1:N) */
[DRAFT]: {type: 'has_many', foreignKey: 'root_id'},
/** A POST can have multiple FILE. (relationship is 1:N)*/
[FILE]: {type: 'has_many', foreignKey: 'post_id'},
/** A POST can have multiple POSTS_IN_THREAD. (relationship is 1:N)*/
[POSTS_IN_THREAD]: {type: 'has_many', foreignKey: 'root_id'},
/** A POST can have multiple REACTION. (relationship is 1:N)*/
[REACTION]: {type: 'has_many', foreignKey: 'post_id'},
/** A POST can have an associated thread. (relationship is 1:1) */
[THREAD]: {type: 'has_many', foreignKey: 'id'},
/** A USER can have multiple POST. A user can author several posts. (relationship is 1:N)*/
[USER]: {type: 'belongs_to', key: 'user_id'},
};
/** channel_id : The foreign key for the Channel to which this post belongs to. */
@field('channel_id') channelId!: string;
/** create_at : The timestamp to when this post was first created */
@field('create_at') createAt!: number;
/** delete_at : The timestamp to when this post was last archived/deleted */
@field('delete_at') deleteAt!: number;
/** update_at : The timestamp to when this post was last updated on the server */
@field('update_at') updateAt!: number;
/** edit_at : The timestamp to when this post was last edited */
@field('edit_at') editAt!: number;
/** is_pinned : A Boolean flag indicating if this Post is pinned */
@field('is_pinned') isPinned!: boolean;
/** message : Message in the post */
@field('message') message!: string;
/** messageSource : will contain the message as submitted by the user if Message has been modified
by Mattermost for presentation (e.g if an image proxy is being used). It should be used to
populate edit boxes if present. */
@field('message_source') messageSource!: string;
/** metadata: All the extra data associated with this Post */
@json('metadata', safeParseJSON) metadata!: PostMetadata | null;
/** original_id : Any post will have this value empty unless it is updated */
@field('original_id') originalId!: string;
/** pending_post_id : The id given to a post before it is published on the server */
@field('pending_post_id') pendingPostId!: string;
/** previous_post_id : Id of the previous post. If this value is empty, this implies that it is not in the db and we will request it from server */
@field('previous_post_id') previousPostId!: string;
/** redaction_verified_epoch : Local-only. The redaction epoch this post's attachment metadata was
* last confirmed under. Never sent to the server. Attachments render only while this is at least
* the epoch currently required for the post's channel; see @actions/local/redaction. */
@field('redaction_verified_epoch') redactionVerifiedEpoch!: number;
/** root_id : Used in threads. All posts under a thread will have this id in common */
@field('root_id') rootId!: string;
/** type : Type of props (e.g. system message) */
@field('type') type!: PostType;
/** user_id : The foreign key of the User who authored this post. */
@field('user_id') userId!: string;
/** props : Additional attributes for this props */
@json('props', safeParseJSON) props!: any;
// A draft can be associated with this post for as long as this post is a parent post
@lazy drafts = this.collections.get<DraftModel>(DRAFT).query(Q.on(POST, 'id', this.id));
@lazy root = this.collection.query(Q.where('id', this.rootId)) as Query<PostModel>;
/** postsInThread: The thread to which this post is associated */
@lazy postsInThread = this.collections.get<PostInThreadModel>(POSTS_IN_THREAD).query(
Q.where('root_id', this.rootId || this.id),
Q.sortBy('latest', Q.desc),
Q.take(1),
);
/** files: All the files associated with this Post */
@children(FILE) files!: Query<FileModel>;
/** reactions: All the reactions associated with this Post */
@children(REACTION) reactions!: Query<ReactionModel>;
/** author: The author of this Post */
@immutableRelation(USER, 'user_id') author!: Relation<UserModel>;
/** channel: The channel which is presenting this Post */
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<ChannelModel>;
/** thread : The thread data for the post */
@immutableRelation(THREAD, 'id') thread!: Relation<ThreadModel>;
async destroyPermanently() {
await this.reactions.destroyAllPermanently();
await this.files.destroyAllPermanently();
await this.drafts.destroyAllPermanently();
await this.collections.get(POSTS_IN_THREAD).query(
Q.where('root_id', this.id),
).destroyAllPermanently();
try {
const thread = await this.thread.fetch();
if (thread) {
await thread.destroyPermanently();
await thread.participants.destroyAllPermanently();
await thread.threadsInTeam.destroyAllPermanently();
}
} catch {
// there is no thread record for this post
}
super.destroyPermanently();
}
async hasReplies() {
if (!this.rootId) {
return (await this.postsInThread.fetch()).length > 0;
}
const root = await this.root.fetch();
if (root.length) {
return (await root[0].postsInThread.fetch()).length > 0;
}
return false;
}
toApi = async (): Promise<Post> => ({
id: this.id,
create_at: this.createAt,
update_at: this.updateAt,
edit_at: this.editAt,
delete_at: this.deleteAt,
is_pinned: this.isPinned,
user_id: this.userId,
channel_id: this.channelId,
root_id: this.rootId,
original_id: this.originalId,
message: this.message,
message_source: this.messageSource,
type: this.type,
props: this.props,
pending_post_id: this.pendingPostId,
file_ids: (await this.files.fetchIds()),
metadata: (this.metadata ? this.metadata : {}) as PostMetadata,
hashtags: '',
reply_count: 0,
});
}