mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 02:18:56 +00:00
* 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>
191 lines
7.7 KiB
TypeScript
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,
|
|
});
|
|
}
|