diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx index 36bcd7f04..f0efcfeb3 100644 --- a/app/components/markdown/markdown.tsx +++ b/app/components/markdown/markdown.tsx @@ -29,11 +29,11 @@ import MarkdownTable from './markdown_table'; import MarkdownTableCell, {type MarkdownTableCellProps} from './markdown_table_cell'; import MarkdownTableImage from './markdown_table_image'; import MarkdownTableRow, {type MarkdownTableRowProps} from './markdown_table_row'; -import {addListItemIndices, combineTextNodes, highlightMentions, highlightSearchPatterns, parseTaskLists, pullOutImages} from './transform'; +import {addListItemIndices, combineTextNodes, highlightMentions, highlightWithoutNotification, highlightSearchPatterns, parseTaskLists, pullOutImages} from './transform'; import type { MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer, - MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, SearchPattern, UserMentionKey, + MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, SearchPattern, UserMentionKey, HighlightWithoutNotificationKey, } from '@typings/global/markdown'; type MarkdownProps = { @@ -55,6 +55,7 @@ type MarkdownProps = { disableTables?: boolean; enableLatex: boolean; enableInlineLatex: boolean; + highlightKeys?: HighlightWithoutNotificationKey[]; imagesMetadata?: Record; isEdited?: boolean; isReplyPost?: boolean; @@ -133,7 +134,7 @@ const Markdown = ({ disableCodeBlock, disableGallery, disableHashtags, disableHeading, disableTables, enableInlineLatex, enableLatex, maxNodes, imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutHeight, layoutWidth, - location, mentionKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns, + location, mentionKeys, highlightKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns, textStyles = {}, theme, value = '', baseParagraphStyle, onLinkLongPress, }: MarkdownProps) => { const style = getStyleSheet(theme); @@ -578,6 +579,7 @@ const Markdown = ({ mention_highlight: Renderer.forwardChildren, search_highlight: Renderer.forwardChildren, + highlight_without_notification: Renderer.forwardChildren, checkbox: renderCheckbox, editedIndicator: renderEditedIndicator, @@ -604,10 +606,12 @@ const Markdown = ({ if (mentionKeys) { ast = highlightMentions(ast, mentionKeys); } + if (highlightKeys) { + ast = highlightWithoutNotification(ast, highlightKeys); + } if (searchPatterns) { ast = highlightSearchPatterns(ast, searchPatterns); } - if (isEdited) { const editIndicatorNode = new Node('edited_indicator'); if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) { diff --git a/app/components/markdown/transform.test.ts b/app/components/markdown/transform.test.ts index 1e3efff64..bcecd4c97 100644 --- a/app/components/markdown/transform.test.ts +++ b/app/components/markdown/transform.test.ts @@ -13,6 +13,8 @@ import { highlightTextNode, mentionKeysToPatterns, pullOutImages, + highlightWithoutNotification, + highlightKeysToPatterns, } from '@components/markdown/transform'; import {logError} from '@utils/log'; @@ -2602,7 +2604,7 @@ describe('Components.Markdown.transform', () => { } }); - describe('getFirstMention', () => { + describe('getFirstMention with mentionKeysToPatterns', () => { const tests = [{ name: 'no mention keys', input: 'apple banana orange', @@ -2801,6 +2803,461 @@ describe('Components.Markdown.transform', () => { }); } }); + + describe('highlightWithoutNotification', () => { + const tests = [{ + name: 'no highlights', + input: 'Cant put down an anti gravity book', + highlightKeys: [], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + literal: 'Cant put down an anti gravity book', + }], + }], + }, + }, { + name: 'key bigger than input', + input: 'incredible', + highlightKeys: [{key: 'incredible and industructable'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + literal: 'incredible', + }], + }], + }, + }, { + name: 'key part of the word', + input: 'Sesquipedalian', + highlightKeys: [{key: 'quipedalian'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + literal: 'Sesquipedalian', + }], + }], + }, + }, { + name: 'word part of key', + input: 'floccinauc', + highlightKeys: [{key: 'floccinaucinihilipilification'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + literal: 'floccinauc', + }], + }], + }, + }, { + name: 'a word highlight', + input: 'Cant put down an anti gravity book', + highlightKeys: [{key: 'anti'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [ + { + type: 'text', + literal: 'Cant put down an ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'anti', + }], + }, + { + type: 'text', + literal: ' gravity book', + }, + ], + }], + }, + }, { + name: 'a sentence highlight', + input: 'Cant put down an anti gravity book', + highlightKeys: [{key: 'anti gravity'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [ + { + type: 'text', + literal: 'Cant put down an ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'anti gravity', + }], + }, + { + type: 'text', + literal: ' book', + }, + ], + }], + }, + }, { + name: 'insensitive keywords', + input: 'Cant put down an anti gravity book', + highlightKeys: [{key: 'dOwN'}, {key: 'Anti'}, {key: 'BOOK'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [ + { + type: 'text', + literal: 'Cant put ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'down', + }], + }, + { + type: 'text', + literal: ' an ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'anti', + }], + }, + { + type: 'text', + literal: ' gravity ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'book', + }], + }, + ], + }], + }, + }, { + name: 'insensitive keywords', + input: 'Cant put down an anti gravity book', + highlightKeys: [{key: 'dOwN'}, {key: 'Anti'}, {key: 'BOOK'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [ + { + type: 'text', + literal: 'Cant put ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'down', + }], + }, + { + type: 'text', + literal: ' an ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'anti', + }], + }, + { + type: 'text', + literal: ' gravity ', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'book', + }], + }, + ], + }], + }, + }, { + name: 'words with characters surrounding them', + input: 'peace& ^peace -peace-', + highlightKeys: [{key: 'PEACE'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [ + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'peace', + }], + }, + { + type: 'text', + literal: '& ^', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'peace', + }], + }, + { + type: 'text', + literal: ' -', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'peace', + }], + }, + { + type: 'text', + literal: '-', + }, + ], + }], + }, + }, { + name: 'input in code block', + input: '```\nTurning it off and\non\n```', + highlightKeys: [{key: 'words'}], + expected: { + type: 'document', + children: [{ + type: 'code_block', + literal: 'Turning it off and\non\n', + }], + }, + }, { + name: 'key in bold', + input: 'Actions speak **louder** than words', + highlightKeys: [{key: 'louder'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + literal: 'Actions speak ', + }, { + type: 'strong', + children: [{ + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'louder', + }], + }], + }, { + type: 'text', + literal: ' than words', + }], + }], + }, + }, { + name: 'key in italic', + input: 'Actions speak *louder* than words', + highlightKeys: [{key: 'louder'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + literal: 'Actions speak ', + }, { + type: 'emph', + children: [{ + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'louder', + }], + }], + }, { + type: 'text', + literal: ' than words', + }], + }], + }, + }, { + name: 'key in heading', + input: '### Actions speak louder than words', + highlightKeys: [{key: 'Actions'}], + expected: { + type: 'document', + children: [{ + type: 'heading', + level: 3, + children: [ + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: 'Actions', + }], + }, + { + type: 'text', + literal: ' speak louder than words', + }], + }], + }, + }, { + name: 'Do not mention partial keys', + input: 'Adding more memory wont help @bob', + highlightKeys: [{key: 'bob'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + literal: 'Adding more memory wont help ', + }, { + type: 'at_mention', + _mentionName: 'bob', + }], + }], + }, + }, { + name: 'CJK word highlight', + input: '我确实喜欢我的同事。', + highlightKeys: [{key: '喜欢'}], + expected: { + type: 'document', + children: [{ + type: 'paragraph', + children: [ + { + type: 'text', + literal: '我确实', + }, + { + type: 'highlight_without_notification', + children: [{ + type: 'text', + literal: '喜欢', + }], + }, + { + type: 'text', + literal: '我的同事。', + }, + ], + }], + }, + }]; + + for (const test of tests) { + it(test.name, () => { + const input = combineTextNodes(parser.parse(test.input)); + const expected = makeAst(test.expected); + const actual = highlightWithoutNotification(input, test.highlightKeys); + + assert.ok(verifyAst(actual)); + assert.deepStrictEqual(stripUnusedFields(actual), stripUnusedFields(expected)); + }); + } + }); + + describe('getFirstMatch with highlightKeysToPatterns', () => { + const tests = [{ + name: 'text with space before', + input: ' lol', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 1, length: 3}, + }, + { + name: 'text with space afterwards and before', + input: ' Lol ', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 1, length: 3}, + }, + { + name: 'text with a non word character before (?)', + input: '?Lol', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 1, length: 3}, + }, + { + name: 'text with a non word character before (.)', + input: '?Lol', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 1, length: 3}, + }, + { + name: 'text with a non word character before (_)', + input: '?Lol', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 1, length: 3}, + }, + { + name: 'text with non word character after (.)', + input: 'Lol.', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 0, length: 3}, + }, + { + name: 'text with non word character after (?)', + input: 'Lol?', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 0, length: 3}, + }, + { + name: 'text with non word character after (_)', + input: 'Lol_', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 0, length: 3}, + }, + { + name: 'text with non word character before and after', + input: '?Lol?', + patterns: highlightKeysToPatterns([{key: 'lol'}]), + expected: {index: 1, length: 3}, + }]; + + for (const test of tests) { + it(test.name, () => { + const actual = getFirstMatch(test.input, test.patterns); + + assert.deepStrictEqual(actual, test.expected); + }); + } + }); }); // Testing and debugging functions diff --git a/app/components/markdown/transform.ts b/app/components/markdown/transform.ts index bf32d0c57..20b4bfd8a 100644 --- a/app/components/markdown/transform.ts +++ b/app/components/markdown/transform.ts @@ -5,7 +5,7 @@ import {Node, type NodeType} from 'commonmark'; import {escapeRegex} from '@utils/markdown'; -import type {SearchPattern, UserMentionKey} from '@typings/global/markdown'; +import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown'; /* eslint-disable no-underscore-dangle */ @@ -174,6 +174,55 @@ export function mentionKeysToPatterns(mentionKeys: UserMentionKey[]) { }); } +export function highlightWithoutNotification(ast: Node, highlightKeys: HighlightWithoutNotificationKey[]) { + const walker = ast.walker(); + + const patterns = highlightKeysToPatterns(highlightKeys); + + let e; + while ((e = walker.next())) { + if (!e.entering) { + continue; + } + + const node = e.node; + if (node.type === 'text' && node.literal) { + const {index, length} = getFirstMatch(node.literal, patterns); + + // If the text node doesn't match any of the patterns, skip the loop + if (index === -1) { + continue; + } + + const matchNode = highlightTextNode(node, index, index + length, 'highlight_without_notification'); + + // Resume processing on the next node after the match node which may include any remaining text + // that was part of this one + walker.resumeAt(matchNode, false); + } + } + return ast; +} + +export function highlightKeysToPatterns(highlightKeys: HighlightWithoutNotificationKey[]) { + if (highlightKeys.length === 0) { + return []; + } + + return highlightKeys. + filter((highlight) => highlight.key.trim() !== ''). + sort((a, b) => b.key.length - a.key.length). + map(({key}) => { + if (cjkPattern.test(key)) { + // If the key contains Chinese, Japanese, Korean or Russian characters, don't mark word boundaries + return new RegExp(`${escapeRegex(key)}`, 'gi'); + } + + // If the key contains only English characters, mark word boundaries + return new RegExp(`(^|\\b)(${escapeRegex(key)})(?=_*\\b)`, 'gi'); + }); +} + export function highlightSearchPatterns(ast: Node, searchPatterns: SearchPattern[]) { const walker = ast.walker(); @@ -212,14 +261,22 @@ export function getFirstMatch(str: string, patterns: RegExp[]) { let firstMatchLength = -1; for (const pattern of patterns) { - const match = pattern.exec(str); - if (!match || match[0] === '') { + let matchResult; + if (pattern.global || pattern.sticky) { + // Since regex objects are stateful in global or sticky flags, we need to reset + const regex = new RegExp(pattern.source, pattern.flags); + matchResult = regex.exec(str); + } else { + matchResult = pattern.exec(str); + } + + if (!matchResult || matchResult[0] === '') { continue; } - if (firstMatchIndex === -1 || match.index < firstMatchIndex) { - firstMatchIndex = match.index; - firstMatchLength = match[0].length; + if (firstMatchIndex === -1 || matchResult.index < firstMatchIndex) { + firstMatchIndex = matchResult.index; + firstMatchLength = matchResult[0].length; } } diff --git a/app/components/post_list/post/body/message/index.ts b/app/components/post_list/post/body/message/index.ts index cb56ac2b6..68ed57f33 100644 --- a/app/components/post_list/post/body/message/index.ts +++ b/app/components/post_list/post/body/message/index.ts @@ -4,6 +4,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import {observeIfHighlightWithoutNotificationHasLicense} from '@queries/servers/system'; import {observeCurrentUser} from '@queries/servers/user'; import Message from './message'; @@ -12,8 +13,10 @@ import type {WithDatabaseArgs} from '@typings/database/database'; const withMessageInput = withObservables([], ({database}: WithDatabaseArgs) => { const currentUser = observeCurrentUser(database); + const isHighlightWithoutNotificationLicensed = observeIfHighlightWithoutNotificationHasLicense(database); return { currentUser, + isHighlightWithoutNotificationLicensed, }; }); diff --git a/app/components/post_list/post/body/message/message.tsx b/app/components/post_list/post/body/message/message.tsx index cfb8df411..1e70425a3 100644 --- a/app/components/post_list/post/body/message/message.tsx +++ b/app/components/post_list/post/body/message/message.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {type LayoutChangeEvent, ScrollView, useWindowDimensions, View} from 'react-native'; import Animated from 'react-native-reanimated'; @@ -16,10 +16,11 @@ import ShowMoreButton from './show_more_button'; import type PostModel from '@typings/database/models/servers/post'; import type UserModel from '@typings/database/models/servers/user'; -import type {SearchPattern} from '@typings/global/markdown'; +import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown'; type MessageProps = { currentUser?: UserModel; + isHighlightWithoutNotificationLicensed?: boolean; highlight: boolean; isEdited: boolean; isPendingOrFailed: boolean; @@ -33,6 +34,9 @@ type MessageProps = { const SHOW_MORE_HEIGHT = 54; +const EMPTY_MENTION_KEYS: UserMentionKey[] = []; +const EMPTY_HIGHLIGHT_KEYS: HighlightWithoutNotificationKey[] = []; + const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { messageContainer: { @@ -52,7 +56,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const Message = ({currentUser, highlight, isEdited, isPendingOrFailed, isReplyPost, layoutWidth, location, post, searchPatterns, theme}: MessageProps) => { +const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight, isEdited, isPendingOrFailed, isReplyPost, layoutWidth, location, post, searchPatterns, theme}: MessageProps) => { const [open, setOpen] = useState(false); const [height, setHeight] = useState(); const dimensions = useWindowDimensions(); @@ -62,10 +66,6 @@ const Message = ({currentUser, highlight, isEdited, isPendingOrFailed, isReplyPo const blockStyles = getMarkdownBlockStyles(theme); const textStyles = getMarkdownTextStyles(theme); - const mentionKeys = useMemo(() => { - return currentUser?.mentionKeys; - }, [currentUser]); - const onLayout = useCallback((event: LayoutChangeEvent) => setHeight(event.nativeEvent.layout.height), []); const onPress = () => setOpen(!open); @@ -96,7 +96,8 @@ const Message = ({currentUser, highlight, isEdited, isPendingOrFailed, isReplyPo postId={post.id} textStyles={textStyles} value={post.message} - mentionKeys={mentionKeys} + mentionKeys={currentUser?.mentionKeys ?? EMPTY_MENTION_KEYS} + highlightKeys={isHighlightWithoutNotificationLicensed ? (currentUser?.highlightKeys ?? EMPTY_HIGHLIGHT_KEYS) : EMPTY_HIGHLIGHT_KEYS} searchPatterns={searchPatterns} theme={theme} /> diff --git a/app/components/user_list/__snapshots__/index.test.tsx.snap b/app/components/user_list/__snapshots__/index.test.tsx.snap index dfd9f626f..80e960a70 100644 --- a/app/components/user_list/__snapshots__/index.test.tsx.snap +++ b/app/components/user_list/__snapshots__/index.test.tsx.snap @@ -30,6 +30,7 @@ exports[`components/channel_list_row should show no results 1`] = ` "desktop_sound": "true", "email": "true", "first_name": "true", + "highlight_keys": "", "mention_keys": "", "push": "mention", "push_status": "away", @@ -317,6 +318,7 @@ exports[`components/channel_list_row should show results and tutorial 1`] = ` "desktop_sound": "true", "email": "true", "first_name": "true", + "highlight_keys": "", "mention_keys": "", "push": "mention", "push_status": "away", @@ -613,6 +615,7 @@ exports[`components/channel_list_row should show results no tutorial 1`] = ` "desktop_sound": "true", "email": "true", "first_name": "true", + "highlight_keys": "", "mention_keys": "", "push": "mention", "push_status": "away", @@ -900,6 +903,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`] "desktop_sound": "true", "email": "true", "first_name": "true", + "highlight_keys": "", "mention_keys": "", "push": "mention", "push_status": "away", @@ -932,6 +936,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`] "desktop_sound": "true", "email": "true", "first_name": "true", + "highlight_keys": "", "mention_keys": "", "push": "mention", "push_status": "away", diff --git a/app/components/user_list/index.test.tsx b/app/components/user_list/index.test.tsx index 8ae4fbaf6..72ef80f53 100644 --- a/app/components/user_list/index.test.tsx +++ b/app/components/user_list/index.test.tsx @@ -34,6 +34,7 @@ describe('components/channel_list_row', () => { email: 'true', first_name: 'true', mention_keys: '', + highlight_keys: '', push: 'mention', push_status: 'away', }, @@ -61,6 +62,7 @@ describe('components/channel_list_row', () => { email: 'true', first_name: 'true', mention_keys: '', + highlight_keys: '', push: 'mention', push_status: 'away', }, diff --git a/app/constants/license.ts b/app/constants/license.ts index 7016c3d57..36edb975e 100644 --- a/app/constants/license.ts +++ b/app/constants/license.ts @@ -9,4 +9,9 @@ export default { Professional: 'professional', Enterprise: 'enterprise', }, + SelfHostedProducts: { + STARTER: 'starter', + PROFESSIONAL: 'professional', + ENTERPRISE: 'enterprise', + }, }; diff --git a/app/database/models/server/user.ts b/app/database/models/server/user.ts index dd4a5db32..c9c22154e 100644 --- a/app/database/models/server/user.ts +++ b/app/database/models/server/user.ts @@ -16,7 +16,7 @@ import type ReactionModel from '@typings/database/models/servers/reaction'; import type TeamMembershipModel from '@typings/database/models/servers/team_membership'; import type ThreadParticipantsModel from '@typings/database/models/servers/thread_participant'; import type UserModelInterface from '@typings/database/models/servers/user'; -import type {UserMentionKey} from '@typings/global/markdown'; +import type {UserMentionKey, HighlightWithoutNotificationKey} from '@typings/global/markdown'; const { CHANNEL, @@ -194,4 +194,24 @@ export default class UserModel extends Model implements UserModelInterface { m.key !== '@here' )); } + + get highlightKeys() { + if (!this.notifyProps) { + return []; + } + + const highlightWithoutNotificationKeys: HighlightWithoutNotificationKey[] = []; + + if (this.notifyProps?.highlight_keys?.length) { + this.notifyProps.highlight_keys. + split(','). + forEach((key) => { + if (key.trim().length > 0) { + highlightWithoutNotificationKeys.push({key: key.trim()}); + } + }); + } + + return highlightWithoutNotificationKeys; + } } diff --git a/app/database/operator/server_data_operator/handlers/user.test.ts b/app/database/operator/server_data_operator/handlers/user.test.ts index b4a230db3..2fd508b15 100644 --- a/app/database/operator/server_data_operator/handlers/user.test.ts +++ b/app/database/operator/server_data_operator/handlers/user.test.ts @@ -74,6 +74,7 @@ describe('*** Operator: User Handlers tests ***', () => { first_name: 'true', mark_unread: 'mention', mention_keys: '', + highlight_keys: '', push: 'mention', channel: 'true', auto_responder_active: 'false', diff --git a/app/database/operator/server_data_operator/transformers/user.test.ts b/app/database/operator/server_data_operator/transformers/user.test.ts index 05d886995..be4b088d7 100644 --- a/app/database/operator/server_data_operator/transformers/user.test.ts +++ b/app/database/operator/server_data_operator/transformers/user.test.ts @@ -57,6 +57,7 @@ describe('*** USER Prepare Records Test ***', () => { email: 'true', first_name: 'true', mention_keys: '', + highlight_keys: '', mark_unread: 'mention', push: 'mention', channel: 'true', diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index 3ad4ad075..a369343cc 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -5,7 +5,7 @@ import {Database, Q} from '@nozbe/watermelondb'; import {of as of$, Observable, combineLatest} from 'rxjs'; import {switchMap, distinctUntilChanged} from 'rxjs/operators'; -import {Preferences} from '@constants'; +import {Preferences, License} from '@constants'; import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy'; import {isMinimumServerVersion} from '@utils/helpers'; @@ -560,3 +560,60 @@ export const observeLastServerVersionCheck = (database: Database) => { switchMap((model) => of$(parseInt(model.value, 10))), ); }; + +export const observeIfHighlightWithoutNotificationHasLicense = (database: Database) => { + const license = observeLicense(database); + + const isCloudStarterFree = checkIsCloudStarterFree(license); + const isStarterSKULicense = checkIsStarterSKULicense(license); + const isSelfHostedStarter = observeIsSelfHosterStarter(database); + const isEnterpriseReady = observeConfigBooleanValue(database, 'BuildEnterpriseReady', false); + + return combineLatest([isCloudStarterFree, isStarterSKULicense, isSelfHostedStarter, isEnterpriseReady]).pipe( + switchMap(([isCSF, isSSL, isSHS, isEnt]) => { + // It should have enterprise build AND not have a starter license of any kind + const highlightWithoutNotificationHasLicense = isEnt && !(isCSF || isSSL || isSHS); + + return of$(highlightWithoutNotificationHasLicense); + }), + ); +}; + +function checkIsCloudStarterFree(license: Observable) { + return license.pipe( + switchMap((l) => { + const isCloud = l?.Cloud === 'true'; + const isStarterSKU = l?.SkuShortName === License.SKU_SHORT_NAME.Starter; + + return of$(isCloud && isStarterSKU); + }), + distinctUntilChanged(), + ); +} + +function checkIsStarterSKULicense(license: Observable) { + return license.pipe( + switchMap((l) => { + const isLicensed = l?.IsLicensed === 'true'; + const isSelfHostedStarterProduct = l?.SelfHostedProducts === License.SelfHostedProducts.STARTER; + + return of$(isLicensed && isSelfHostedStarterProduct); + }), + distinctUntilChanged(), + ); +} + +const observeIsSelfHosterStarter = (database: Database) => { + const license = observeLicense(database); + const isEnterpriseReady = observeConfigBooleanValue(database, 'BuildEnterpriseReady', false); + + return combineLatest([license, isEnterpriseReady]).pipe( + switchMap(([lic, isEnt]) => { + const isLicensed = lic?.IsLicensed === 'true'; + const isSelfHostedStarter = isEnt && !isLicensed; + + return of$(isSelfHostedStarter); + }), + ); +}; + diff --git a/app/screens/integration_selector/selected_option/index.test.tsx b/app/screens/integration_selector/selected_option/index.test.tsx index a352b518f..611d1df63 100644 --- a/app/screens/integration_selector/selected_option/index.test.tsx +++ b/app/screens/integration_selector/selected_option/index.test.tsx @@ -59,6 +59,7 @@ describe('components/integration_selector/selected_option', () => { email: 'true', first_name: 'true', mention_keys: 'false', + highlight_keys: '', push: 'mention', push_status: 'ooo', }, diff --git a/app/screens/integration_selector/selected_options/index.test.tsx b/app/screens/integration_selector/selected_options/index.test.tsx index a90244ec0..74870e5ac 100644 --- a/app/screens/integration_selector/selected_options/index.test.tsx +++ b/app/screens/integration_selector/selected_options/index.test.tsx @@ -39,6 +39,7 @@ describe('components/integration_selector/selected_options', () => { email: 'true', first_name: 'true', mention_keys: 'false', + highlight_keys: '', push: 'mention', push_status: 'ooo', }, diff --git a/app/utils/markdown/index.ts b/app/utils/markdown/index.ts index 045a5cfee..9611c3f6c 100644 --- a/app/utils/markdown/index.ts +++ b/app/utils/markdown/index.ts @@ -111,6 +111,10 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme: Theme) => { mention_highlight: { color: theme.mentionHighlightLink, }, + highlight_without_notification: { + backgroundColor: theme.mentionHighlightBg, + color: theme.mentionHighlightLink, + }, search_highlight: { backgroundColor: theme.mentionHighlightBg, color: theme.mentionHighlightLink, diff --git a/app/utils/user/index.ts b/app/utils/user/index.ts index 601e7d02d..958c3e0c1 100644 --- a/app/utils/user/index.ts +++ b/app/utils/user/index.ts @@ -334,6 +334,7 @@ export function getNotificationProps(user?: UserModel) { first_name: (!user || !user.firstName) ? 'false' : 'true', mark_unread: 'all', mention_keys: user ? `${user.username},@${user.username}` : '', + highlight_keys: '', push: 'mention', push_status: 'online', push_threads: 'all', diff --git a/package-lock.json b/package-lock.json index 3e2691022..342f5d19a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "mattermost-mobile", - "version": "2.10.0", + "version": "2.11.0", "hasInstallScript": true, "license": "Apache 2.0", "dependencies": { @@ -42,7 +42,7 @@ "@tsconfig/react-native": "3.0.2", "@voximplant/react-native-foreground-service": "3.0.2", "base-64": "1.0.0", - "commonmark": "npm:@mattermost/commonmark@0.30.1-1", + "commonmark": "npm:@mattermost/commonmark@0.30.1-2", "commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf", "deep-equal": "2.2.2", "deepmerge": "4.3.1", @@ -9688,9 +9688,9 @@ }, "node_modules/commonmark": { "name": "@mattermost/commonmark", - "version": "0.30.1-1", - "resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-1.tgz", - "integrity": "sha512-zOiOtPA5LjB1WCUVbaMt5KNuNdq/0NLpBOcQlGrKe+ftuJVhiis7pWEqS4agIBlpjhm7ssywwl7p1eurOjcm5A==", + "version": "0.30.1-2", + "resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-2.tgz", + "integrity": "sha512-r/xhNVt49pEFTjOgmKvjnPNM5RA8OztWsUn3CSQBcXiH2r36QipnR6qxU1hHo3XCteXkYDu9ypn+voA+jaN4Xg==", "dependencies": { "entities": "~3.0.1", "mdurl": "~1.0.1", @@ -23634,8 +23634,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "requires": { - } + "requires": {} }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -25489,8 +25488,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.3.5.tgz", "integrity": "sha512-REdUEsm/RA6lI1Rt4b009jvWn28f7H+e27gd4hlNk6zesIh/dlfiHwYfInW/vwbNFBdSPpvHy7Qi2mdcvrNqhg==", - "requires": { - } + "requires": {} }, "@mattermost/react-native-network-client": { "version": "1.4.1", @@ -25513,8 +25511,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@mattermost/react-native-turbo-log/-/react-native-turbo-log-0.2.3.tgz", "integrity": "sha512-usWyD8zVAHzrYqgPH1ne5I14gCOkhS2mefK58g5v4DewZfCm0/Uc0w8MRuPS/9jyOPPq1rUZj8U1AqKgEne9tQ==", - "requires": { - } + "requires": {} }, "@msgpack/msgpack": { "version": "2.8.0", @@ -25606,15 +25603,13 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-5.7.2.tgz", "integrity": "sha512-s8VAUG1Kvi+tEJkLHObmOJdXAL/uclnXJ/IdnJtx2fCKiWA3Ho0ln9gDQqCYHHHHu+sXk7wovsH/I2/AYy0brg==", - "requires": { - } + "requires": {} }, "@react-native-clipboard/clipboard": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.11.2.tgz", "integrity": "sha512-bHyZVW62TuleiZsXNHS1Pv16fWc0fh8O9WvBzl4h2fykqZRW9a+Pv/RGTH56E3X2PqzHP38K5go8zmCZUoIsoQ==", - "requires": { - } + "requires": {} }, "@react-native-community/cli": { "version": "10.2.4", @@ -27323,8 +27318,7 @@ "version": "9.4.1", "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-9.4.1.tgz", "integrity": "sha512-dAbY5mfw+6Kas/GJ6QX9AZyY+K+eq9ad4Su6utoph/nxyH3whp5cMSgRNgE2VhGQVRZ/OG0qq3IaD3+wzoqJXw==", - "requires": { - } + "requires": {} }, "@react-native-cookies/cookies": { "version": "6.2.1", @@ -27490,8 +27484,7 @@ "version": "1.3.18", "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.18.tgz", "integrity": "sha512-/0hwnJkrr415yP0Hf4PjUKgGyfshrvNUKFXN85Mrt1gY49hy9IwxZgrrxlh0THXkPeq8q4VWw44eHDfAcQf20Q==", - "requires": { - } + "requires": {} }, "@react-navigation/native": { "version": "6.1.7", @@ -27718,72 +27711,63 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@stream-io/flat-list-mvcp/-/flat-list-mvcp-0.10.3.tgz", "integrity": "sha512-2ZK8piYlEfKIPZrH8BpZz9uj8HZcUvMCV0X7qSLSAc/vhLOANBfR0SSn0OaWPbqb2mFGAd4FxmLSPp1zKEYuaw==", - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-remove-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-svg-dynamic-title": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-svg-em-dimensions": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-transform-react-native-svg": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.0.0.tgz", "integrity": "sha512-UKrY3860AQICgH7g+6h2zkoxeVEPLYwX/uAjmqo4PIq2FIHppwhIqZstIyTz0ZtlwreKR41O3W3BzsBBiJV2Aw==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-plugin-transform-svg-component": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", "dev": true, - "requires": { - } + "requires": {} }, "@svgr/babel-preset": { "version": "8.0.0", @@ -28590,8 +28574,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@voximplant/react-native-foreground-service/-/react-native-foreground-service-3.0.2.tgz", "integrity": "sha512-ZIyccOAXPqznA1PAVAYlKZ+GI7kXYCuYgH+gmAkhPouyJbkgrSXaCJJzQ+uBkPr4FBa/PuC/yjzK8vf6tJREQA==", - "requires": { - } + "requires": {} }, "@webassemblyjs/ast": { "version": "1.11.1", @@ -28759,8 +28742,7 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", "dev": true, - "requires": { - } + "requires": {} }, "@webpack-cli/info": { "version": "1.4.0", @@ -28776,8 +28758,7 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", "dev": true, - "requires": { - } + "requires": {} }, "@xtuc/ieee754": { "version": "1.2.0", @@ -28832,15 +28813,13 @@ "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", "dev": true, "peer": true, - "requires": { - } + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": { - } + "requires": {} }, "agent-base": { "version": "6.0.2", @@ -28896,8 +28875,7 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "peer": true, - "requires": { - } + "requires": {} }, "anser": { "version": "1.4.10", @@ -29175,8 +29153,7 @@ "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", - "requires": { - } + "requires": {} }, "babel-jest": { "version": "29.6.2", @@ -30106,9 +30083,9 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, "commonmark": { - "version": "npm:@mattermost/commonmark@0.30.1-1", - "resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-1.tgz", - "integrity": "sha512-zOiOtPA5LjB1WCUVbaMt5KNuNdq/0NLpBOcQlGrKe+ftuJVhiis7pWEqS4agIBlpjhm7ssywwl7p1eurOjcm5A==", + "version": "npm:@mattermost/commonmark@0.30.1-2", + "resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-2.tgz", + "integrity": "sha512-r/xhNVt49pEFTjOgmKvjnPNM5RA8OztWsUn3CSQBcXiH2r36QipnR6qxU1hHo3XCteXkYDu9ypn+voA+jaN4Xg==", "requires": { "entities": "~3.0.1", "mdurl": "~1.0.1", @@ -30377,8 +30354,7 @@ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", "dev": true, - "requires": { - } + "requires": {} }, "deep-equal": { "version": "2.2.2", @@ -31281,8 +31257,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", - "requires": { - } + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.7", @@ -31367,8 +31342,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", "dev": true, - "requires": { - } + "requires": {} }, "eslint-plugin-import": { "version": "2.28.0", @@ -31502,8 +31476,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "requires": { - } + "requires": {} }, "eslint-plugin-react-native": { "version": "4.0.0", @@ -33877,8 +33850,7 @@ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, - "requires": { - } + "requires": {} }, "jest-regex-util": { "version": "29.4.3", @@ -37106,8 +37078,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz", "integrity": "sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==", - "requires": { - } + "requires": {} }, "react-intl": { "version": "6.4.4", @@ -37428,8 +37399,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz", "integrity": "sha512-TE4Kiy7jUyv+hugxDxitzu38sW1NqjCk4uE5IgU2WevLv7sZacaBc6PZKOShNRPGirLl1NWkaG3LDEkdb9Um5g==", - "requires": { - } + "requires": {} }, "react-native-button": { "version": "3.1.0", @@ -37482,15 +37452,13 @@ "version": "1.6.4", "resolved": "https://registry.npmjs.org/react-native-create-thumbnail/-/react-native-create-thumbnail-1.6.4.tgz", "integrity": "sha512-JWuKXswDXtqUPfuqh6rjCVMvTSSG3kUtwvSK/YdaNU0i+nZKxeqHmt/CO2+TyI/WSUFynGVmWT1xOHhCZAFsRQ==", - "requires": { - } + "requires": {} }, "react-native-device-info": { "version": "10.8.0", "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.8.0.tgz", "integrity": "sha512-DE4/X82ZVhdcnR1Y21iTP46WSSJA/rHK3lmeqWfGGq1RKLwXTIdxmfbZZnYwryqJ+esrw2l4ND19qlgxDGby8A==", - "requires": { - } + "requires": {} }, "react-native-document-picker": { "version": "9.0.1", @@ -37527,22 +37495,19 @@ "version": "2.10.10", "resolved": "https://registry.npmjs.org/react-native-exception-handler/-/react-native-exception-handler-2.10.10.tgz", "integrity": "sha512-otAXGoZDl1689OoUJWN/rXxVbdoZ3xcmyF1uq/CsizdLwwyZqVGd6d+p/vbYvnF996FfEyAEBnHrdFxulTn51w==", - "requires": { - } + "requires": {} }, "react-native-fast-image": { "version": "8.6.3", "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz", "integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==", - "requires": { - } + "requires": {} }, "react-native-file-viewer": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/react-native-file-viewer/-/react-native-file-viewer-2.1.5.tgz", "integrity": "sha512-MGC6sx9jsqHdefhVQ6o0akdsPGpkXgiIbpygb2Sg4g4bh7v6K1cardLV1NwGB9A6u1yICOSDT/MOC//9Ez6EUg==", - "requires": { - } + "requires": {} }, "react-native-fs": { "version": "2.20.0", @@ -37581,22 +37546,19 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-2.0.3.tgz", "integrity": "sha512-7+qvcxXZts/hA+HOOIFyM1x9m9fn/TJVSTgXaoQ8uT4gLc97IMvqHQ559tDmnlth+hHMzd3HRMpmRLWoKPL0DA==", - "requires": { - } + "requires": {} }, "react-native-hw-keyboard-event": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/react-native-hw-keyboard-event/-/react-native-hw-keyboard-event-0.0.4.tgz", "integrity": "sha512-G8qp0nm17PHigLb/axgdF9xg51BKCG2p1AGeq//J/luLp5zNczIcQJh+nm02R1MeEUE3e53wqO4LMe0MV3raZg==", - "requires": { - } + "requires": {} }, "react-native-image-picker": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-5.6.1.tgz", "integrity": "sha512-LPPlgJi97EzCDY4NWp7z0oUWmCbagnB6HSoKcLJHJD/DaFYN/dJPrqjqKaqqw8K/5Ze6DIsNg9PZohjNEYQQWQ==", - "requires": { - } + "requires": {} }, "react-native-in-app-review": { "version": "4.3.3", @@ -37607,15 +37569,13 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/react-native-incall-manager/-/react-native-incall-manager-4.1.0.tgz", "integrity": "sha512-v1c+XOGu5VudY5//E3i5xiaRA9v6RvevMzZ4RumLqI+hte+4XslB2z6HSek2FF0EmAnY1rCn4ckiwgkTI1Tmtw==", - "requires": { - } + "requires": {} }, "react-native-iphone-x-helper": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz", "integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==", - "requires": { - } + "requires": {} }, "react-native-keyboard-aware-scroll-view": { "version": "0.9.5", @@ -37630,8 +37590,7 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/react-native-keyboard-tracking-view/-/react-native-keyboard-tracking-view-5.7.0.tgz", "integrity": "sha512-MDeEwAbn9LJDOfHq0QLCGaZirVLk2X/tHqkAqz3y6uxryTRdSl9PwleOVar5Jx2oAPEg4J9BXbUD1wwOOi+5Kg==", - "requires": { - } + "requires": {} }, "react-native-keychain": { "version": "8.1.2", @@ -37642,15 +37601,13 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.2.tgz", "integrity": "sha512-hgmCsgzd58WNcDCyPtKrvxsaoETjb/jLGxis/dmU3Aqm2u4ICIduj4ECjbil7B7pm9OnuTkmpwXu08XV2mpg8g==", - "requires": { - } + "requires": {} }, "react-native-localize": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.0.2.tgz", "integrity": "sha512-/l/oE1LVNgIRRhLbhmfFMHiWV0xhUn0A0iz1ytLVRYywL7FTp8Rx2vkJS/q/RpExDvV7yLw2493XZBYIM1dnLQ==", - "requires": { - } + "requires": {} }, "react-native-math-view": { "version": "3.9.5", @@ -37686,8 +37643,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-native-notifications/-/react-native-notifications-5.1.0.tgz", "integrity": "sha512-laqDSDlCvEASmJR6cXpqaryK855ejQd07vrfYERzhv68YDOoSkKy/URExRP4vAfAOVqHhix80tLbNUcfvZk2VQ==", - "requires": { - } + "requires": {} }, "react-native-permissions": { "version": "3.8.4", @@ -37728,8 +37684,7 @@ "version": "4.7.1", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.7.1.tgz", "integrity": "sha512-X2pJG2ttmAbiGlItWedvDkZg1T1ikmEDiz+7HsiIwAIm2UbFqlhqn+B1JF53mSxPzdNaDcCQVHRNPvj8oFu6Yg==", - "requires": { - } + "requires": {} }, "react-native-screens": { "version": "3.24.0", @@ -37762,8 +37717,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.3.1.tgz", "integrity": "sha512-mKOfBLIBFBcs9br1rlZDvxD5+mAl8Gfr5CounwJtxI6Z82rGrMO+Kgl9EIg3RMVf3G855a85YVqHJL2f5EDRlw==", - "requires": { - } + "requires": {} }, "react-native-svg": { "version": "13.11.0", @@ -39437,8 +39391,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", "dev": true, - "requires": { - } + "requires": {} }, "ts-jest": { "version": "29.1.1", @@ -39759,15 +39712,13 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.1.6.tgz", "integrity": "sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg==", - "requires": { - } + "requires": {} }, "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "requires": { - } + "requires": {} }, "utf8": { "version": "3.0.0", @@ -40100,8 +40051,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", - "requires": { - } + "requires": {} }, "xdate": { "version": "0.8.2", diff --git a/package.json b/package.json index 75c09c28b..bc6d9c070 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@tsconfig/react-native": "3.0.2", "@voximplant/react-native-foreground-service": "3.0.2", "base-64": "1.0.0", - "commonmark": "npm:@mattermost/commonmark@0.30.1-1", + "commonmark": "npm:@mattermost/commonmark@0.30.1-2", "commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf", "deep-equal": "2.2.2", "deepmerge": "4.3.1", diff --git a/test/test_helper.ts b/test/test_helper.ts index 4d5b9a0b6..0962b1e7d 100644 --- a/test/test_helper.ts +++ b/test/test_helper.ts @@ -396,6 +396,7 @@ class TestHelper { first_name: 'true', channel: 'true', mention_keys: '', + highlight_keys: '', }, last_picture_update: 0, position: '', diff --git a/types/api/license.d.ts b/types/api/license.d.ts index 72ad20f0c..2d517a565 100644 --- a/types/api/license.d.ts +++ b/types/api/license.d.ts @@ -28,6 +28,8 @@ interface ClientLicense { OpenId: string; RemoteClusterService: string; SAML: string; + SelfHostedProducts: string; SharedChannels: string; + SkuShortName: string; Users: string; } diff --git a/types/api/users.d.ts b/types/api/users.d.ts index c0d822392..7f6b615f7 100644 --- a/types/api/users.d.ts +++ b/types/api/users.d.ts @@ -13,6 +13,7 @@ type UserNotifyProps = { first_name: 'true' | 'false'; mark_unread?: 'all' | 'mention'; mention_keys: string; + highlight_keys: string; push: 'default' | 'all' | 'mention' | 'none'; push_status: 'ooo' | 'offline' | 'away' | 'dnd' | 'online'; user_id?: string; diff --git a/types/database/models/servers/user.ts b/types/database/models/servers/user.ts index 0ce24fb0b..604c32806 100644 --- a/types/database/models/servers/user.ts +++ b/types/database/models/servers/user.ts @@ -11,7 +11,7 @@ import type ThreadParticipantsModel from './thread_participant'; import type {Model} from '@nozbe/watermelondb'; import type {Associations} from '@nozbe/watermelondb/Model'; import type Query from '@nozbe/watermelondb/Query'; -import type {UserMentionKey} from '@typings/global/markdown'; +import type {HighlightWithoutNotificationKey, UserMentionKey} from '@typings/global/markdown'; /** * The User model represents the 'USER' table and its relationship to other @@ -113,6 +113,9 @@ declare class UserModel extends Model { /* user mentions keys always excluding @channel, @all, @here */ userMentionKeys: UserMentionKey[]; + /* user highlight keys are custom words that gets highlighted without notification */ + highlightKeys: HighlightWithoutNotificationKey[]; + /** termsOfServiceId : The id of the last accepted terms of service */ termsOfServiceId: string; diff --git a/types/global/markdown.ts b/types/global/markdown.ts index df1ab6946..0ef7db453 100644 --- a/types/global/markdown.ts +++ b/types/global/markdown.ts @@ -13,6 +13,10 @@ export type UserMentionKey = { caseSensitive?: boolean; }; +export type HighlightWithoutNotificationKey = { + key: string; +}; + export type MarkdownBlockStyles = { adjacentParagraph: ViewStyle; horizontalRule: ViewStyle;