forked from Ivasoft/mattermost-mobile
[MM-49540] Message Priority Phase 3 (#7142)
* Init * i18 and types * Acknowledge button, api * Ack button + display ackd users * Saves priority on draft and addresses some comments * Addresses review comments round 2 * Moves fetching userprofiles upon opening ACKs * Adds metadata column in drafts table + Addresses some more review comments. * Small refactor according to review comments * Addresses some review comments * Addresses some review comments * Uses local action when ACKing * Fixes first time selecting priority and other * Updates snapshots * Fixes i18n * Fixes ts errors --------- Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com> Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
@@ -155,3 +155,37 @@ export const removeDraft = async (serverUrl: string, channelId: string, rootId =
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export async function updateDraftPriority(serverUrl: string, channelId: string, rootId: string, postPriority: PostPriority, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const draft = await getDraft(database, channelId, rootId);
|
||||
if (!draft) {
|
||||
const newDraft: Draft = {
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
metadata: {
|
||||
priority: postPriority,
|
||||
},
|
||||
};
|
||||
|
||||
return operator.handleDraft({drafts: [newDraft], prepareRecordsOnly});
|
||||
}
|
||||
|
||||
draft.prepareUpdate((d) => {
|
||||
d.metadata = {
|
||||
...d.metadata,
|
||||
priority: postPriority,
|
||||
};
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft], 'updateDraftPriority');
|
||||
}
|
||||
|
||||
return {draft};
|
||||
} catch (error) {
|
||||
logError('Failed updateDraftPriority', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {fetchPostAuthors} from '@actions/remote/post';
|
||||
import {ActionType, Post} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
import {countUsersFromMentions, getPostById, prepareDeletePost, queryPostsById} from '@queries/servers/post';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
||||
import {generateId} from '@utils/general';
|
||||
@@ -249,6 +249,72 @@ export async function getPosts(serverUrl: string, ids: string[], sort?: Q.SortOr
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPostAcknowledgement(serverUrl: string, postId: string, userId: string, acknowledgedAt: number, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const post = await getPostById(database, postId);
|
||||
if (!post) {
|
||||
throw new Error('Post not found');
|
||||
}
|
||||
|
||||
// Check if the post has already been acknowledged by the user
|
||||
const isAckd = post.metadata?.acknowledgements?.find((a) => a.user_id === userId);
|
||||
if (isAckd) {
|
||||
return {error: false};
|
||||
}
|
||||
|
||||
const acknowledgements = [...(post.metadata?.acknowledgements || []), {
|
||||
user_id: userId,
|
||||
acknowledged_at: acknowledgedAt,
|
||||
post_id: postId,
|
||||
}];
|
||||
|
||||
const model = post.prepareUpdate((p) => {
|
||||
p.metadata = {
|
||||
...p.metadata,
|
||||
acknowledgements,
|
||||
};
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([model], 'addPostAcknowledgement');
|
||||
}
|
||||
|
||||
return {model};
|
||||
} catch (error) {
|
||||
logError('Failed addPostAcknowledgement', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function removePostAcknowledgement(serverUrl: string, postId: string, userId: string, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const post = await getPostById(database, postId);
|
||||
if (!post) {
|
||||
throw new Error('Post not found');
|
||||
}
|
||||
|
||||
const model = post.prepareUpdate((record) => {
|
||||
record.metadata = {
|
||||
...post.metadata,
|
||||
acknowledgements: post.metadata?.acknowledgements?.filter(
|
||||
(a) => a.user_id !== userId,
|
||||
) || [],
|
||||
};
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([model], 'removePostAcknowledgement');
|
||||
}
|
||||
|
||||
return {model};
|
||||
} catch (error) {
|
||||
logError('Failed removePostAcknowledgement', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePosts(serverUrl: string, postIds: string[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
@@ -276,3 +342,12 @@ export async function deletePosts(serverUrl: string, postIds: string[]) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export function getUsersCountFromMentions(serverUrl: string, mentions: string[]): Promise<number> {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
return countUsersFromMentions(database, mentions);
|
||||
} catch (error) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {markChannelAsUnread, updateLastPostAt} from '@actions/local/channel';
|
||||
import {removePost, storePostsForChannel} from '@actions/local/post';
|
||||
import {addPostAcknowledgement, removePost, removePostAcknowledgement, storePostsForChannel} from '@actions/local/post';
|
||||
import {addRecentReaction} from '@actions/local/reactions';
|
||||
import {createThreadFromNewPost} from '@actions/local/thread';
|
||||
import {ActionType, Events, General, Post, ServerErrors} from '@constants';
|
||||
@@ -23,6 +23,7 @@ import {getPostById, getRecentPostsInChannel} from '@queries/servers/post';
|
||||
import {getCurrentUserId, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
||||
import {queryAllUsers} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setFetchingThreadState} from '@store/fetching_thread_store';
|
||||
import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers';
|
||||
import {isServerError} from '@utils/errors';
|
||||
@@ -500,41 +501,32 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc
|
||||
}
|
||||
|
||||
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false): Promise<AuthorsRequest> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
const currentUserId = await getCurrentUserId(operator.database);
|
||||
const users = await queryAllUsers(operator.database).fetch();
|
||||
const existingUserIds = new Set<string>();
|
||||
const existingUserNames = new Set<string>();
|
||||
let excludeUsername;
|
||||
users.forEach((u) => {
|
||||
existingUserIds.add(u.id);
|
||||
existingUserNames.add(u.username);
|
||||
if (u.id === currentUserId) {
|
||||
excludeUsername = u.username;
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
const users = await queryAllUsers(database).fetch();
|
||||
const existingUserIds = new Set<string>();
|
||||
const existingUserNames = new Set<string>();
|
||||
let excludeUsername;
|
||||
users.forEach((u) => {
|
||||
existingUserIds.add(u.id);
|
||||
existingUserNames.add(u.username);
|
||||
if (u.id === currentUserId) {
|
||||
excludeUsername = u.username;
|
||||
}
|
||||
});
|
||||
|
||||
const usernamesToLoad = getNeededAtMentionedUsernames(existingUserNames, posts, excludeUsername);
|
||||
const userIdsToLoad = new Set<string>();
|
||||
for (const p of posts) {
|
||||
const {user_id} = p;
|
||||
if (user_id !== currentUserId) {
|
||||
userIdsToLoad.add(user_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const usernamesToLoad = getNeededAtMentionedUsernames(existingUserNames, posts, excludeUsername);
|
||||
const userIdsToLoad = new Set<string>();
|
||||
for (const p of posts) {
|
||||
const {user_id} = p;
|
||||
if (user_id !== currentUserId) {
|
||||
userIdsToLoad.add(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const promises: Array<Promise<UserProfile[]>> = [];
|
||||
if (userIdsToLoad.size) {
|
||||
promises.push(client.getProfilesByIds(Array.from(userIdsToLoad)));
|
||||
@@ -1130,3 +1122,38 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function acknowledgePost(serverUrl: string, postId: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
EphemeralStore.setAcknowledgingPost(postId);
|
||||
|
||||
const userId = await getCurrentUserId(database);
|
||||
const {acknowledged_at: acknowledgedAt} = await client.acknowledgePost(postId, userId);
|
||||
|
||||
return addPostAcknowledgement(serverUrl, postId, userId, acknowledgedAt, false);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.unsetAcknowledgingPost(postId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unacknowledgePost(serverUrl: string, postId: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
EphemeralStore.setUnacknowledgingPost(postId);
|
||||
const userId = await getCurrentUserId(database);
|
||||
await client.unacknowledgePost(postId, userId);
|
||||
|
||||
return removePostAcknowledgement(serverUrl, postId, userId, false);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
} finally {
|
||||
EphemeralStore.unsetUnacknowledgingPost(postId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import {handleChannelConvertedEvent, handleChannelCreatedEvent,
|
||||
handleUserRemovedFromChannelEvent} from './channel';
|
||||
import {handleGroupMemberAddEvent, handleGroupMemberDeleteEvent, handleGroupReceivedEvent, handleGroupTeamAssociatedEvent, handleGroupTeamDissociateEvent} from './group';
|
||||
import {handleOpenDialogEvent} from './integrations';
|
||||
import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
|
||||
import {handleNewPostEvent, handlePostAcknowledgementAdded, handlePostAcknowledgementRemoved, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
|
||||
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
|
||||
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
|
||||
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
@@ -177,6 +177,13 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
handlePostUnread(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_ACKNOWLEDGEMENT_ADDED:
|
||||
handlePostAcknowledgementAdded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.POST_ACKNOWLEDGEMENT_REMOVED:
|
||||
handlePostAcknowledgementRemoved(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.LEAVE_TEAM:
|
||||
handleLeaveTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel';
|
||||
import {markPostAsDeleted} from '@actions/local/post';
|
||||
import {addPostAcknowledgement, markPostAsDeleted, removePostAcknowledgement} from '@actions/local/post';
|
||||
import {createThreadFromNewPost, updateThread} from '@actions/local/thread';
|
||||
import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel';
|
||||
import {fetchPostAuthors, fetchPostById} from '@actions/remote/post';
|
||||
import {fetchThread} from '@actions/remote/thread';
|
||||
import {fetchMissingProfilesByIds} from '@actions/remote/user';
|
||||
import {ActionType, Events, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getChannelById, getMyChannel} from '@queries/servers/channel';
|
||||
@@ -326,3 +327,41 @@ export async function handlePostUnread(serverUrl: string, msg: WebSocketMessage)
|
||||
markChannelAsUnread(serverUrl, channelId, delta, mentions, lastViewedAt);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePostAcknowledgementAdded(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const acknowledgement = JSON.parse(msg.data.acknowledgement);
|
||||
const {user_id, post_id, acknowledged_at} = acknowledgement;
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
const currentUserId = getCurrentUserId(database);
|
||||
if (EphemeralStore.isAcknowledgingPost(post_id) && currentUserId === user_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
addPostAcknowledgement(serverUrl, post_id, user_id, acknowledged_at);
|
||||
fetchMissingProfilesByIds(serverUrl, [user_id]);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePostAcknowledgementRemoved(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const acknowledgement = JSON.parse(msg.data.acknowledgement);
|
||||
const {user_id, post_id} = acknowledgement;
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
const currentUserId = getCurrentUserId(database);
|
||||
if (EphemeralStore.isUnacknowledgingPost(post_id) && currentUserId === user_id) {
|
||||
return;
|
||||
}
|
||||
await removePostAcknowledgement(serverUrl, post_id, user_id);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface ClientPostsMix {
|
||||
searchPosts: (teamId: string, terms: string, isOrSearch: boolean) => Promise<PostResponse>;
|
||||
doPostAction: (postId: string, actionId: string, selectedOption?: string) => Promise<any>;
|
||||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
|
||||
acknowledgePost: (postId: string, userId: string) => Promise<PostAcknowledgement>;
|
||||
unacknowledgePost: (postId: string, userId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
@@ -240,6 +242,20 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
||||
{method: 'post', body: msg},
|
||||
);
|
||||
};
|
||||
|
||||
acknowledgePost = async (postId: string, userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/ack`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
unacknowledgePost = async (postId: string, userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/ack`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPosts;
|
||||
|
||||
@@ -39,11 +39,23 @@ const enhance = withObservables(['channel', 'showTeamName', 'shouldHighlightActi
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const myChannel = observeMyChannel(database, channel.id);
|
||||
|
||||
const hasDraft = shouldHighlightState ?
|
||||
queryDraft(database, channel.id).observeWithColumns(['message', 'files']).pipe(
|
||||
switchMap((draft) => of$(draft.length > 0)),
|
||||
distinctUntilChanged(),
|
||||
) : of$(false);
|
||||
const hasDraft = shouldHighlightState ? queryDraft(database, channel.id).observeWithColumns(['message', 'files', 'metadata']).pipe(
|
||||
switchMap((drafts) => {
|
||||
if (!drafts.length) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
const draft = drafts[0];
|
||||
const standardPriority = draft?.metadata?.priority?.priority === '';
|
||||
|
||||
if (!draft.message && !draft.files.length && standardPriority) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
return of$(true);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
) : of$(false);
|
||||
|
||||
const isActive = shouldHighlightActive ?
|
||||
observeCurrentChannelId(database).pipe(
|
||||
|
||||
@@ -27,7 +27,7 @@ const OptionType = {
|
||||
...TouchableOptionTypes,
|
||||
} as const;
|
||||
|
||||
type OptionType = typeof OptionType[keyof typeof OptionType];
|
||||
export type OptionType = typeof OptionType[keyof typeof OptionType];
|
||||
|
||||
export const ITEM_HEIGHT = 48;
|
||||
|
||||
@@ -108,6 +108,7 @@ export type OptionItemProps = {
|
||||
info?: string;
|
||||
inline?: boolean;
|
||||
label: string;
|
||||
labelContainerStyle?: StyleProp<ViewStyle>;
|
||||
onRemove?: () => void;
|
||||
optionDescriptionTextStyle?: StyleProp<TextStyle>;
|
||||
optionLabelTextStyle?: StyleProp<TextStyle>;
|
||||
@@ -130,6 +131,7 @@ const OptionItem = ({
|
||||
info,
|
||||
inline = false,
|
||||
label,
|
||||
labelContainerStyle,
|
||||
onRemove,
|
||||
optionDescriptionTextStyle,
|
||||
optionLabelTextStyle,
|
||||
@@ -238,7 +240,7 @@ const OptionItem = ({
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.labelContainer}>
|
||||
<View style={[styles.labelContainer, labelContainerStyle]}>
|
||||
{Boolean(icon) && (
|
||||
<View style={styles.iconContainer}>
|
||||
<OptionIcon
|
||||
|
||||
84
app/components/post_draft/draft_input/header.tsx
Normal file
84
app/components/post_draft/draft_input/header.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Platform, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import PostPriorityLabel from '@components/post_priority/post_priority_label';
|
||||
import {PostPriorityColors} from '@constants/post';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
postPriority: PostPriority;
|
||||
noMentionsError: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 12,
|
||||
gap: 7,
|
||||
},
|
||||
error: {
|
||||
color: PostPriorityColors.URGENT,
|
||||
},
|
||||
acknowledgements: {
|
||||
color: theme.onlineIndicator,
|
||||
},
|
||||
paddingTopStyle: {
|
||||
paddingTop: Platform.select({ios: 6, android: 8}),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function DraftInputHeader({
|
||||
postPriority,
|
||||
noMentionsError,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const hasLabels = postPriority.priority !== '' || postPriority.requested_ack;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={[style.container, hasLabels ? style.paddingTopStyle : undefined]}>
|
||||
{postPriority.priority && (
|
||||
<PostPriorityLabel label={postPriority.priority}/>
|
||||
)}
|
||||
{postPriority.requested_ack && (
|
||||
<>
|
||||
<CompassIcon
|
||||
color={theme.onlineIndicator}
|
||||
name='check-circle-outline'
|
||||
size={14}
|
||||
/>
|
||||
{!postPriority.priority && (
|
||||
<FormattedText
|
||||
id='requested_ack.title'
|
||||
defaultMessage='Request Acknowledgements'
|
||||
style={{color: theme.onlineIndicator}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{postPriority.persistent_notifications && (
|
||||
<>
|
||||
<CompassIcon
|
||||
color={PostPriorityColors.URGENT}
|
||||
name='bell-ring-outline'
|
||||
size={14}
|
||||
/>
|
||||
{noMentionsError && (
|
||||
<FormattedText
|
||||
id='persistent_notifications.error.no_mentions.title'
|
||||
defaultMessage='Recipients must be @mentioned'
|
||||
style={style.error}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef} from 'react';
|
||||
import React, {useCallback, useMemo, useRef} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {type LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
|
||||
import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import PostPriorityLabel from '@components/post_priority/post_priority_label';
|
||||
import {General} from '@constants';
|
||||
import {MENTIONS_REGEX} from '@constants/autocomplete';
|
||||
import {PostPriorityType} from '@constants/post';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {persistentNotificationsConfirmation} from '@utils/post';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import PostInput from '../post_input';
|
||||
@@ -15,18 +20,23 @@ import SendAction from '../send_action';
|
||||
import Typing from '../typing';
|
||||
import Uploads from '../uploads';
|
||||
|
||||
import Header from './header';
|
||||
|
||||
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
channelId: string;
|
||||
channelType?: ChannelType;
|
||||
rootId?: string;
|
||||
currentUserId: string;
|
||||
canShowPostPriority?: boolean;
|
||||
|
||||
// Post Props
|
||||
postPriority: PostPriorityData;
|
||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
||||
postPriority: PostPriority;
|
||||
updatePostPriority: (postPriority: PostPriority) => void;
|
||||
persistentNotificationInterval: number;
|
||||
persistentNotificationMaxRecipients: number;
|
||||
|
||||
// Cursor Position Handler
|
||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||
@@ -97,6 +107,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
export default function DraftInput({
|
||||
testID,
|
||||
channelId,
|
||||
channelType,
|
||||
currentUserId,
|
||||
canShowPostPriority,
|
||||
files,
|
||||
@@ -113,8 +124,12 @@ export default function DraftInput({
|
||||
updatePostInputTop,
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
persistentNotificationInterval,
|
||||
persistentNotificationMaxRecipients,
|
||||
setIsFocused,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
@@ -132,6 +147,31 @@ export default function DraftInput({
|
||||
const sendActionTestID = `${testID}.send_action`;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const persistenNotificationsEnabled = postPriority.persistent_notifications && postPriority.priority === PostPriorityType.URGENT;
|
||||
const {noMentionsError, mentionsList} = useMemo(() => {
|
||||
let error = false;
|
||||
let mentions: string[] = [];
|
||||
if (
|
||||
channelType !== General.DM_CHANNEL &&
|
||||
persistenNotificationsEnabled
|
||||
) {
|
||||
mentions = (value.match(MENTIONS_REGEX) || []);
|
||||
error = mentions.length === 0;
|
||||
}
|
||||
|
||||
return {noMentionsError: error, mentionsList: mentions};
|
||||
}, [channelType, persistenNotificationsEnabled, value]);
|
||||
|
||||
const handleSendMessage = useCallback(async () => {
|
||||
if (persistenNotificationsEnabled) {
|
||||
persistentNotificationsConfirmation(serverUrl, value, mentionsList, intl, sendMessage, persistentNotificationMaxRecipients, persistentNotificationInterval);
|
||||
} else {
|
||||
sendMessage();
|
||||
}
|
||||
}, [serverUrl, mentionsList, persistenNotificationsEnabled, persistentNotificationMaxRecipients, sendMessage, value]);
|
||||
|
||||
const sendActionDisabled = !canSend || noMentionsError;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typing
|
||||
@@ -156,11 +196,10 @@ export default function DraftInput({
|
||||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
>
|
||||
{Boolean(postPriority?.priority) && (
|
||||
<View style={style.postPriorityLabel}>
|
||||
<PostPriorityLabel label={postPriority!.priority}/>
|
||||
</View>
|
||||
)}
|
||||
<Header
|
||||
noMentionsError={noMentionsError}
|
||||
postPriority={postPriority}
|
||||
/>
|
||||
<PostInput
|
||||
testID={postInputTestID}
|
||||
channelId={channelId}
|
||||
@@ -171,7 +210,7 @@ export default function DraftInput({
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
addFiles={addFiles}
|
||||
sendMessage={sendMessage}
|
||||
sendMessage={handleSendMessage}
|
||||
inputRef={inputRef}
|
||||
setIsFocused={setIsFocused}
|
||||
/>
|
||||
@@ -196,8 +235,8 @@ export default function DraftInput({
|
||||
/>
|
||||
<SendAction
|
||||
testID={sendActionTestID}
|
||||
disabled={!canSend}
|
||||
sendMessage={sendMessage}
|
||||
disabled={sendActionDisabled}
|
||||
sendMessage={handleSendMessage}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General, Permissions} from '@constants';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {queryDraft} from '@queries/servers/drafts';
|
||||
import {queryDraft, observeFirstDraft} from '@queries/servers/drafts';
|
||||
import {observePermissionForChannel} from '@queries/servers/role';
|
||||
import {observeConfigBooleanValue, observeCurrentChannelId} from '@queries/servers/system';
|
||||
import {observeCurrentUser, observeUser} from '@queries/servers/user';
|
||||
@@ -18,7 +18,6 @@ import {isSystemAdmin, getUserIdFromChannelName} from '@utils/user';
|
||||
import PostDraft from './post_draft';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type DraftModel from '@typings/database/models/servers/draft';
|
||||
|
||||
type OwnProps = {
|
||||
channelId: string;
|
||||
@@ -26,8 +25,6 @@ type OwnProps = {
|
||||
rootId?: string;
|
||||
}
|
||||
|
||||
const observeFirst = (v: DraftModel[]) => v[0]?.observe() || of$(undefined);
|
||||
|
||||
const enhanced = withObservables(['channelId', 'rootId', 'channelIsArchived'], (ownProps: WithDatabaseArgs & OwnProps) => {
|
||||
const {database, rootId = ''} = ownProps;
|
||||
let channelId = of$(ownProps.channelId);
|
||||
@@ -36,8 +33,8 @@ const enhanced = withObservables(['channelId', 'rootId', 'channelIsArchived'], (
|
||||
}
|
||||
|
||||
const draft = channelId.pipe(
|
||||
switchMap((cId) => queryDraft(database, cId, rootId).observeWithColumns(['message', 'files']).pipe(
|
||||
switchMap(observeFirst),
|
||||
switchMap((cId) => queryDraft(database, cId, rootId).observeWithColumns(['message', 'files', 'metadata']).pipe(
|
||||
switchMap(observeFirstDraft),
|
||||
)),
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
|
||||
import {observeCanUploadFiles, observeIsPostPriorityEnabled, observeMaxFileCount} from '@queries/servers/system';
|
||||
import {observeIsPostPriorityEnabled} from '@queries/servers/post';
|
||||
import {observeCanUploadFiles, observeMaxFileCount} from '@queries/servers/system';
|
||||
|
||||
import QuickActions from './quick_actions';
|
||||
|
||||
|
||||
@@ -3,22 +3,21 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {Keyboard, StyleSheet} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import PostPriorityPicker, {COMPONENT_HEIGHT} from '@components/post_priority/post_priority_picker';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Screens} from '@constants';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
postPriority: PostPriorityData;
|
||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
||||
postPriority: PostPriority;
|
||||
updatePostPriority: (postPriority: PostPriority) => void;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
@@ -29,40 +28,34 @@ const style = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const POST_PRIORITY_PICKER_BUTTON = 'close-post-priority-picker';
|
||||
|
||||
export default function PostPriorityAction({
|
||||
testID,
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const theme = useTheme();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
|
||||
const handlePostPriorityPicker = useCallback((postPriorityData: PostPriorityData) => {
|
||||
updatePostPriority(postPriorityData);
|
||||
dismissBottomSheet();
|
||||
}, [updatePostPriority]);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
return (
|
||||
<PostPriorityPicker
|
||||
data={{
|
||||
priority: postPriority?.priority || '',
|
||||
}}
|
||||
onSubmit={handlePostPriorityPicker}
|
||||
/>
|
||||
);
|
||||
}, [handlePostPriorityPicker, postPriority]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
bottomSheet({
|
||||
title: intl.formatMessage({id: 'post_priority.picker.title', defaultMessage: 'Message priority'}),
|
||||
renderContent,
|
||||
snapPoints: [1, bottomSheetSnapPoint(1, COMPONENT_HEIGHT, bottom)],
|
||||
Keyboard.dismiss();
|
||||
|
||||
const title = isTablet ? intl.formatMessage({id: 'post_priority.picker.title', defaultMessage: 'Message priority'}) : '';
|
||||
|
||||
openAsBottomSheet({
|
||||
closeButtonId: POST_PRIORITY_PICKER_BUTTON,
|
||||
screen: Screens.POST_PRIORITY_PICKER,
|
||||
theme,
|
||||
closeButtonId: 'post-priority-close-id',
|
||||
title,
|
||||
props: {
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
closeButtonId: POST_PRIORITY_PICKER_BUTTON,
|
||||
},
|
||||
});
|
||||
}, [intl, renderContent, theme, bottom]);
|
||||
}, [intl, postPriority, updatePostPriority, theme]);
|
||||
|
||||
const iconName = 'alert-circle-outline';
|
||||
const iconColor = changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
||||
@@ -22,8 +22,8 @@ type Props = {
|
||||
value: string;
|
||||
updateValue: (value: string) => void;
|
||||
addFiles: (file: FileInfo[]) => void;
|
||||
postPriority: PostPriorityData;
|
||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
||||
postPriority: PostPriority;
|
||||
updatePostPriority: (postPriority: PostPriority) => void;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ import {General, Permissions} from '@constants';
|
||||
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {observeChannel, observeChannelInfo, observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observeFirstDraft, queryDraft} from '@queries/servers/drafts';
|
||||
import {observePermissionForChannel} from '@queries/servers/role';
|
||||
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
|
||||
import SendHandler from './send_handler';
|
||||
import SendHandler, {INITIAL_PRIORITY} from './send_handler';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
@@ -36,15 +37,28 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
||||
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const currentUser = currentUserId.pipe(
|
||||
switchMap((id) => observeUser(database, id),
|
||||
));
|
||||
switchMap((id) => observeUser(database, id)),
|
||||
);
|
||||
const userIsOutOfOffice = currentUser.pipe(
|
||||
switchMap((u) => of$(u?.status === General.OUT_OF_OFFICE)),
|
||||
);
|
||||
|
||||
const postPriority = queryDraft(database, channelId, rootId).observeWithColumns(['metadata']).pipe(
|
||||
switchMap(observeFirstDraft),
|
||||
switchMap((d) => {
|
||||
if (!d?.metadata?.priority) {
|
||||
return of$(INITIAL_PRIORITY);
|
||||
}
|
||||
|
||||
return of$(d.metadata.priority);
|
||||
}),
|
||||
);
|
||||
|
||||
const enableConfirmNotificationsToChannel = observeConfigBooleanValue(database, 'EnableConfirmNotificationsToChannel');
|
||||
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
|
||||
const maxMessageLength = observeConfigIntValue(database, 'MaxPostSize', MAX_MESSAGE_LENGTH_FALLBACK);
|
||||
const persistentNotificationInterval = observeConfigIntValue(database, 'PersistentNotificationInterval');
|
||||
const persistentNotificationMaxRecipients = observeConfigIntValue(database, 'PersistentNotificationMaxRecipients');
|
||||
|
||||
const useChannelMentions = combineLatest([channel, currentUser]).pipe(
|
||||
switchMap(([c, u]) => {
|
||||
@@ -57,6 +71,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
||||
);
|
||||
|
||||
const channelInfo = channel.pipe(switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$(undefined))));
|
||||
const channelType = channel.pipe(switchMap((c) => of$(c?.type)));
|
||||
const membersCount = channelInfo.pipe(
|
||||
switchMap((i) => (i ? of$(i.memberCount) : of$(0))),
|
||||
);
|
||||
@@ -64,6 +79,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
||||
const customEmojis = queryAllCustomEmojis(database).observe();
|
||||
|
||||
return {
|
||||
channelType,
|
||||
currentUserId,
|
||||
enableConfirmNotificationsToChannel,
|
||||
isTimezoneEnabled,
|
||||
@@ -72,6 +88,9 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
||||
userIsOutOfOffice,
|
||||
useChannelMentions,
|
||||
customEmojis,
|
||||
persistentNotificationInterval,
|
||||
persistentNotificationMaxRecipients,
|
||||
postPriority,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {updateDraftPriority} from '@actions/local/draft';
|
||||
import {getChannelTimezones} from '@actions/remote/channel';
|
||||
import {executeCommand, handleGotoLocation} from '@actions/remote/command';
|
||||
import {createPost} from '@actions/remote/post';
|
||||
@@ -28,6 +29,7 @@ import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji
|
||||
type Props = {
|
||||
testID?: string;
|
||||
channelId: string;
|
||||
channelType?: ChannelType;
|
||||
rootId: string;
|
||||
canShowPostPriority?: boolean;
|
||||
setIsFocused: (isFocused: boolean) => void;
|
||||
@@ -52,15 +54,21 @@ type Props = {
|
||||
updatePostInputTop: (top: number) => void;
|
||||
addFiles: (file: FileInfo[]) => void;
|
||||
uploadFileError: React.ReactNode;
|
||||
persistentNotificationInterval: number;
|
||||
persistentNotificationMaxRecipients: number;
|
||||
postPriority: PostPriority;
|
||||
}
|
||||
|
||||
const INITIAL_PRIORITY = {
|
||||
export const INITIAL_PRIORITY = {
|
||||
priority: PostPriorityType.STANDARD,
|
||||
requested_ack: false,
|
||||
persistent_notifications: false,
|
||||
};
|
||||
|
||||
export default function SendHandler({
|
||||
testID,
|
||||
channelId,
|
||||
channelType,
|
||||
currentUserId,
|
||||
enableConfirmNotificationsToChannel,
|
||||
files,
|
||||
@@ -81,13 +89,15 @@ export default function SendHandler({
|
||||
updateCursorPosition,
|
||||
updatePostInputTop,
|
||||
setIsFocused,
|
||||
persistentNotificationInterval,
|
||||
persistentNotificationMaxRecipients,
|
||||
postPriority,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const [channelTimezoneCount, setChannelTimezoneCount] = useState(0);
|
||||
const [sendingMessage, setSendingMessage] = useState(false);
|
||||
const [postPriority, setPostPriority] = useState<PostPriorityData>(INITIAL_PRIORITY);
|
||||
|
||||
const canSend = useCallback(() => {
|
||||
if (sendingMessage) {
|
||||
@@ -114,6 +124,10 @@ export default function SendHandler({
|
||||
setSendingMessage(false);
|
||||
}, [serverUrl, rootId, clearDraft]);
|
||||
|
||||
const handlePostPriority = useCallback((priority: PostPriority) => {
|
||||
updateDraftPriority(serverUrl, channelId, rootId, priority);
|
||||
}, [serverUrl, rootId]);
|
||||
|
||||
const doSubmitMessage = useCallback(() => {
|
||||
const postFiles = files.filter((f) => !f.failed);
|
||||
const post = {
|
||||
@@ -123,7 +137,11 @@ export default function SendHandler({
|
||||
message: value,
|
||||
} as Post;
|
||||
|
||||
if (Object.keys(postPriority).length) {
|
||||
if (!rootId && (
|
||||
postPriority.priority ||
|
||||
postPriority.requested_ack ||
|
||||
postPriority.persistent_notifications)
|
||||
) {
|
||||
post.metadata = {
|
||||
priority: postPriority,
|
||||
};
|
||||
@@ -133,7 +151,6 @@ export default function SendHandler({
|
||||
|
||||
clearDraft();
|
||||
setSendingMessage(false);
|
||||
setPostPriority(INITIAL_PRIORITY);
|
||||
DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL);
|
||||
}, [files, currentUserId, channelId, rootId, value, clearDraft, postPriority]);
|
||||
|
||||
@@ -253,6 +270,7 @@ export default function SendHandler({
|
||||
<DraftInput
|
||||
testID={testID}
|
||||
channelId={channelId}
|
||||
channelType={channelType}
|
||||
currentUserId={currentUserId}
|
||||
rootId={rootId}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
@@ -268,7 +286,9 @@ export default function SendHandler({
|
||||
maxMessageLength={maxMessageLength}
|
||||
updatePostInputTop={updatePostInputTop}
|
||||
postPriority={postPriority}
|
||||
updatePostPriority={setPostPriority}
|
||||
updatePostPriority={handlePostPriority}
|
||||
persistentNotificationInterval={persistentNotificationInterval}
|
||||
persistentNotificationMaxRecipients={persistentNotificationMaxRecipients}
|
||||
setIsFocused={setIsFocused}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observeSavedPostsByIds} from '@queries/servers/post';
|
||||
import {observeSavedPostsByIds, observeIsPostAcknowledgementsEnabled} from '@queries/servers/post';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
||||
@@ -30,6 +30,7 @@ const enhancedWithoutPosts = withObservables([], ({database}: WithDatabaseArgs)
|
||||
customEmojiNames: queryAllCustomEmojis(database).observeWithColumns(['name']).pipe(
|
||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||
),
|
||||
isPostAcknowledgementEnabled: observeIsPostAcknowledgementsEnabled(database),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, Text, TouchableOpacity, useWindowDimensions} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {acknowledgePost, unacknowledgePost} from '@actions/remote/post';
|
||||
import {fetchMissingProfilesByIds} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {moreThan5minAgo} from '@utils/post';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import UsersList from './users_list';
|
||||
import {USER_ROW_HEIGHT} from './users_list/user_list_item';
|
||||
|
||||
import type {BottomSheetProps} from '@gorhom/bottom-sheet';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
currentUserId: UserModel['id'];
|
||||
currentUserTimezone: UserModel['timezone'];
|
||||
hasReactions: boolean;
|
||||
location: string;
|
||||
post: PostModel;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
backgroundColor: changeOpacity(theme.onlineIndicator, 0.12),
|
||||
flexDirection: 'row',
|
||||
height: 32,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
containerActive: {
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
},
|
||||
text: {
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
color: theme.onlineIndicator,
|
||||
},
|
||||
textActive: {
|
||||
color: '#fff',
|
||||
},
|
||||
icon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
divider: {
|
||||
width: 1,
|
||||
height: 32,
|
||||
marginHorizontal: 8,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
},
|
||||
listHeaderText: {
|
||||
marginBottom: 12,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Acknowledgements = ({currentUserId, currentUserTimezone, hasReactions, location, post, theme}: Props) => {
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const serverUrl = useServerUrl();
|
||||
const {height} = useWindowDimensions();
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const isCurrentAuthor = post.userId === currentUserId;
|
||||
const acknowledgements = post.metadata?.acknowledgements || [];
|
||||
|
||||
const acknowledgedAt = useMemo(() => {
|
||||
if (acknowledgements.length > 0) {
|
||||
const ack = acknowledgements.find((item) => item.user_id === currentUserId);
|
||||
|
||||
if (ack) {
|
||||
return ack.acknowledged_at;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}, [acknowledgements]);
|
||||
|
||||
const handleOnPress = useCallback(() => {
|
||||
if ((acknowledgedAt && moreThan5minAgo(acknowledgedAt)) || isCurrentAuthor) {
|
||||
return;
|
||||
}
|
||||
if (acknowledgedAt) {
|
||||
unacknowledgePost(serverUrl, post.id);
|
||||
} else {
|
||||
acknowledgePost(serverUrl, post.id);
|
||||
}
|
||||
}, [acknowledgedAt, isCurrentAuthor, post.id, serverUrl]);
|
||||
|
||||
const handleOnLongPress = useCallback(async () => {
|
||||
if (!acknowledgements.length) {
|
||||
return;
|
||||
}
|
||||
const userAcknowledgements: Record<string, number> = {};
|
||||
const userIds: string[] = [];
|
||||
|
||||
acknowledgements.forEach((item) => {
|
||||
userAcknowledgements[item.user_id] = item.acknowledged_at;
|
||||
userIds.push(item.user_id);
|
||||
});
|
||||
|
||||
try {
|
||||
fetchMissingProfilesByIds(serverUrl, userIds);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
{!isTablet && (
|
||||
<FormattedText
|
||||
id='mobile.acknowledgements.header'
|
||||
defaultMessage={'Acknowledgements'}
|
||||
style={style.listHeaderText}
|
||||
/>
|
||||
)}
|
||||
<UsersList
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
userAcknowledgements={userAcknowledgements}
|
||||
userIds={userIds}
|
||||
timezone={currentUserTimezone || undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const snapPoint1 = bottomSheetSnapPoint(Math.min(userIds.length, 5), USER_ROW_HEIGHT, bottom) + TITLE_HEIGHT;
|
||||
const snapPoint2 = height * 0.8;
|
||||
const snapPoints: BottomSheetProps['snapPoints'] = [1, Math.min(snapPoint1, snapPoint2)];
|
||||
if (userIds.length > 5 && snapPoint1 < snapPoint2) {
|
||||
snapPoints.push(snapPoint2);
|
||||
}
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId: 'close-ack-users-list',
|
||||
renderContent,
|
||||
initialSnapIndex: 1,
|
||||
snapPoints,
|
||||
title: intl.formatMessage({id: 'mobile.acknowledgements.header', defaultMessage: 'Acknowledgements'}),
|
||||
theme,
|
||||
});
|
||||
}, [bottom, intl, isTablet, acknowledgements, theme, location, post.channelId, currentUserTimezone]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={handleOnPress}
|
||||
onLongPress={handleOnLongPress}
|
||||
style={[style.container, acknowledgedAt ? style.containerActive : undefined]}
|
||||
>
|
||||
<CompassIcon
|
||||
color={acknowledgedAt ? '#fff' : theme.onlineIndicator}
|
||||
name='check-circle-outline'
|
||||
size={24}
|
||||
style={style.icon}
|
||||
/>
|
||||
{isCurrentAuthor || acknowledgements.length ? (
|
||||
<Text style={[style.text, acknowledgedAt ? style.textActive : undefined]}>
|
||||
{acknowledgements.length}
|
||||
</Text>
|
||||
) : (
|
||||
<FormattedText
|
||||
id='post_priority.button.acknowledge'
|
||||
defaultMessage='Acknowledge'
|
||||
style={style.text}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{hasReactions && <View style={style.divider}/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Acknowledgements;
|
||||
29
app/components/post_list/post/body/acknowledgements/index.ts
Normal file
29
app/components/post_list/post/body/acknowledgements/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import compose from 'lodash/fp/compose';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import Acknowledgements from './acknowledgements';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], (ownProps: WithDatabaseArgs) => {
|
||||
const database = ownProps.database;
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
return {
|
||||
currentUserId: currentUser.pipe(switchMap((c) => of$(c?.id))),
|
||||
currentUserTimezone: currentUser.pipe(switchMap((c) => of$(c?.timezone))),
|
||||
};
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withDatabase,
|
||||
enhanced,
|
||||
)(Acknowledgements);
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {queryUsersById} from '@queries/servers/user';
|
||||
|
||||
import UsersList from './users_list';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
userIds: string[];
|
||||
};
|
||||
|
||||
const enhanced = withObservables(['userIds'], ({database, userIds}: Props) => {
|
||||
return {
|
||||
users: queryUsersById(database, userIds).observe(),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(UsersList));
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import FormattedRelativeTime from '@components/formatted_relative_time';
|
||||
import UserItem from '@components/user_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export const USER_ROW_HEIGHT = 60;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
paddingLeft: 0,
|
||||
height: USER_ROW_HEIGHT,
|
||||
},
|
||||
pictureContainer: {
|
||||
alignItems: 'flex-start',
|
||||
width: 40,
|
||||
},
|
||||
time: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 75),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
user: UserModel;
|
||||
userAcknowledgement: number;
|
||||
timezone?: UserTimezone;
|
||||
}
|
||||
|
||||
const UserListItem = ({
|
||||
channelId,
|
||||
location,
|
||||
timezone,
|
||||
user,
|
||||
userAcknowledgement,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const handleUserPress = useCallback(async (userProfile: UserProfile) => {
|
||||
if (userProfile) {
|
||||
await dismissBottomSheet(Screens.BOTTOM_SHEET);
|
||||
const screen = Screens.USER_PROFILE;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const closeButtonId = 'close-user-profile';
|
||||
const props = {closeButtonId, location, userId: userProfile.id, channelId};
|
||||
|
||||
Keyboard.dismiss();
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||
}
|
||||
}, [channelId, location]);
|
||||
|
||||
return (
|
||||
<UserItem
|
||||
FooterComponent={
|
||||
<FormattedRelativeTime
|
||||
value={userAcknowledgement}
|
||||
timezone={timezone}
|
||||
style={style.time}
|
||||
/>
|
||||
}
|
||||
containerStyle={style.container}
|
||||
onUserPress={handleUserPress}
|
||||
size={40}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserListItem;
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef} from 'react';
|
||||
import {FlatList} from 'react-native-gesture-handler';
|
||||
|
||||
import UserListItem from './user_list_item';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {ListRenderItemInfo} from 'react-native';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
location: string;
|
||||
users: UserModel[];
|
||||
userAcknowledgements: Record<string, number>;
|
||||
timezone?: UserTimezone;
|
||||
};
|
||||
|
||||
const UsersList = ({channelId, location, users, userAcknowledgements, timezone}: Props) => {
|
||||
const listRef = useRef<FlatList>(null);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<UserModel>) => (
|
||||
<UserListItem
|
||||
channelId={channelId}
|
||||
location={location}
|
||||
user={item}
|
||||
userAcknowledgement={userAcknowledgements[item.id]}
|
||||
timezone={timezone}
|
||||
/>
|
||||
), [channelId, location, timezone]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={users}
|
||||
ref={listRef}
|
||||
renderItem={renderItem}
|
||||
overScrollMode={'always'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersList;
|
||||
@@ -12,6 +12,7 @@ import {THREAD} from '@constants/screens';
|
||||
import {isEdited as postEdited, isPostFailed} from '@utils/post';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import Acknowledgements from './acknowledgements';
|
||||
import AddMembers from './add_members';
|
||||
import Content from './content';
|
||||
import Failed from './failed';
|
||||
@@ -33,6 +34,7 @@ type BodyProps = {
|
||||
isJumboEmoji: boolean;
|
||||
isLastReply?: boolean;
|
||||
isPendingOrFailed: boolean;
|
||||
isPostAcknowledgementEnabled?: boolean;
|
||||
isPostAddChannelMember: boolean;
|
||||
location: string;
|
||||
post: PostModel;
|
||||
@@ -43,6 +45,13 @@ type BodyProps = {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
ackAndReactionsContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignContent: 'flex-start',
|
||||
marginTop: 12,
|
||||
},
|
||||
messageBody: {
|
||||
paddingVertical: 2,
|
||||
flex: 1,
|
||||
@@ -76,7 +85,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
||||
const Body = ({
|
||||
appsEnabled, hasFiles, hasReactions, highlight, highlightReplyBar,
|
||||
isCRTEnabled, isEphemeral, isFirstReply, isJumboEmoji, isLastReply, isPendingOrFailed, isPostAddChannelMember,
|
||||
isCRTEnabled, isEphemeral, isFirstReply, isJumboEmoji, isLastReply, isPendingOrFailed, isPostAcknowledgementEnabled, isPostAddChannelMember,
|
||||
location, post, searchPatterns, showAddReaction, theme,
|
||||
}: BodyProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -158,6 +167,8 @@ const Body = ({
|
||||
);
|
||||
}
|
||||
|
||||
const acknowledgementsVisible = isPostAcknowledgementEnabled && post.metadata?.priority?.requested_ack;
|
||||
const reactionsVisible = hasReactions && showAddReaction;
|
||||
if (!hasBeenDeleted) {
|
||||
body = (
|
||||
<View style={style.messageBody}>
|
||||
@@ -180,13 +191,25 @@ const Body = ({
|
||||
isReplyPost={isReplyPost}
|
||||
/>
|
||||
}
|
||||
{hasReactions && showAddReaction &&
|
||||
<Reactions
|
||||
location={location}
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{(acknowledgementsVisible || reactionsVisible) && (
|
||||
<View style={style.ackAndReactionsContainer}>
|
||||
{acknowledgementsVisible && (
|
||||
<Acknowledgements
|
||||
hasReactions={hasReactions}
|
||||
location={location}
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{reactionsVisible && (
|
||||
<Reactions
|
||||
location={location}
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, TouchableOpacity, View} from 'react-native';
|
||||
import {Keyboard, TouchableOpacity} from 'react-native';
|
||||
|
||||
import {addReaction, removeReaction} from '@actions/remote/reactions';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -50,13 +50,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
paddingHorizontal: 6,
|
||||
width: 36,
|
||||
},
|
||||
reactionsContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignContent: 'flex-start',
|
||||
marginTop: 12,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -171,7 +164,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.reactionsContainer}>
|
||||
<>
|
||||
{
|
||||
Array.from(sortedReactions).map((r) => {
|
||||
const reaction = reactionsByName.get(r);
|
||||
@@ -189,7 +182,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
})
|
||||
}
|
||||
{addMoreReactions}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {Permissions, Preferences, Screens} from '@constants';
|
||||
import {queryFilesForPost} from '@queries/servers/file';
|
||||
import {observePost, observePostAuthor, queryPostsBetween} from '@queries/servers/post';
|
||||
import {observePost, observePostAuthor, queryPostsBetween, observeIsPostPriorityEnabled} from '@queries/servers/post';
|
||||
import {queryReactionsForPost} from '@queries/servers/reaction';
|
||||
import {observeCanManageChannelMembers, observePermissionForPost} from '@queries/servers/role';
|
||||
import {observeIsPostPriorityEnabled} from '@queries/servers/system';
|
||||
import {observeThreadById} from '@queries/servers/thread';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {areConsecutivePosts, isPostEphemeral} from '@utils/post';
|
||||
|
||||
@@ -51,6 +51,7 @@ type PostProps = {
|
||||
isCRTEnabled?: boolean;
|
||||
isEphemeral: boolean;
|
||||
isFirstReply?: boolean;
|
||||
isPostAcknowledgementEnabled?: boolean;
|
||||
isSaved?: boolean;
|
||||
isLastReply?: boolean;
|
||||
isPostAddChannelMember: boolean;
|
||||
@@ -109,7 +110,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
||||
const Post = ({
|
||||
appsEnabled, canDelete, currentUser, customEmojiNames, differentThreadSequence, hasFiles, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
|
||||
isCRTEnabled, isConsecutivePost, isEphemeral, isFirstReply, isSaved, isLastReply, isPostAddChannelMember, isPostPriorityEnabled,
|
||||
isCRTEnabled, isConsecutivePost, isEphemeral, isFirstReply, isSaved, isLastReply, isPostAcknowledgementEnabled, isPostAddChannelMember, isPostPriorityEnabled,
|
||||
location, post, rootId, hasReactions, searchPatterns, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style,
|
||||
testID, thread, previousPost,
|
||||
}: PostProps) => {
|
||||
@@ -312,6 +313,7 @@ const Post = ({
|
||||
isJumboEmoji={isJumboEmoji}
|
||||
isLastReply={isLastReply}
|
||||
isPendingOrFailed={isPendingOrFailed}
|
||||
isPostAcknowledgementEnabled={isPostAcknowledgementEnabled}
|
||||
isPostAddChannelMember={isPostAddChannelMember}
|
||||
location={location}
|
||||
post={post}
|
||||
|
||||
@@ -36,6 +36,7 @@ type Props = {
|
||||
highlightedId?: PostModel['id'];
|
||||
highlightPinnedOrSaved?: boolean;
|
||||
isCRTEnabled?: boolean;
|
||||
isPostAcknowledgementEnabled?: boolean;
|
||||
isTimezoneEnabled: boolean;
|
||||
lastViewedAt: number;
|
||||
location: string;
|
||||
@@ -97,6 +98,7 @@ const PostList = ({
|
||||
highlightedId,
|
||||
highlightPinnedOrSaved = true,
|
||||
isCRTEnabled,
|
||||
isPostAcknowledgementEnabled,
|
||||
isTimezoneEnabled,
|
||||
lastViewedAt,
|
||||
location,
|
||||
@@ -276,6 +278,7 @@ const PostList = ({
|
||||
appsEnabled,
|
||||
customEmojiNames,
|
||||
isCRTEnabled,
|
||||
isPostAcknowledgementEnabled,
|
||||
highlight: highlightedId === post.id,
|
||||
highlightPinnedOrSaved,
|
||||
isSaved: post.isSaved,
|
||||
@@ -294,7 +297,7 @@ const PostList = ({
|
||||
return (<Post {...postProps}/>);
|
||||
}
|
||||
}
|
||||
}, [appsEnabled, currentTimezone, customEmojiNames, highlightPinnedOrSaved, isCRTEnabled, isTimezoneEnabled, shouldRenderReplyButton, theme]);
|
||||
}, [appsEnabled, currentTimezone, customEmojiNames, highlightPinnedOrSaved, isCRTEnabled, isPostAcknowledgementEnabled, isTimezoneEnabled, shouldRenderReplyButton, theme]);
|
||||
|
||||
const scrollToIndex = useCallback((index: number, animated = true, applyOffset = true) => {
|
||||
listRef.current?.scrollToIndex({
|
||||
|
||||
@@ -35,7 +35,7 @@ const style = StyleSheet.create({
|
||||
});
|
||||
|
||||
type Props = {
|
||||
label: PostPriorityData['priority'];
|
||||
label: PostPriority['priority'];
|
||||
};
|
||||
|
||||
const PostPriorityLabel = ({label}: Props) => {
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {PostPriorityColors, PostPriorityType} from '@constants/post';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import PostPriorityPickerItem from './post_priority_picker_item';
|
||||
|
||||
type Props = {
|
||||
data: PostPriorityData;
|
||||
onSubmit: (data: PostPriorityData) => void;
|
||||
};
|
||||
|
||||
export const COMPONENT_HEIGHT = 200;
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 200,
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
betaContainer: {
|
||||
backgroundColor: PostPriorityColors.IMPORTANT,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
marginLeft: 8,
|
||||
},
|
||||
beta: {
|
||||
color: '#fff',
|
||||
...typography('Body', 25, 'SemiBold'),
|
||||
},
|
||||
|
||||
optionsContainer: {
|
||||
paddingVertical: 12,
|
||||
},
|
||||
}));
|
||||
|
||||
const PostPriorityPicker = ({data, onSubmit}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const style = getStyle(theme);
|
||||
|
||||
// For now, we just have one option but the spec suggest we have more in the next phase
|
||||
// const [data, setData] = React.useState<PostPriorityData>(defaultData);
|
||||
|
||||
const handleUpdatePriority = React.useCallback((priority: PostPriorityData['priority']) => {
|
||||
onSubmit({priority: priority || ''});
|
||||
}, [onSubmit]);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{!isTablet &&
|
||||
<View style={style.titleContainer}>
|
||||
<FormattedText
|
||||
id='post_priority.picker.title'
|
||||
defaultMessage='Message priority'
|
||||
style={style.title}
|
||||
/>
|
||||
<View style={style.betaContainer}>
|
||||
<FormattedText
|
||||
id='post_priority.picker.beta'
|
||||
defaultMessage='BETA'
|
||||
style={style.beta}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
<View style={style.optionsContainer}>
|
||||
<PostPriorityPickerItem
|
||||
action={handleUpdatePriority}
|
||||
icon='message-text-outline'
|
||||
label={intl.formatMessage({
|
||||
id: 'post_priority.picker.label.standard',
|
||||
defaultMessage: 'Standard',
|
||||
})}
|
||||
selected={data.priority === ''}
|
||||
value={PostPriorityType.STANDARD}
|
||||
/>
|
||||
<PostPriorityPickerItem
|
||||
action={handleUpdatePriority}
|
||||
icon='alert-circle-outline'
|
||||
iconColor={PostPriorityColors.IMPORTANT}
|
||||
label={intl.formatMessage({
|
||||
id: 'post_priority.picker.label.important',
|
||||
defaultMessage: 'Important',
|
||||
})}
|
||||
selected={data.priority === PostPriorityType.IMPORTANT}
|
||||
value={PostPriorityType.IMPORTANT}
|
||||
/>
|
||||
<PostPriorityPickerItem
|
||||
action={handleUpdatePriority}
|
||||
icon='alert-outline'
|
||||
iconColor={PostPriorityColors.URGENT}
|
||||
label={intl.formatMessage({
|
||||
id: 'post_priority.picker.label.urgent',
|
||||
defaultMessage: 'Urgent',
|
||||
})}
|
||||
selected={data.priority === PostPriorityType.URGENT}
|
||||
value={PostPriorityType.URGENT}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostPriorityPicker;
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import OptionItem, {type OptionItemProps} from '@components/option_item';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
optionLabelTextStyle: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
}));
|
||||
|
||||
const PostPriorityPickerItem = (props: Omit<OptionItemProps, 'type'>) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyle(theme);
|
||||
|
||||
const testID = `post_priority_picker_item.${props.value || 'standard'}`;
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
optionLabelTextStyle={style.optionLabelTextStyle}
|
||||
testID={testID}
|
||||
type='select'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostPriorityPickerItem;
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import React, {useCallback, useMemo, type ReactNode} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||
import {StyleSheet, Text, TouchableOpacity, View, type StyleProp, type ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
@@ -18,8 +18,11 @@ import {displayUsername, getUserCustomStatus, isBot, isCustomStatusExpired, isGu
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type AtMentionItemProps = {
|
||||
FooterComponent?: ReactNode;
|
||||
user: UserProfile | UserModel;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
currentUserId: string;
|
||||
size?: number;
|
||||
testID?: string;
|
||||
isCustomStatusEnabled: boolean;
|
||||
showBadges?: boolean;
|
||||
@@ -35,6 +38,13 @@ type AtMentionItemProps = {
|
||||
|
||||
const getThemedStyles = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
rowPicture: {
|
||||
marginRight: 10,
|
||||
marginLeft: 2,
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowFullname: {
|
||||
...typography('Body', 200),
|
||||
color: theme.centerChannelColor,
|
||||
@@ -56,6 +66,13 @@ const nonThemedStyles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
rowInfoBaseContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
rowInfoContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
@@ -71,8 +88,11 @@ const nonThemedStyles = StyleSheet.create({
|
||||
});
|
||||
|
||||
const UserItem = ({
|
||||
FooterComponent,
|
||||
user,
|
||||
containerStyle,
|
||||
currentUserId,
|
||||
size = 24,
|
||||
testID,
|
||||
isCustomStatusEnabled,
|
||||
showBadges = false,
|
||||
@@ -107,7 +127,7 @@ const UserItem = ({
|
||||
|
||||
const userItemTestId = `${testID}.${user?.id}`;
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
const containerViewStyle = useMemo(() => {
|
||||
return [
|
||||
nonThemedStyles.row,
|
||||
{
|
||||
@@ -133,68 +153,72 @@ const UserItem = ({
|
||||
>
|
||||
<View
|
||||
ref={viewRef}
|
||||
style={containerStyle}
|
||||
style={[containerViewStyle, containerStyle]}
|
||||
testID={userItemTestId}
|
||||
>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={24}
|
||||
size={size}
|
||||
showStatus={false}
|
||||
testID={`${userItemTestId}.profile_picture`}
|
||||
containerStyle={nonThemedStyles.profile}
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
testID={`${userItemTestId}.display_name`}
|
||||
>
|
||||
{nonBreakingString(displayName)}
|
||||
{Boolean(showTeammateDisplay) && (
|
||||
<View style={nonThemedStyles.rowInfoBaseContainer}>
|
||||
<View style={nonThemedStyles.rowInfoContainer}>
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
testID={`${userItemTestId}.username`}
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
testID={`${userItemTestId}.display_name`}
|
||||
>
|
||||
{nonBreakingString(` @${user!.username}`)}
|
||||
{nonBreakingString(displayName)}
|
||||
{Boolean(showTeammateDisplay) && (
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
testID={`${userItemTestId}.username`}
|
||||
>
|
||||
{nonBreakingString(` @${user!.username}`)}
|
||||
</Text>
|
||||
)}
|
||||
{Boolean(deleteAt) && (
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
testID={`${userItemTestId}.deactivated`}
|
||||
>
|
||||
{nonBreakingString(` ${intl.formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}`)}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{Boolean(deleteAt) && (
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
testID={`${userItemTestId}.deactivated`}
|
||||
>
|
||||
{nonBreakingString(` ${intl.formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}`)}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
{showBadges && bot && (
|
||||
<BotTag
|
||||
testID={`${userItemTestId}.bot.tag`}
|
||||
style={nonThemedStyles.tag}
|
||||
/>
|
||||
)}
|
||||
{showBadges && guest && (
|
||||
<GuestTag
|
||||
testID={`${userItemTestId}.guest.tag`}
|
||||
style={nonThemedStyles.tag}
|
||||
/>
|
||||
)}
|
||||
{Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && (
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus!}
|
||||
style={nonThemedStyles.icon}
|
||||
/>
|
||||
)}
|
||||
{shared && (
|
||||
<CompassIcon
|
||||
name={'circle-multiple-outline'}
|
||||
size={16}
|
||||
color={theme.centerChannelColor}
|
||||
style={nonThemedStyles.icon}
|
||||
/>
|
||||
)}
|
||||
<View style={nonThemedStyles.flex}/>
|
||||
{Boolean(rightDecorator) && rightDecorator}
|
||||
{showBadges && bot && (
|
||||
<BotTag
|
||||
testID={`${userItemTestId}.bot.tag`}
|
||||
style={nonThemedStyles.tag}
|
||||
/>
|
||||
)}
|
||||
{showBadges && guest && (
|
||||
<GuestTag
|
||||
testID={`${userItemTestId}.guest.tag`}
|
||||
style={nonThemedStyles.tag}
|
||||
/>
|
||||
)}
|
||||
{Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && (
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus!}
|
||||
style={nonThemedStyles.icon}
|
||||
/>
|
||||
)}
|
||||
{shared && (
|
||||
<CompassIcon
|
||||
name={'circle-multiple-outline'}
|
||||
size={16}
|
||||
color={theme.centerChannelColor}
|
||||
style={nonThemedStyles.icon}
|
||||
/>
|
||||
)}
|
||||
<View style={nonThemedStyles.flex}/>
|
||||
{Boolean(rightDecorator) && rightDecorator}
|
||||
</View>
|
||||
{FooterComponent}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -153,17 +153,20 @@ exports[`components/channel_list_row should show no results 1`] = `
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1"
|
||||
@@ -196,44 +199,61 @@ exports[`components/channel_list_row should show no results 1`] = `
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -418,17 +438,20 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1"
|
||||
@@ -461,44 +484,61 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -763,17 +803,20 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1"
|
||||
@@ -806,44 +849,61 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1060,17 +1120,20 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1"
|
||||
@@ -1103,44 +1166,61 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.1.1.display_name"
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -1230,17 +1310,20 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"paddingTop": 4,
|
||||
"paddingVertical": 8,
|
||||
},
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 20,
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.2.2"
|
||||
@@ -1273,44 +1356,61 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.2.2.display_name"
|
||||
>
|
||||
rocky
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"flex": 0,
|
||||
"flexShrink": 1,
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="create_direct_message.user_list.user_item.2.2.display_name"
|
||||
>
|
||||
rocky
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.32)"
|
||||
name="circle-outline"
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
32
app/constants/autocomplete.test.ts
Normal file
32
app/constants/autocomplete.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MENTIONS_REGEX} from './autocomplete';
|
||||
|
||||
describe('test regular expressions', () => {
|
||||
test.each([
|
||||
['test @mention', ['@mention']],
|
||||
['test @mention.', ['@mention']],
|
||||
['test @mention_', ['@mention_']],
|
||||
['test @user_.name', ['@user_.name']],
|
||||
['test @user_.name.', ['@user_.name']],
|
||||
['test@mention', null],
|
||||
['test @mention1 @mention2 mentions', ['@mention1', '@mention2']],
|
||||
['test @mention1 @mention2. Mentions...', ['@mention1', '@mention2']],
|
||||
|
||||
['where is @jessica.hyde?', ['@jessica.hyde']],
|
||||
['where is @jessica.hyde.', ['@jessica.hyde']],
|
||||
['test @user.name. @user2.name', ['@user.name', '@user2.name']],
|
||||
['test @user.name.@user2.name', ['@user.name', '@user2.name']],
|
||||
|
||||
['non latin @桜 mention', ['@桜']],
|
||||
['@γεια non latin', ['@γεια']],
|
||||
['non latin @γεια.', ['@γεια']],
|
||||
|
||||
// since word boundaries don't work with non latin characters
|
||||
// the following is a known bug
|
||||
['άντε@γεια.com', ['@γεια.com']],
|
||||
])('MENTIONS_REGEX %s => %s', (text, expected) => {
|
||||
expect(text.match(MENTIONS_REGEX)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,10 @@ export const ALL_SEARCH_FLAGS_REGEX = /\b\w+:/g;
|
||||
|
||||
export const CODE_REGEX = /(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)| *(`{3,}|~{3,})[ .]*(\S+)? *\n([\s\S]*?\s*)\3 *(?:\n+|$)/g;
|
||||
|
||||
export const MENTIONS_REGEX = /(?:\B|\b_+)@([\p{L}0-9.\-_]+)(?<![.])/gui;
|
||||
|
||||
export const SPECIAL_MENTIONS_REGEX = /(?:\B|\b_+)@(channel|all|here)(?!(\.|-|_)*[^\W_])/gi;
|
||||
|
||||
export const MAX_LIST_HEIGHT = 230;
|
||||
export const MAX_LIST_TABLET_DIFF = 90;
|
||||
|
||||
@@ -30,4 +34,6 @@ export default {
|
||||
CODE_REGEX,
|
||||
DATE_MENTION_SEARCH_REGEX,
|
||||
MAX_LIST_HEIGHT,
|
||||
MENTIONS_REGEX,
|
||||
SPECIAL_MENTIONS_REGEX,
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ export const ONBOARDING = 'Onboarding';
|
||||
export const PERMALINK = 'Permalink';
|
||||
export const PINNED_MESSAGES = 'PinnedMessages';
|
||||
export const POST_OPTIONS = 'PostOptions';
|
||||
export const POST_PRIORITY_PICKER = 'PostPriorityPicker';
|
||||
export const REACTIONS = 'Reactions';
|
||||
export const REVIEW_APP = 'ReviewApp';
|
||||
export const SAVED_MESSAGES = 'SavedMessages';
|
||||
@@ -111,6 +112,7 @@ export default {
|
||||
PERMALINK,
|
||||
PINNED_MESSAGES,
|
||||
POST_OPTIONS,
|
||||
POST_PRIORITY_PICKER,
|
||||
REACTIONS,
|
||||
REVIEW_APP,
|
||||
SAVED_MESSAGES,
|
||||
@@ -168,6 +170,7 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
|
||||
BOTTOM_SHEET,
|
||||
EMOJI_PICKER,
|
||||
POST_OPTIONS,
|
||||
POST_PRIORITY_PICKER,
|
||||
THREAD_OPTIONS,
|
||||
REACTIONS,
|
||||
USER_PROFILE,
|
||||
|
||||
@@ -5,6 +5,8 @@ import Calls from '@constants/calls';
|
||||
|
||||
const WebsocketEvents = {
|
||||
POSTED: 'posted',
|
||||
POST_ACKNOWLEDGEMENT_ADDED: 'post_acknowledgement_added',
|
||||
POST_ACKNOWLEDGEMENT_REMOVED: 'post_acknowledgement_removed',
|
||||
POST_EDITED: 'post_edited',
|
||||
POST_DELETED: 'post_deleted',
|
||||
POST_UNREAD: 'post_unread',
|
||||
|
||||
@@ -41,7 +41,12 @@ export const transformPostRecord = ({action, database, value}: TransformerArgs):
|
||||
post.updateAt = raw.update_at;
|
||||
post.isPinned = Boolean(raw.is_pinned);
|
||||
post.message = raw.message;
|
||||
post.metadata = raw.metadata && Object.keys(raw.metadata).length ? raw.metadata : null;
|
||||
|
||||
// When we extract the posts from the threads, we don't get the metadata
|
||||
// So, it might not be present in the raw post, so we use the one from the record
|
||||
const metadata = raw.metadata ?? post.metadata;
|
||||
post.metadata = metadata && Object.keys(metadata).length ? metadata : null;
|
||||
|
||||
post.userId = raw.user_id;
|
||||
post.originalId = raw.original_id;
|
||||
post.pendingPostId = raw.pending_post_id;
|
||||
@@ -132,6 +137,7 @@ export const transformFileRecord = ({action, database, value}: TransformerArgs):
|
||||
*/
|
||||
export const transformDraftRecord = ({action, database, value}: TransformerArgs): Promise<DraftModel> => {
|
||||
const emptyFileInfo: FileInfo[] = [];
|
||||
const emptyPostMetadata: PostMetadata = {};
|
||||
const raw = value.raw as Draft;
|
||||
|
||||
// We use the raw id as Draft is client side only and we would only be creating/deleting drafts
|
||||
@@ -141,6 +147,7 @@ export const transformDraftRecord = ({action, database, value}: TransformerArgs)
|
||||
draft.message = raw?.message ?? '';
|
||||
draft.channelId = raw?.channel_id ?? '';
|
||||
draft.files = raw?.files ?? emptyFileInfo;
|
||||
draft.metadata = raw?.metadata ?? emptyPostMetadata;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
|
||||
@@ -2,35 +2,29 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
import {MENTIONS_REGEX} from '@constants/autocomplete';
|
||||
|
||||
export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Post[], excludeUsername?: string) => {
|
||||
const usernamesToLoad = new Set<string>();
|
||||
|
||||
const pattern = /\B@(([a-z0-9_.-]*[a-z0-9_])[.-]*)/gi;
|
||||
|
||||
posts.forEach((p) => {
|
||||
let match;
|
||||
while ((match = pattern.exec(p.message)) !== null) {
|
||||
const lowercaseMatch1 = match[1].toLowerCase();
|
||||
const lowercaseMatch2 = match[2].toLowerCase();
|
||||
while ((match = MENTIONS_REGEX.exec(p.message)) !== null) {
|
||||
const lowercaseMatch = match[1].toLowerCase();
|
||||
|
||||
// match[1] is the matched mention including trailing punctuation
|
||||
// match[2] is the matched mention without trailing punctuation
|
||||
if (General.SPECIAL_MENTIONS.has(lowercaseMatch2)) {
|
||||
if (General.SPECIAL_MENTIONS.has(lowercaseMatch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lowercaseMatch1 === excludeUsername || lowercaseMatch2 === excludeUsername) {
|
||||
if (lowercaseMatch === excludeUsername) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (usernames.has(lowercaseMatch1) || usernames.has(lowercaseMatch2)) {
|
||||
if (usernames.has(lowercaseMatch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there's no trailing punctuation, this will only add 1 item to the set
|
||||
usernamesToLoad.add(lowercaseMatch1);
|
||||
usernamesToLoad.add(lowercaseMatch2);
|
||||
usernamesToLoad.add(lowercaseMatch);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import {of as of$} from 'rxjs';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
@@ -25,3 +26,7 @@ export const queryDraft = (database: Database, channelId: string, rootId = '') =
|
||||
Q.where('root_id', rootId),
|
||||
);
|
||||
};
|
||||
|
||||
export function observeFirstDraft(v: DraftModel[]) {
|
||||
return v[0]?.observe() || of$(undefined);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Model, Q, Query} from '@nozbe/watermelondb';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import {of as of$, combineLatestWith} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import {queryGroupsByNames} from './group';
|
||||
import {querySavedPostsPreferences} from './preference';
|
||||
import {observeUser} from './user';
|
||||
import {getConfigValue, observeConfigBooleanValue} from './system';
|
||||
import {queryUsersByUsername, observeUser, observeCurrentUser} from './user';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
||||
@@ -230,3 +232,52 @@ export const observeSavedPostsByIds = (database: Database, postIds: string[]) =>
|
||||
switchMap((prefs) => of$(new Set(prefs.map((p) => p.name)))),
|
||||
);
|
||||
};
|
||||
|
||||
export const getIsPostPriorityEnabled = async (database: Database) => {
|
||||
const featureFlag = await getConfigValue(database, 'FeatureFlagPostPriority');
|
||||
const cfg = await getConfigValue(database, 'PostPriority');
|
||||
return featureFlag === 'true' && cfg === 'true';
|
||||
};
|
||||
|
||||
export const getIsPostAcknowledgementsEnabled = async (database: Database) => {
|
||||
const cfg = await getConfigValue(database, 'PostAcknowledgements');
|
||||
return cfg === 'true';
|
||||
};
|
||||
|
||||
export const observeIsPostPriorityEnabled = (database: Database) => {
|
||||
const featureFlag = observeConfigBooleanValue(database, 'FeatureFlagPostPriority');
|
||||
const cfg = observeConfigBooleanValue(database, 'PostPriority');
|
||||
return featureFlag.pipe(
|
||||
combineLatestWith(cfg),
|
||||
switchMap(([ff, c]) => of$(ff && c)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeIsPostAcknowledgementsEnabled = (database: Database) => {
|
||||
return observeConfigBooleanValue(database, 'PostAcknowledgements');
|
||||
};
|
||||
|
||||
export const observePersistentNotificationsEnabled = (database: Database) => {
|
||||
const user = observeCurrentUser(database);
|
||||
const enabledForAll = observeConfigBooleanValue(database, 'AllowPersistentNotifications');
|
||||
const enabledForGuests = observeConfigBooleanValue(database, 'AllowPersistentNotificationsForGuests');
|
||||
return user.pipe(
|
||||
combineLatestWith(enabledForAll, enabledForGuests),
|
||||
switchMap(([u, forAll, forGuests]) => {
|
||||
if (u?.isGuest) {
|
||||
return of$(forAll && forGuests);
|
||||
}
|
||||
return of$(forAll);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export const countUsersFromMentions = async (database: Database, mentions: string[]) => {
|
||||
const groupsQuery = queryGroupsByNames(database, mentions).fetch();
|
||||
const usersQuery = queryUsersByUsername(database, mentions).fetchCount();
|
||||
const [groups, usersCount] = await Promise.all([groupsQuery, usersQuery]);
|
||||
return groups.reduce((acc, v) => acc + v.memberCount, usersCount);
|
||||
};
|
||||
|
||||
@@ -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 {Config, Preferences} from '@constants';
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
@@ -239,15 +239,6 @@ export const observeConfigIntValue = (database: Database, key: keyof ClientConfi
|
||||
);
|
||||
};
|
||||
|
||||
export const observeIsPostPriorityEnabled = (database: Database) => {
|
||||
const featureFlag = observeConfigValue(database, 'FeatureFlagPostPriority');
|
||||
const cfg = observeConfigValue(database, 'PostPriority');
|
||||
return combineLatest([featureFlag, cfg]).pipe(
|
||||
switchMap(([ff, c]) => of$(ff === Config.TRUE && c === Config.TRUE)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeLicense = (database: Database): Observable<ClientLicense | undefined> => {
|
||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.LICENSE).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: undefined}))),
|
||||
|
||||
@@ -176,6 +176,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.POST_OPTIONS:
|
||||
screen = withServerDatabase(require('@screens/post_options').default);
|
||||
break;
|
||||
case Screens.POST_PRIORITY_PICKER:
|
||||
screen = withServerDatabase(require('@screens/post_priority_picker').default);
|
||||
break;
|
||||
case Screens.REACTIONS:
|
||||
screen = withServerDatabase(require('@screens/reactions').default);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import OptionItem, {type OptionItemProps, type OptionType} from '@components/option_item';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
labelContainer: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
optionLabelText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = Omit<OptionItemProps, 'type'> & {
|
||||
type?: OptionType;
|
||||
}
|
||||
|
||||
const PickerOption = ({type, ...rest}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const testID = `post_priority_picker_item.${rest.value || 'standard'}`;
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
labelContainerStyle={style.labelContainer}
|
||||
optionLabelTextStyle={style.optionLabelText}
|
||||
testID={testID}
|
||||
type={type || 'select'}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PickerOption;
|
||||
90
app/screens/post_priority_picker/footer.tsx
Normal file
90
app/screens/post_priority_picker/footer.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BottomSheetFooter, type BottomSheetFooterProps} from '@gorhom/bottom-sheet';
|
||||
import React from 'react';
|
||||
import {Platform, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
export type Props = BottomSheetFooterProps & {
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
borderTopWidth: 1,
|
||||
paddingTop: 20,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
cancelButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: theme.buttonBg,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
applyButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
marginLeft: 8,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
applyButtonText: {
|
||||
color: theme.buttonColor,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const PostPriorityPickerFooter = ({onCancel, onSubmit, ...props}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
return (
|
||||
<BottomSheetFooter {...props}>
|
||||
<View
|
||||
style={[style.container, {
|
||||
paddingBottom: Platform.select({ios: (isTablet ? 20 : 32), android: 20}),
|
||||
}]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={onCancel}
|
||||
style={style.cancelButton}
|
||||
>
|
||||
<FormattedText
|
||||
id='post_priority.picker.cancel'
|
||||
defaultMessage='Cancel'
|
||||
style={style.cancelButtonText}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onSubmit}
|
||||
style={style.applyButton}
|
||||
>
|
||||
<FormattedText
|
||||
id='post_priority.picker.apply'
|
||||
defaultMessage='Apply'
|
||||
style={style.applyButtonText}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</BottomSheetFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostPriorityPickerFooter;
|
||||
20
app/screens/post_priority_picker/index.ts
Normal file
20
app/screens/post_priority_picker/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {observeIsPostAcknowledgementsEnabled, observePersistentNotificationsEnabled} from '@queries/servers/post';
|
||||
|
||||
import PostPriorityPicker from './post_priority_picker';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
const enhanced = withObservables([], ({database}: {database: Database}) => {
|
||||
return {
|
||||
isPostAcknowledgementEnabled: observeIsPostAcknowledgementsEnabled(database),
|
||||
isPersistenNotificationsEnabled: observePersistentNotificationsEnabled(database),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(PostPriorityPicker));
|
||||
229
app/screens/post_priority_picker/post_priority_picker.tsx
Normal file
229
app/screens/post_priority_picker/post_priority_picker.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Screens} from '@constants';
|
||||
import {PostPriorityColors, PostPriorityType} from '@constants/post';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import BottomSheet from '@screens/bottom_sheet';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import PickerOption from './components/picker_option';
|
||||
import Footer from './footer';
|
||||
import {labels} from './utils';
|
||||
|
||||
import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
isPostAcknowledgementEnabled: boolean;
|
||||
isPersistenNotificationsEnabled: boolean;
|
||||
postPriority: PostPriority;
|
||||
updatePostPriority: (data: PostPriority) => void;
|
||||
closeButtonId: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
betaContainer: {
|
||||
backgroundColor: PostPriorityColors.IMPORTANT,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
marginLeft: 8,
|
||||
},
|
||||
beta: {
|
||||
color: '#fff',
|
||||
...typography('Body', 25, 'SemiBold'),
|
||||
},
|
||||
|
||||
optionsContainer: {
|
||||
paddingTop: 12,
|
||||
},
|
||||
|
||||
optionsSeparator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
height: 1,
|
||||
},
|
||||
|
||||
toggleOptionContainer: {
|
||||
marginTop: 16,
|
||||
},
|
||||
}));
|
||||
|
||||
const PostPriorityPicker = ({
|
||||
componentId,
|
||||
isPostAcknowledgementEnabled,
|
||||
isPersistenNotificationsEnabled,
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
closeButtonId,
|
||||
}: Props) => {
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const theme = useTheme();
|
||||
const [data, setData] = useState<PostPriority>(postPriority);
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const closeBottomSheet = useCallback(() => {
|
||||
return dismissBottomSheet(Screens.POST_PRIORITY_PICKER);
|
||||
}, []);
|
||||
|
||||
useNavButtonPressed(closeButtonId, componentId, closeBottomSheet, []);
|
||||
useAndroidHardwareBackHandler(componentId, closeBottomSheet);
|
||||
|
||||
const displayPersistentNotifications = isPersistenNotificationsEnabled && data.priority === PostPriorityType.URGENT;
|
||||
|
||||
const snapPoints = useMemo(() => {
|
||||
let COMPONENT_HEIGHT = 280;
|
||||
|
||||
if (isPostAcknowledgementEnabled) {
|
||||
COMPONENT_HEIGHT += 75;
|
||||
|
||||
if (displayPersistentNotifications) {
|
||||
COMPONENT_HEIGHT += 75;
|
||||
}
|
||||
}
|
||||
|
||||
return [1, bottomSheetSnapPoint(1, COMPONENT_HEIGHT, bottom)];
|
||||
}, [displayPersistentNotifications, isPostAcknowledgementEnabled, bottom]);
|
||||
|
||||
const handleUpdatePriority = useCallback((priority: PostPriority['priority']) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
priority,
|
||||
persistent_notifications: undefined, // Uncheck if checked already
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleUpdateRequestedAck = useCallback((requested_ack: boolean) => {
|
||||
setData((prevData) => ({...prevData, requested_ack}));
|
||||
}, [data]);
|
||||
|
||||
const handleUpdatePersistentNotifications = useCallback((persistent_notifications: boolean) => {
|
||||
setData((prevData) => ({...prevData, persistent_notifications}));
|
||||
}, [data]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
updatePostPriority(data);
|
||||
closeBottomSheet();
|
||||
}, [data]);
|
||||
|
||||
const renderContent = () => (
|
||||
<View style={style.container}>
|
||||
{!isTablet &&
|
||||
<View style={style.titleContainer}>
|
||||
<FormattedText
|
||||
id='post_priority.picker.title'
|
||||
defaultMessage='Message priority'
|
||||
style={style.title}
|
||||
/>
|
||||
<View style={style.betaContainer}>
|
||||
<FormattedText
|
||||
id='post_priority.picker.beta'
|
||||
defaultMessage='BETA'
|
||||
style={style.beta}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
<View style={style.optionsContainer}>
|
||||
<PickerOption
|
||||
action={handleUpdatePriority}
|
||||
icon='message-text-outline'
|
||||
label={intl.formatMessage(labels.standard.label)}
|
||||
selected={data.priority === ''}
|
||||
value={PostPriorityType.STANDARD}
|
||||
/>
|
||||
<PickerOption
|
||||
action={handleUpdatePriority}
|
||||
icon='alert-circle-outline'
|
||||
iconColor={PostPriorityColors.IMPORTANT}
|
||||
label={intl.formatMessage(labels.important.label)}
|
||||
selected={data.priority === PostPriorityType.IMPORTANT}
|
||||
value={PostPriorityType.IMPORTANT}
|
||||
/>
|
||||
<PickerOption
|
||||
action={handleUpdatePriority}
|
||||
icon='alert-outline'
|
||||
iconColor={PostPriorityColors.URGENT}
|
||||
label={intl.formatMessage(labels.urgent.label)}
|
||||
selected={data.priority === PostPriorityType.URGENT}
|
||||
value={PostPriorityType.URGENT}
|
||||
/>
|
||||
{(isPostAcknowledgementEnabled) && (
|
||||
<>
|
||||
<View style={style.optionsSeparator}/>
|
||||
<View style={style.toggleOptionContainer}>
|
||||
<PickerOption
|
||||
action={handleUpdateRequestedAck}
|
||||
label={intl.formatMessage(labels.requestAck.label)}
|
||||
description={intl.formatMessage(labels.requestAck.description)}
|
||||
icon='check-circle-outline'
|
||||
type='toggle'
|
||||
selected={data.requested_ack}
|
||||
/>
|
||||
</View>
|
||||
{displayPersistentNotifications && (
|
||||
<View style={style.toggleOptionContainer}>
|
||||
<PickerOption
|
||||
action={handleUpdatePersistentNotifications}
|
||||
label={intl.formatMessage(labels.persistentNotifications.label)}
|
||||
description={intl.formatMessage(labels.persistentNotifications.description)}
|
||||
icon='bell-ring-outline'
|
||||
type='toggle'
|
||||
selected={data.persistent_notifications}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderFooter = (props: BottomSheetFooterProps) => (
|
||||
<Footer
|
||||
{...props}
|
||||
onCancel={closeBottomSheet}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
renderContent={renderContent}
|
||||
closeButtonId={closeButtonId}
|
||||
componentId={Screens.POST_PRIORITY_PICKER}
|
||||
footerComponent={renderFooter}
|
||||
initialSnapIndex={1}
|
||||
snapPoints={snapPoints}
|
||||
testID='post_options'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostPriorityPicker;
|
||||
45
app/screens/post_priority_picker/utils.ts
Normal file
45
app/screens/post_priority_picker/utils.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {t} from '@app/i18n';
|
||||
|
||||
export const labels = {
|
||||
standard: {
|
||||
label: {
|
||||
id: t('post_priority.picker.label.standard'),
|
||||
defaultMessage: 'Standard',
|
||||
},
|
||||
},
|
||||
urgent: {
|
||||
label: {
|
||||
id: t('post_priority.picker.label.urgent'),
|
||||
defaultMessage: 'Urgent',
|
||||
},
|
||||
},
|
||||
important: {
|
||||
label: {
|
||||
id: t('post_priority.picker.label.important'),
|
||||
defaultMessage: 'Important',
|
||||
},
|
||||
},
|
||||
requestAck: {
|
||||
label: {
|
||||
id: t('post_priority.picker.label.request_ack'),
|
||||
defaultMessage: 'Request acknowledgement',
|
||||
},
|
||||
description: {
|
||||
id: t('post_priority.picker.label.request_ack.description'),
|
||||
defaultMessage: 'An acknowledgement button appears with your message.',
|
||||
},
|
||||
},
|
||||
persistentNotifications: {
|
||||
label: {
|
||||
id: t('post_priority.picker.label.persistent_notifications'),
|
||||
defaultMessage: 'Send persistent notifications',
|
||||
},
|
||||
description: {
|
||||
id: t('post_priority.picker.label.persistent_notifications.description'),
|
||||
defaultMessage: 'Recipients are notified every five minutes until they acknowledge or reply.',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -28,6 +28,8 @@ class EphemeralStore {
|
||||
private archivingChannels = new Set<string>();
|
||||
private convertingChannels = new Set<string>();
|
||||
private switchingToChannel = new Set<string>();
|
||||
private acknowledgingPost = new Set<string>();
|
||||
private unacknowledgingPost = new Set<string>();
|
||||
private currentThreadId = '';
|
||||
private notificationTapped = false;
|
||||
private enablingCRT = false;
|
||||
@@ -219,6 +221,30 @@ class EphemeralStore {
|
||||
wasNotificationTapped = () => {
|
||||
return this.notificationTapped;
|
||||
};
|
||||
|
||||
setAcknowledgingPost = (postId: string) => {
|
||||
this.acknowledgingPost.add(postId);
|
||||
};
|
||||
|
||||
unsetAcknowledgingPost = (postId: string) => {
|
||||
this.acknowledgingPost.delete(postId);
|
||||
};
|
||||
|
||||
isAcknowledgingPost = (postId: string) => {
|
||||
return this.acknowledgingPost.has(postId);
|
||||
};
|
||||
|
||||
setUnacknowledgingPost = (postId: string) => {
|
||||
this.unacknowledgingPost.add(postId);
|
||||
};
|
||||
|
||||
unsetUnacknowledgingPost = (postId: string) => {
|
||||
this.unacknowledgingPost.delete(postId);
|
||||
};
|
||||
|
||||
isUnacknowledgingPost = (postId: string) => {
|
||||
return this.unacknowledgingPost.has(postId);
|
||||
};
|
||||
}
|
||||
|
||||
export default new EphemeralStore();
|
||||
|
||||
52
app/utils/post/index.test.ts
Normal file
52
app/utils/post/index.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as utils from './index';
|
||||
|
||||
describe('post utils', () => {
|
||||
test.each([
|
||||
['@here where is Jessica Hyde', true],
|
||||
['@all where is Jessica Hyde', true],
|
||||
['@channel where is Jessica Hyde', true],
|
||||
|
||||
['where is Jessica Hyde @here', true],
|
||||
['where is Jessica Hyde @all', true],
|
||||
['where is Jessica Hyde @channel', true],
|
||||
|
||||
['where is Jessica @here Hyde', true],
|
||||
['where is Jessica @all Hyde', true],
|
||||
['where is Jessica @channel Hyde', true],
|
||||
|
||||
['where is Jessica Hyde\n@here', true],
|
||||
['where is Jessica Hyde\n@all', true],
|
||||
['where is Jessica Hyde\n@channel', true],
|
||||
|
||||
['where is Jessica\n@here Hyde', true],
|
||||
['where is Jessica\n@all Hyde', true],
|
||||
['where is Jessica\n@channel Hyde', true],
|
||||
|
||||
['where is Jessica Hyde @her', false],
|
||||
['where is Jessica Hyde @al', false],
|
||||
['where is Jessica Hyde @chann', false],
|
||||
|
||||
['where is Jessica Hyde@here', false],
|
||||
['where is Jessica Hyde@all', false],
|
||||
['where is Jessica Hyde@channel', false],
|
||||
|
||||
['where is Jessica @hereHyde', false],
|
||||
['where is Jessica @allHyde', false],
|
||||
['where is Jessica @channelHyde', false],
|
||||
|
||||
['@herewhere is Jessica Hyde@here', false],
|
||||
['@allwhere is Jessica Hyde@all', false],
|
||||
['@channelwhere is Jessica Hyde@channel', false],
|
||||
|
||||
['where is Jessica Hyde here', false],
|
||||
['where is Jessica Hyde all', false],
|
||||
['where is Jessica Hyde channel', false],
|
||||
|
||||
['where is Jessica Hyde', false],
|
||||
])('hasSpecialMentions: %s => %s', (message, expected) => {
|
||||
expect(utils.hasSpecialMentions(message)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert, type AlertButton} from 'react-native';
|
||||
|
||||
import {getUsersCountFromMentions} from '@actions/local/post';
|
||||
import {Post} from '@constants';
|
||||
import {SPECIAL_MENTIONS_REGEX} from '@constants/autocomplete';
|
||||
import {POST_TIME_TO_FAIL} from '@constants/post';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export function areConsecutivePosts(post: PostModel, previousPost: PostModel) {
|
||||
let consecutive = false;
|
||||
@@ -85,3 +91,92 @@ export const getLastFetchedAtFromPosts = (posts?: Post[]) => {
|
||||
return Math.max(maxTimestamp, timestamp);
|
||||
}, 0) || 0;
|
||||
};
|
||||
|
||||
export const moreThan5minAgo = (time: number) => {
|
||||
return Date.now() - time > toMilliseconds({minutes: 5});
|
||||
};
|
||||
|
||||
export function hasSpecialMentions(message: string): boolean {
|
||||
const result = SPECIAL_MENTIONS_REGEX.test(message);
|
||||
|
||||
// https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-results
|
||||
SPECIAL_MENTIONS_REGEX.lastIndex = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function persistentNotificationsConfirmation(serverUrl: string, value: string, mentionsList: string[], intl: IntlShape, sendMessage: () => void, persistentNotificationMaxRecipients: number, persistentNotificationInterval: number) {
|
||||
let title = '';
|
||||
let description = '';
|
||||
let buttons: AlertButton[] = [{
|
||||
text: intl.formatMessage({
|
||||
id: 'persistent_notifications.error.okay',
|
||||
defaultMessage: 'Okay',
|
||||
}),
|
||||
style: 'cancel',
|
||||
}];
|
||||
|
||||
if (hasSpecialMentions(value)) {
|
||||
description = intl.formatMessage({
|
||||
id: 'persistent_notifications.error.special_mentions',
|
||||
defaultMessage: 'Cannot use @channel, @all or @here to mention recipients of persistent notifications.',
|
||||
});
|
||||
} else {
|
||||
// removes the @ from the mention
|
||||
const formattedMentionsList = mentionsList.map((mention) => mention.slice(1));
|
||||
const usersCount = await getUsersCountFromMentions(serverUrl, formattedMentionsList);
|
||||
if (usersCount > persistentNotificationMaxRecipients) {
|
||||
title = intl.formatMessage({
|
||||
id: 'persistent_notifications.error.max_recipients.title',
|
||||
defaultMessage: 'Too many recipients',
|
||||
});
|
||||
description = intl.formatMessage({
|
||||
id: 'persistent_notifications.error.max_recipients.description',
|
||||
defaultMessage: 'You can send persistent notifications to a maximum of {max} recipients. There are {count} recipients mentioned in your message. You’ll need to change who you’ve mentioned before you can send.',
|
||||
}, {
|
||||
max: persistentNotificationMaxRecipients,
|
||||
count: mentionsList.length,
|
||||
});
|
||||
} else if (usersCount === 0) {
|
||||
title = intl.formatMessage({
|
||||
id: 'persistent_notifications.error.no_mentions.title',
|
||||
defaultMessage: 'Recipients must be @mentioned',
|
||||
});
|
||||
description = intl.formatMessage({
|
||||
id: 'persistent_notifications.error.no_mentions.description',
|
||||
defaultMessage: 'There are no recipients mentioned in your message. You’ll need add mentions to be able to send persistent notifications.',
|
||||
});
|
||||
} else {
|
||||
title = intl.formatMessage({
|
||||
id: 'persistent_notifications.confirm.title',
|
||||
defaultMessage: 'Send persistent notifications',
|
||||
});
|
||||
description = intl.formatMessage({
|
||||
id: 'persistent_notifications.confirm.description',
|
||||
defaultMessage: '@mentioned recipients will be notified every {interval, plural, one {minute} other {{interval} minutes}} until they’ve acknowledged or replied to the message.',
|
||||
}, {
|
||||
interval: persistentNotificationInterval,
|
||||
});
|
||||
|
||||
buttons = [{
|
||||
text: intl.formatMessage({
|
||||
id: 'persistent_notifications.confirm.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'persistent_notifications.confirm.send',
|
||||
defaultMessage: 'Send',
|
||||
}),
|
||||
onPress: sendMessage,
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
description,
|
||||
buttons,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -408,6 +408,7 @@
|
||||
"mentions.empty.title": "No Mentions yet",
|
||||
"mobile.about.appVersion": "App Version: {version} (Build {number})",
|
||||
"mobile.account.settings.save": "Save",
|
||||
"mobile.acknowledgements.header": "Acknowledgements",
|
||||
"mobile.action_menu.select": "Select an option",
|
||||
"mobile.add_team.create_team": "Create a new team",
|
||||
"mobile.add_team.join_team": "Join Another Team",
|
||||
@@ -795,6 +796,16 @@
|
||||
"permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?",
|
||||
"permalink.show_dialog_warn.join": "Join",
|
||||
"permalink.show_dialog_warn.title": "Join private channel",
|
||||
"persistent_notifications.confirm.cancel": "Cancel",
|
||||
"persistent_notifications.confirm.description": "@mentioned recipients will be notified every {interval, plural, one {minute} other {{interval} minutes}} until they’ve acknowledged or replied to the message.",
|
||||
"persistent_notifications.confirm.send": "Send",
|
||||
"persistent_notifications.confirm.title": "Send persistent notifications",
|
||||
"persistent_notifications.error.max_recipients.description": "You can send persistent notifications to a maximum of {max} recipients. There are {count} recipients mentioned in your message. You’ll need to change who you’ve mentioned before you can send.",
|
||||
"persistent_notifications.error.max_recipients.title": "Too many recipients",
|
||||
"persistent_notifications.error.no_mentions.description": "There are no recipients mentioned in your message. You’ll need add mentions to be able to send persistent notifications.",
|
||||
"persistent_notifications.error.no_mentions.title": "Recipients must be @mentioned",
|
||||
"persistent_notifications.error.okay": "Okay",
|
||||
"persistent_notifications.error.special_mentions": "Cannot use @channel, @all or @here to mention recipients of persistent notifications.",
|
||||
"pinned_messages.empty.paragraph": "To pin important messages, long-press on a message and choose Pin To Channel. Pinned messages will be visible to everyone in this channel.",
|
||||
"pinned_messages.empty.title": "No pinned messages yet",
|
||||
"plus_menu.browse_channels.title": "Browse Channels",
|
||||
@@ -817,10 +828,17 @@
|
||||
"post_info.guest": "Guest",
|
||||
"post_info.system": "System",
|
||||
"post_message_view.edited": "(edited)",
|
||||
"post_priority.button.acknowledge": "Acknowledge",
|
||||
"post_priority.label.important": "IMPORTANT",
|
||||
"post_priority.label.urgent": "URGENT",
|
||||
"post_priority.picker.apply": "Apply",
|
||||
"post_priority.picker.beta": "BETA",
|
||||
"post_priority.picker.cancel": "Cancel",
|
||||
"post_priority.picker.label.important": "Important",
|
||||
"post_priority.picker.label.persistent_notifications": "Send persistent notifications",
|
||||
"post_priority.picker.label.persistent_notifications.description": "Recipients are notified every five minutes until they acknowledge or reply.",
|
||||
"post_priority.picker.label.request_ack": "Request acknowledgement",
|
||||
"post_priority.picker.label.request_ack.description": "Recipients are notified every five minutes until they acknowledge or reply.",
|
||||
"post_priority.picker.label.standard": "Standard",
|
||||
"post_priority.picker.label.urgent": "Urgent",
|
||||
"post_priority.picker.title": "Message priority",
|
||||
@@ -835,6 +853,7 @@
|
||||
"rate.error.title": "Error",
|
||||
"rate.subtitle": "Let us know what you think.",
|
||||
"rate.title": "Enjoying Mattermost?",
|
||||
"requested_ack.title": "Request Acknowledgements",
|
||||
"saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.",
|
||||
"saved_messages.empty.title": "No saved messages yet",
|
||||
"screen.channel_files.header.recent_files": "Recent Files",
|
||||
|
||||
5
types/api/config.d.ts
vendored
5
types/api/config.d.ts
vendored
@@ -153,6 +153,11 @@ interface ClientConfig {
|
||||
PluginsEnabled: string;
|
||||
PostEditTimeLimit: string;
|
||||
PostPriority: string;
|
||||
PostAcknowledgements: string;
|
||||
AllowPersistentNotifications: string;
|
||||
PersistentNotificationMaxRecipients: string;
|
||||
PersistentNotificationInterval: string;
|
||||
AllowPersistentNotificationsForGuests: string;
|
||||
PrivacyPolicyLink: string;
|
||||
ReportAProblemLink: string;
|
||||
RequireEmailVerification: string;
|
||||
|
||||
15
types/api/posts.d.ts
vendored
15
types/api/posts.d.ts
vendored
@@ -20,8 +20,16 @@ type PostType =
|
||||
|
||||
type PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
|
||||
|
||||
type PostPriorityData = {
|
||||
priority: ''|'urgent'|'important';
|
||||
type PostAcknowledgement = {
|
||||
post_id: string;
|
||||
user_id: string;
|
||||
acknowledged_at: number;
|
||||
}
|
||||
|
||||
type PostPriority = {
|
||||
priority: '' | 'urgent' | 'important';
|
||||
requested_ack?: boolean;
|
||||
persistent_notifications?: boolean;
|
||||
};
|
||||
|
||||
type PostEmbed = {
|
||||
@@ -38,12 +46,13 @@ type PostImage = {
|
||||
};
|
||||
|
||||
type PostMetadata = {
|
||||
acknowledgements?: PostAcknowledgement[];
|
||||
embeds?: PostEmbed[];
|
||||
emojis?: CustomEmoji[];
|
||||
files?: FileInfo[];
|
||||
images?: Dictionary<PostImage | undefined>;
|
||||
reactions?: Reaction[];
|
||||
priority?: PostPriorityData;
|
||||
priority?: PostPriority;
|
||||
};
|
||||
|
||||
type Post = {
|
||||
|
||||
1
types/database/raw_values.d.ts
vendored
1
types/database/raw_values.d.ts
vendored
@@ -22,6 +22,7 @@ type Draft = {
|
||||
files?: FileInfo[];
|
||||
message?: string;
|
||||
root_id: string;
|
||||
metadata?: PostMetadata;
|
||||
};
|
||||
|
||||
type MyTeam = {
|
||||
|
||||
Reference in New Issue
Block a user