forked from Ivasoft/mattermost-mobile
Compare commits
11 Commits
test1.0.5
...
MM-49540_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a32fd7b9f | ||
|
|
fddf44072d | ||
|
|
4b30ce3f9a | ||
|
|
76398ca2c8 | ||
|
|
81c89d51c2 | ||
|
|
b2f90f9b52 | ||
|
|
ab92d224c7 | ||
|
|
8d109715f8 | ||
|
|
7154ce75c2 | ||
|
|
3534383507 | ||
|
|
849e05e2c5 |
@@ -155,3 +155,40 @@ export const removeDraft = async (serverUrl: string, channelId: string, rootId =
|
|||||||
return {error};
|
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) {
|
||||||
|
if (!postPriority) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDraft: Draft = {
|
||||||
|
channel_id: channelId,
|
||||||
|
root_id: rootId,
|
||||||
|
metadata: {
|
||||||
|
priority: postPriority,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return operator.handleDraft({drafts: [newDraft], prepareRecordsOnly});
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.prepareUpdate((d) => {
|
||||||
|
d.metadata = {
|
||||||
|
priority: postPriority,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!prepareRecordsOnly) {
|
||||||
|
await operator.batchRecords([draft], 'updateDraftPriority');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {draft};
|
||||||
|
} catch (error) {
|
||||||
|
logError('Failed updateDraftPriority', error);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -248,6 +248,72 @@ export async function getPosts(serverUrl: string, ids: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[]) {
|
export async function deletePosts(serverUrl: string, postIds: string[]) {
|
||||||
try {
|
try {
|
||||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {extractRecordsForTable} from '@helpers/database';
|
|||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
import {getMyChannel, prepareMissingChannelsForAllTeams, queryAllMyChannel} from '@queries/servers/channel';
|
import {getMyChannel, prepareMissingChannelsForAllTeams, queryAllMyChannel} from '@queries/servers/channel';
|
||||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||||
import {getPostById, getRecentPostsInChannel} from '@queries/servers/post';
|
import {getIsPostAcknowledgementsEnabled, getIsPostPriorityEnabled, getPostById, getRecentPostsInChannel} from '@queries/servers/post';
|
||||||
import {getCurrentUserId, getCurrentChannelId} from '@queries/servers/system';
|
import {getCurrentUserId, getCurrentChannelId} from '@queries/servers/system';
|
||||||
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
||||||
import {queryAllUsers} from '@queries/servers/user';
|
import {queryAllUsers} from '@queries/servers/user';
|
||||||
@@ -499,74 +499,80 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false): Promise<AuthorsRequest> => {
|
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 {
|
try {
|
||||||
client = NetworkManager.getClient(serverUrl);
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
} catch (error) {
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
return {error};
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentUserId = await getCurrentUserId(operator.database);
|
const currentUserId = await getCurrentUserId(database);
|
||||||
const users = await queryAllUsers(operator.database).fetch();
|
const users = await queryAllUsers(database).fetch();
|
||||||
const existingUserIds = new Set<string>();
|
const existingUserIds = new Set<string>();
|
||||||
const existingUserNames = new Set<string>();
|
const existingUserNames = new Set<string>();
|
||||||
let excludeUsername;
|
let excludeUsername;
|
||||||
users.forEach((u) => {
|
users.forEach((u) => {
|
||||||
existingUserIds.add(u.id);
|
existingUserIds.add(u.id);
|
||||||
existingUserNames.add(u.username);
|
existingUserNames.add(u.username);
|
||||||
if (u.id === currentUserId) {
|
if (u.id === currentUserId) {
|
||||||
excludeUsername = u.username;
|
excludeUsername = u.username;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const usernamesToLoad = getNeededAtMentionedUsernames(existingUserNames, posts, excludeUsername);
|
const isPostPriorityEnabled = await getIsPostPriorityEnabled(database);
|
||||||
const userIdsToLoad = new Set<string>();
|
const isPostAcknowledgementsEnabled = await getIsPostAcknowledgementsEnabled(database);
|
||||||
for (const p of posts) {
|
const fetchAckUsers = isPostPriorityEnabled && isPostAcknowledgementsEnabled;
|
||||||
const {user_id} = p;
|
|
||||||
if (user_id !== currentUserId) {
|
|
||||||
userIdsToLoad.add(user_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const usernamesToLoad = getNeededAtMentionedUsernames(existingUserNames, posts, excludeUsername);
|
||||||
const promises: Array<Promise<UserProfile[]>> = [];
|
const userIdsToLoad = new Set<string>();
|
||||||
if (userIdsToLoad.size) {
|
for (const p of posts) {
|
||||||
promises.push(client.getProfilesByIds(Array.from(userIdsToLoad)));
|
const {user_id} = p;
|
||||||
}
|
if (user_id !== currentUserId) {
|
||||||
|
userIdsToLoad.add(user_id);
|
||||||
if (usernamesToLoad.size) {
|
}
|
||||||
promises.push(client.getProfilesByUsernames(Array.from(usernamesToLoad)));
|
if (fetchAckUsers) {
|
||||||
}
|
p.metadata?.acknowledgements?.forEach((ack) => {
|
||||||
|
if (ack.user_id !== currentUserId && !existingUserIds.has(ack.user_id)) {
|
||||||
if (promises.length) {
|
userIdsToLoad.add(ack.user_id);
|
||||||
const authorsResult = await Promise.allSettled(promises);
|
}
|
||||||
const result = authorsResult.reduce<UserProfile[][]>((acc, item) => {
|
|
||||||
if (item.status === 'fulfilled') {
|
|
||||||
acc.push(item.value);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const authors = result.flat();
|
|
||||||
if (!fetchOnly && authors.length) {
|
|
||||||
await operator.handleUsers({
|
|
||||||
users: authors,
|
|
||||||
prepareRecordsOnly: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {authors};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {authors: [] as UserProfile[]};
|
try {
|
||||||
|
const promises: Array<Promise<UserProfile[]>> = [];
|
||||||
|
if (userIdsToLoad.size) {
|
||||||
|
promises.push(client.getProfilesByIds(Array.from(userIdsToLoad)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernamesToLoad.size) {
|
||||||
|
promises.push(client.getProfilesByUsernames(Array.from(usernamesToLoad)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promises.length) {
|
||||||
|
const authorsResult = await Promise.allSettled(promises);
|
||||||
|
const result = authorsResult.reduce<UserProfile[][]>((acc, item) => {
|
||||||
|
if (item.status === 'fulfilled') {
|
||||||
|
acc.push(item.value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const authors = result.flat();
|
||||||
|
if (!fetchOnly && authors.length) {
|
||||||
|
await operator.handleUsers({
|
||||||
|
users: authors,
|
||||||
|
prepareRecordsOnly: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {authors};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {authors: [] as UserProfile[]};
|
||||||
|
} catch (error) {
|
||||||
|
logError('FETCH AUTHORS ERROR', error);
|
||||||
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('FETCH AUTHORS ERROR', error);
|
|
||||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
|
||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1129,3 +1135,51 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
|
|||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function acknowledgePost(serverUrl: string, postId: string) {
|
||||||
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||||
|
if (!operator) {
|
||||||
|
return {error: `${serverUrl} database not found`};
|
||||||
|
}
|
||||||
|
let client;
|
||||||
|
try {
|
||||||
|
client = NetworkManager.getClient(serverUrl);
|
||||||
|
} catch (error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = await getCurrentUserId(operator.database);
|
||||||
|
const data = await client.acknowledgePost(postId, userId);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unacknowledgePost(serverUrl: string, postId: string) {
|
||||||
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||||
|
if (!operator) {
|
||||||
|
return {error: `${serverUrl} database not found`};
|
||||||
|
}
|
||||||
|
let client;
|
||||||
|
try {
|
||||||
|
client = NetworkManager.getClient(serverUrl);
|
||||||
|
} catch (error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = await getCurrentUserId(operator.database);
|
||||||
|
const data = await client.unacknowledgePost(postId, userId);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import {handleChannelConvertedEvent, handleChannelCreatedEvent,
|
|||||||
handleUserRemovedFromChannelEvent} from './channel';
|
handleUserRemovedFromChannelEvent} from './channel';
|
||||||
import {handleGroupMemberAddEvent, handleGroupMemberDeleteEvent, handleGroupReceivedEvent, handleGroupTeamAssociatedEvent, handleGroupTeamDissociateEvent} from './group';
|
import {handleGroupMemberAddEvent, handleGroupMemberDeleteEvent, handleGroupReceivedEvent, handleGroupTeamAssociatedEvent, handleGroupTeamDissociateEvent} from './group';
|
||||||
import {handleOpenDialogEvent} from './integrations';
|
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 {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
|
||||||
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
|
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
|
||||||
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
|
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
|
||||||
@@ -177,6 +177,13 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
|||||||
handlePostUnread(serverUrl, msg);
|
handlePostUnread(serverUrl, msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WebsocketEvents.POST_ACKNOWLEDGEMENT_ADDED:
|
||||||
|
handlePostAcknowledgementAdded(serverUrl, msg);
|
||||||
|
break;
|
||||||
|
case WebsocketEvents.POST_ACKNOWLEDGEMENT_REMOVED:
|
||||||
|
handlePostAcknowledgementRemoved(serverUrl, msg);
|
||||||
|
break;
|
||||||
|
|
||||||
case WebsocketEvents.LEAVE_TEAM:
|
case WebsocketEvents.LEAVE_TEAM:
|
||||||
handleLeaveTeamEvent(serverUrl, msg);
|
handleLeaveTeamEvent(serverUrl, msg);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
import {DeviceEventEmitter} from 'react-native';
|
import {DeviceEventEmitter} from 'react-native';
|
||||||
|
|
||||||
import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel';
|
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 {createThreadFromNewPost, updateThread} from '@actions/local/thread';
|
||||||
import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel';
|
import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel';
|
||||||
import {fetchPostAuthors, fetchPostById} from '@actions/remote/post';
|
import {fetchPostAuthors, fetchPostById} from '@actions/remote/post';
|
||||||
import {fetchThread} from '@actions/remote/thread';
|
import {fetchThread} from '@actions/remote/thread';
|
||||||
|
import {fetchMissingProfilesByIds} from '@actions/remote/user';
|
||||||
import {ActionType, Events, Screens} from '@constants';
|
import {ActionType, Events, Screens} from '@constants';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {getChannelById, getMyChannel} from '@queries/servers/channel';
|
import {getChannelById, getMyChannel} from '@queries/servers/channel';
|
||||||
@@ -300,3 +301,24 @@ export async function handlePostUnread(serverUrl: string, msg: WebSocketMessage)
|
|||||||
markChannelAsUnread(serverUrl, channelId, delta, mentions, lastViewedAt);
|
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;
|
||||||
|
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;
|
||||||
|
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>;
|
searchPosts: (teamId: string, terms: string, isOrSearch: boolean) => Promise<PostResponse>;
|
||||||
doPostAction: (postId: string, actionId: string, selectedOption?: string) => Promise<any>;
|
doPostAction: (postId: string, actionId: string, selectedOption?: string) => Promise<any>;
|
||||||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: 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 {
|
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},
|
{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;
|
export default ClientPosts;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const OptionType = {
|
|||||||
...TouchableOptionTypes,
|
...TouchableOptionTypes,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type OptionType = typeof OptionType[keyof typeof OptionType];
|
export type OptionType = typeof OptionType[keyof typeof OptionType];
|
||||||
|
|
||||||
export const ITEM_HEIGHT = 48;
|
export const ITEM_HEIGHT = 48;
|
||||||
|
|
||||||
@@ -108,6 +108,7 @@ export type OptionItemProps = {
|
|||||||
info?: string;
|
info?: string;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
|
labelContainerStyle?: StyleProp<ViewStyle>;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
optionDescriptionTextStyle?: StyleProp<TextStyle>;
|
optionDescriptionTextStyle?: StyleProp<TextStyle>;
|
||||||
optionLabelTextStyle?: StyleProp<TextStyle>;
|
optionLabelTextStyle?: StyleProp<TextStyle>;
|
||||||
@@ -130,6 +131,7 @@ const OptionItem = ({
|
|||||||
info,
|
info,
|
||||||
inline = false,
|
inline = false,
|
||||||
label,
|
label,
|
||||||
|
labelContainerStyle,
|
||||||
onRemove,
|
onRemove,
|
||||||
optionDescriptionTextStyle,
|
optionDescriptionTextStyle,
|
||||||
optionLabelTextStyle,
|
optionLabelTextStyle,
|
||||||
@@ -238,7 +240,7 @@ const OptionItem = ({
|
|||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
>
|
>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<View style={styles.labelContainer}>
|
<View style={[styles.labelContainer, labelContainerStyle]}>
|
||||||
{Boolean(icon) && (
|
{Boolean(icon) && (
|
||||||
<View style={styles.iconContainer}>
|
<View style={styles.iconContainer}>
|
||||||
<OptionIcon
|
<OptionIcon
|
||||||
|
|||||||
81
app/components/post_draft/draft_input/header.tsx
Normal file
81
app/components/post_draft/draft_input/header.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {Platform, StyleSheet, 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';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
postPriority: PostPriority;
|
||||||
|
noMentionsError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginLeft: 12,
|
||||||
|
marginTop: Platform.select({
|
||||||
|
ios: 3,
|
||||||
|
android: 10,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
labelContainer: {
|
||||||
|
marginRight: 7,
|
||||||
|
},
|
||||||
|
ackContainer: {
|
||||||
|
marginRight: 7,
|
||||||
|
},
|
||||||
|
notificationsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: PostPriorityColors.URGENT,
|
||||||
|
marginLeft: 7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function DraftInputHeader({
|
||||||
|
postPriority,
|
||||||
|
noMentionsError,
|
||||||
|
}: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style.container}>
|
||||||
|
<View style={style.labelContainer}>
|
||||||
|
<PostPriorityLabel label={postPriority!.priority}/>
|
||||||
|
</View>
|
||||||
|
{postPriority.requested_ack && (
|
||||||
|
<View style={style.ackContainer}>
|
||||||
|
<CompassIcon
|
||||||
|
color={theme.onlineIndicator}
|
||||||
|
name='check-circle-outline'
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{postPriority.persistent_notifications && (
|
||||||
|
<View style={style.notificationsContainer}>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback, useRef} from 'react';
|
import React, {useCallback, useMemo, useRef} from 'react';
|
||||||
import {LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
|
import {useIntl} from 'react-intl';
|
||||||
|
import {Alert, LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
|
||||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import PostPriorityLabel from '@components/post_priority/post_priority_label';
|
import {General} from '@constants';
|
||||||
|
import {MENTIONS_REGEX, SPECIAL_MENTIONS_REGEX} from '@constants/autocomplete';
|
||||||
|
import {PostPriorityType} from '@constants/post';
|
||||||
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {getUsersCountFromMentions} from '@queries/servers/post';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
import PostInput from '../post_input';
|
import PostInput from '../post_input';
|
||||||
@@ -15,18 +21,23 @@ import SendAction from '../send_action';
|
|||||||
import Typing from '../typing';
|
import Typing from '../typing';
|
||||||
import Uploads from '../uploads';
|
import Uploads from '../uploads';
|
||||||
|
|
||||||
|
import Header from './header';
|
||||||
|
|
||||||
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
|
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
testID?: string;
|
testID?: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
channelType?: ChannelType;
|
||||||
rootId?: string;
|
rootId?: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
canShowPostPriority?: boolean;
|
canShowPostPriority?: boolean;
|
||||||
|
|
||||||
// Post Props
|
// Post Props
|
||||||
postPriority: PostPriorityData;
|
postPriority: PostPriority;
|
||||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
updatePostPriority: (postPriority: PostPriority) => void;
|
||||||
|
persistentNotificationInterval: number;
|
||||||
|
persistentNotificationMaxRecipients: number;
|
||||||
|
|
||||||
// Cursor Position Handler
|
// Cursor Position Handler
|
||||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||||
@@ -97,6 +108,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||||||
export default function DraftInput({
|
export default function DraftInput({
|
||||||
testID,
|
testID,
|
||||||
channelId,
|
channelId,
|
||||||
|
channelType,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
canShowPostPriority,
|
canShowPostPriority,
|
||||||
files,
|
files,
|
||||||
@@ -113,10 +125,16 @@ export default function DraftInput({
|
|||||||
updatePostInputTop,
|
updatePostInputTop,
|
||||||
postPriority,
|
postPriority,
|
||||||
updatePostPriority,
|
updatePostPriority,
|
||||||
|
persistentNotificationInterval,
|
||||||
|
persistentNotificationMaxRecipients,
|
||||||
setIsFocused,
|
setIsFocused,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const intl = useIntl();
|
||||||
|
const serverUrl = useServerUrl();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||||
|
|
||||||
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
||||||
updatePostInputTop(e.nativeEvent.layout.height);
|
updatePostInputTop(e.nativeEvent.layout.height);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -132,6 +150,102 @@ export default function DraftInput({
|
|||||||
const sendActionTestID = `${testID}.send_action`;
|
const sendActionTestID = `${testID}.send_action`;
|
||||||
const style = getStyleSheet(theme);
|
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) {
|
||||||
|
let title = '';
|
||||||
|
let description = '';
|
||||||
|
let error = true;
|
||||||
|
if (new RegExp(SPECIAL_MENTIONS_REGEX).test(value)) {
|
||||||
|
description = intl.formatMessage({
|
||||||
|
id: 'persistent_notifications.error.special_mentions',
|
||||||
|
defaultMessage: 'Cannot use @channel, @all or @here to mention recipients of persistent notifications.',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const formattedMentionsList = mentionsList.map((mention) => mention.slice(1));
|
||||||
|
const usersCount = database ? await getUsersCountFromMentions(database, formattedMentionsList) : 0;
|
||||||
|
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 {
|
||||||
|
error = false;
|
||||||
|
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 {1 minute} other {{interval} minutes}} until they’ve acknowledged or replied to the message.',
|
||||||
|
}, {
|
||||||
|
interval: persistentNotificationInterval,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Alert.alert(
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
error ? [{
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'persistent_notifications.error.okay',
|
||||||
|
defaultMessage: 'Okay',
|
||||||
|
}),
|
||||||
|
style: 'cancel',
|
||||||
|
}] : [
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'persistent_notifications.confirm.cancel',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
}),
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'persistent_notifications.confirm.send',
|
||||||
|
defaultMessage: 'Send',
|
||||||
|
}),
|
||||||
|
onPress: sendMessage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
}, [database, mentionsList, persistenNotificationsEnabled, persistentNotificationMaxRecipients, sendMessage, value]);
|
||||||
|
|
||||||
|
const sendActionDisabled = !canSend || noMentionsError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typing
|
<Typing
|
||||||
@@ -156,11 +270,10 @@ export default function DraftInput({
|
|||||||
overScrollMode={'never'}
|
overScrollMode={'never'}
|
||||||
disableScrollViewPanResponder={true}
|
disableScrollViewPanResponder={true}
|
||||||
>
|
>
|
||||||
{Boolean(postPriority?.priority) && (
|
<Header
|
||||||
<View style={style.postPriorityLabel}>
|
noMentionsError={noMentionsError}
|
||||||
<PostPriorityLabel label={postPriority!.priority}/>
|
postPriority={postPriority}
|
||||||
</View>
|
/>
|
||||||
)}
|
|
||||||
<PostInput
|
<PostInput
|
||||||
testID={postInputTestID}
|
testID={postInputTestID}
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
@@ -196,8 +309,8 @@ export default function DraftInput({
|
|||||||
/>
|
/>
|
||||||
<SendAction
|
<SendAction
|
||||||
testID={sendActionTestID}
|
testID={sendActionTestID}
|
||||||
disabled={!canSend}
|
disabled={sendActionDisabled}
|
||||||
sendMessage={sendMessage}
|
sendMessage={handleSendMessage}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
|||||||
import withObservables from '@nozbe/with-observables';
|
import withObservables from '@nozbe/with-observables';
|
||||||
import React from 'react';
|
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';
|
import QuickActions from './quick_actions';
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,22 @@
|
|||||||
|
|
||||||
import React, {useCallback} from 'react';
|
import React, {useCallback} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {StyleSheet} from 'react-native';
|
import {Keyboard, StyleSheet} from 'react-native';
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
import CompassIcon from '@components/compass_icon';
|
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 TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||||
|
import {Screens} from '@constants';
|
||||||
import {ICON_SIZE} from '@constants/post_draft';
|
import {ICON_SIZE} from '@constants/post_draft';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
import {openAsBottomSheet} from '@screens/navigation';
|
||||||
|
import {POST_PRIORITY_PICKER_BUTTON} from '@screens/post_priority_picker';
|
||||||
import {changeOpacity} from '@utils/theme';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
testID?: string;
|
testID?: string;
|
||||||
postPriority: PostPriorityData;
|
postPriority: PostPriority;
|
||||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
updatePostPriority: (postPriority: PostPriority) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = StyleSheet.create({
|
const style = StyleSheet.create({
|
||||||
@@ -35,34 +35,25 @@ export default function PostPriorityAction({
|
|||||||
updatePostPriority,
|
updatePostPriority,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const isTablet = useIsTablet();
|
||||||
const theme = useTheme();
|
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(() => {
|
const onPress = useCallback(() => {
|
||||||
bottomSheet({
|
Keyboard.dismiss();
|
||||||
title: intl.formatMessage({id: 'post_priority.picker.title', defaultMessage: 'Message priority'}),
|
|
||||||
renderContent,
|
const title = isTablet ? intl.formatMessage({id: 'post_priority.picker.title', defaultMessage: 'Message priority'}) : '';
|
||||||
snapPoints: [1, bottomSheetSnapPoint(1, COMPONENT_HEIGHT, bottom)],
|
|
||||||
|
openAsBottomSheet({
|
||||||
|
closeButtonId: POST_PRIORITY_PICKER_BUTTON,
|
||||||
|
screen: Screens.POST_PRIORITY_PICKER,
|
||||||
theme,
|
theme,
|
||||||
closeButtonId: 'post-priority-close-id',
|
title,
|
||||||
|
props: {
|
||||||
|
postPriority,
|
||||||
|
updatePostPriority,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}, [intl, renderContent, theme, bottom]);
|
}, [intl, postPriority, updatePostPriority, theme]);
|
||||||
|
|
||||||
const iconName = 'alert-circle-outline';
|
const iconName = 'alert-circle-outline';
|
||||||
const iconColor = changeOpacity(theme.centerChannelColor, 0.64);
|
const iconColor = changeOpacity(theme.centerChannelColor, 0.64);
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ type Props = {
|
|||||||
value: string;
|
value: string;
|
||||||
updateValue: (value: string) => void;
|
updateValue: (value: string) => void;
|
||||||
addFiles: (file: FileInfo[]) => void;
|
addFiles: (file: FileInfo[]) => void;
|
||||||
postPriority: PostPriorityData;
|
postPriority: PostPriority;
|
||||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
updatePostPriority: (postPriority: PostPriority) => void;
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import {General, Permissions} from '@constants';
|
|||||||
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||||
import {observeChannel, observeChannelInfo, observeCurrentChannel} from '@queries/servers/channel';
|
import {observeChannel, observeChannelInfo, observeCurrentChannel} from '@queries/servers/channel';
|
||||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||||
|
import {queryDraft} from '@queries/servers/drafts';
|
||||||
import {observePermissionForChannel} from '@queries/servers/role';
|
import {observePermissionForChannel} from '@queries/servers/role';
|
||||||
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
|
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
|
||||||
import {observeUser} from '@queries/servers/user';
|
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';
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
import type DraftModel from '@typings/database/models/servers/draft';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
rootId: string;
|
rootId: string;
|
||||||
@@ -24,6 +26,8 @@ type OwnProps = {
|
|||||||
channelIsArchived?: boolean;
|
channelIsArchived?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const observeFirst = (v: DraftModel[]) => v[0]?.observe() || of$(undefined);
|
||||||
|
|
||||||
const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) => {
|
const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) => {
|
||||||
const database = ownProps.database;
|
const database = ownProps.database;
|
||||||
const {rootId, channelId} = ownProps;
|
const {rootId, channelId} = ownProps;
|
||||||
@@ -42,9 +46,22 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
|||||||
switchMap((u) => of$(u?.status === General.OUT_OF_OFFICE)),
|
switchMap((u) => of$(u?.status === General.OUT_OF_OFFICE)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const postPriority = queryDraft(database, channelId, rootId).observeWithColumns(['metadata']).pipe(
|
||||||
|
switchMap(observeFirst),
|
||||||
|
switchMap((d) => {
|
||||||
|
if (!d?.metadata?.priority) {
|
||||||
|
return of$(INITIAL_PRIORITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return of$(d.metadata.priority);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const enableConfirmNotificationsToChannel = observeConfigBooleanValue(database, 'EnableConfirmNotificationsToChannel');
|
const enableConfirmNotificationsToChannel = observeConfigBooleanValue(database, 'EnableConfirmNotificationsToChannel');
|
||||||
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
|
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
|
||||||
const maxMessageLength = observeConfigIntValue(database, 'MaxPostSize', MAX_MESSAGE_LENGTH_FALLBACK);
|
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(
|
const useChannelMentions = combineLatest([channel, currentUser]).pipe(
|
||||||
switchMap(([c, u]) => {
|
switchMap(([c, u]) => {
|
||||||
@@ -57,6 +74,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const channelInfo = channel.pipe(switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$(undefined))));
|
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(
|
const membersCount = channelInfo.pipe(
|
||||||
switchMap((i) => (i ? of$(i.memberCount) : of$(0))),
|
switchMap((i) => (i ? of$(i.memberCount) : of$(0))),
|
||||||
);
|
);
|
||||||
@@ -64,6 +82,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
|||||||
const customEmojis = queryAllCustomEmojis(database).observe();
|
const customEmojis = queryAllCustomEmojis(database).observe();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
channelType,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
enableConfirmNotificationsToChannel,
|
enableConfirmNotificationsToChannel,
|
||||||
isTimezoneEnabled,
|
isTimezoneEnabled,
|
||||||
@@ -72,6 +91,9 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
|||||||
userIsOutOfOffice,
|
userIsOutOfOffice,
|
||||||
useChannelMentions,
|
useChannelMentions,
|
||||||
customEmojis,
|
customEmojis,
|
||||||
|
persistentNotificationInterval,
|
||||||
|
persistentNotificationMaxRecipients,
|
||||||
|
postPriority,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React, {useCallback, useEffect, useState} from 'react';
|
|||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {DeviceEventEmitter} from 'react-native';
|
import {DeviceEventEmitter} from 'react-native';
|
||||||
|
|
||||||
|
import {updateDraftPriority} from '@actions/local/draft';
|
||||||
import {getChannelTimezones} from '@actions/remote/channel';
|
import {getChannelTimezones} from '@actions/remote/channel';
|
||||||
import {executeCommand, handleGotoLocation} from '@actions/remote/command';
|
import {executeCommand, handleGotoLocation} from '@actions/remote/command';
|
||||||
import {createPost} from '@actions/remote/post';
|
import {createPost} from '@actions/remote/post';
|
||||||
@@ -28,6 +29,7 @@ import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji
|
|||||||
type Props = {
|
type Props = {
|
||||||
testID?: string;
|
testID?: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
channelType?: ChannelType;
|
||||||
rootId: string;
|
rootId: string;
|
||||||
canShowPostPriority?: boolean;
|
canShowPostPriority?: boolean;
|
||||||
setIsFocused: (isFocused: boolean) => void;
|
setIsFocused: (isFocused: boolean) => void;
|
||||||
@@ -52,15 +54,21 @@ type Props = {
|
|||||||
updatePostInputTop: (top: number) => void;
|
updatePostInputTop: (top: number) => void;
|
||||||
addFiles: (file: FileInfo[]) => void;
|
addFiles: (file: FileInfo[]) => void;
|
||||||
uploadFileError: React.ReactNode;
|
uploadFileError: React.ReactNode;
|
||||||
|
persistentNotificationInterval: number;
|
||||||
|
persistentNotificationMaxRecipients: number;
|
||||||
|
postPriority: PostPriority;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_PRIORITY = {
|
export const INITIAL_PRIORITY = {
|
||||||
priority: PostPriorityType.STANDARD,
|
priority: PostPriorityType.STANDARD,
|
||||||
|
requested_ack: false,
|
||||||
|
persistent_notifications: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SendHandler({
|
export default function SendHandler({
|
||||||
testID,
|
testID,
|
||||||
channelId,
|
channelId,
|
||||||
|
channelType,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
enableConfirmNotificationsToChannel,
|
enableConfirmNotificationsToChannel,
|
||||||
files,
|
files,
|
||||||
@@ -81,13 +89,15 @@ export default function SendHandler({
|
|||||||
updateCursorPosition,
|
updateCursorPosition,
|
||||||
updatePostInputTop,
|
updatePostInputTop,
|
||||||
setIsFocused,
|
setIsFocused,
|
||||||
|
persistentNotificationInterval,
|
||||||
|
persistentNotificationMaxRecipients,
|
||||||
|
postPriority,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const serverUrl = useServerUrl();
|
const serverUrl = useServerUrl();
|
||||||
|
|
||||||
const [channelTimezoneCount, setChannelTimezoneCount] = useState(0);
|
const [channelTimezoneCount, setChannelTimezoneCount] = useState(0);
|
||||||
const [sendingMessage, setSendingMessage] = useState(false);
|
const [sendingMessage, setSendingMessage] = useState(false);
|
||||||
const [postPriority, setPostPriority] = useState<PostPriorityData>(INITIAL_PRIORITY);
|
|
||||||
|
|
||||||
const canSend = useCallback(() => {
|
const canSend = useCallback(() => {
|
||||||
if (sendingMessage) {
|
if (sendingMessage) {
|
||||||
@@ -114,6 +124,10 @@ export default function SendHandler({
|
|||||||
setSendingMessage(false);
|
setSendingMessage(false);
|
||||||
}, [serverUrl, rootId, clearDraft]);
|
}, [serverUrl, rootId, clearDraft]);
|
||||||
|
|
||||||
|
const handlePostPriority = useCallback((priority: PostPriority) => {
|
||||||
|
updateDraftPriority(serverUrl, channelId, rootId, priority);
|
||||||
|
}, [serverUrl, rootId]);
|
||||||
|
|
||||||
const doSubmitMessage = useCallback(() => {
|
const doSubmitMessage = useCallback(() => {
|
||||||
const postFiles = files.filter((f) => !f.failed);
|
const postFiles = files.filter((f) => !f.failed);
|
||||||
const post = {
|
const post = {
|
||||||
@@ -123,7 +137,11 @@ export default function SendHandler({
|
|||||||
message: value,
|
message: value,
|
||||||
} as Post;
|
} as Post;
|
||||||
|
|
||||||
if (Object.keys(postPriority).length) {
|
if (!rootId && (
|
||||||
|
postPriority.priority ||
|
||||||
|
postPriority.requested_ack ||
|
||||||
|
postPriority.persistent_notifications)
|
||||||
|
) {
|
||||||
post.metadata = {
|
post.metadata = {
|
||||||
priority: postPriority,
|
priority: postPriority,
|
||||||
};
|
};
|
||||||
@@ -133,7 +151,6 @@ export default function SendHandler({
|
|||||||
|
|
||||||
clearDraft();
|
clearDraft();
|
||||||
setSendingMessage(false);
|
setSendingMessage(false);
|
||||||
setPostPriority(INITIAL_PRIORITY);
|
|
||||||
DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL);
|
DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL);
|
||||||
}, [files, currentUserId, channelId, rootId, value, clearDraft, postPriority]);
|
}, [files, currentUserId, channelId, rootId, value, clearDraft, postPriority]);
|
||||||
|
|
||||||
@@ -253,6 +270,7 @@ export default function SendHandler({
|
|||||||
<DraftInput
|
<DraftInput
|
||||||
testID={testID}
|
testID={testID}
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
|
channelType={channelType}
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId}
|
||||||
rootId={rootId}
|
rootId={rootId}
|
||||||
canShowPostPriority={canShowPostPriority}
|
canShowPostPriority={canShowPostPriority}
|
||||||
@@ -268,7 +286,9 @@ export default function SendHandler({
|
|||||||
maxMessageLength={maxMessageLength}
|
maxMessageLength={maxMessageLength}
|
||||||
updatePostInputTop={updatePostInputTop}
|
updatePostInputTop={updatePostInputTop}
|
||||||
postPriority={postPriority}
|
postPriority={postPriority}
|
||||||
updatePostPriority={setPostPriority}
|
updatePostPriority={handlePostPriority}
|
||||||
|
persistentNotificationInterval={persistentNotificationInterval}
|
||||||
|
persistentNotificationMaxRecipients={persistentNotificationMaxRecipients}
|
||||||
setIsFocused={setIsFocused}
|
setIsFocused={setIsFocused}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {of as of$} from 'rxjs';
|
|||||||
import {switchMap} from 'rxjs/operators';
|
import {switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
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 {observeConfigBooleanValue} from '@queries/servers/system';
|
||||||
import {observeCurrentUser} from '@queries/servers/user';
|
import {observeCurrentUser} from '@queries/servers/user';
|
||||||
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
||||||
@@ -30,6 +30,7 @@ const enhancedWithoutPosts = withObservables([], ({database}: WithDatabaseArgs)
|
|||||||
customEmojiNames: queryAllCustomEmojis(database).observeWithColumns(['name']).pipe(
|
customEmojiNames: queryAllCustomEmojis(database).observeWithColumns(['name']).pipe(
|
||||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||||
),
|
),
|
||||||
|
isPostAcknowledgementEnabled: observeIsPostAcknowledgementsEnabled(database),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
186
app/components/post_list/post/body/acknowledgements/index.tsx
Normal file
186
app/components/post_list/post/body/acknowledgements/index.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
// 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} from 'react-native';
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
import {acknowledgePost, unacknowledgePost} from '@actions/remote/post';
|
||||||
|
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 = {
|
||||||
|
currentUser: UserModel;
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
listHeader: {
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
listHeaderText: {
|
||||||
|
color: theme.centerChannelColor,
|
||||||
|
...typography('Heading', 600, 'SemiBold'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const Acknowledgements = ({currentUser, hasReactions, location, post, theme}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const isTablet = useIsTablet();
|
||||||
|
const {bottom} = useSafeAreaInsets();
|
||||||
|
const serverUrl = useServerUrl();
|
||||||
|
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
|
||||||
|
const isCurrentAuthor = post.userId === currentUser.id;
|
||||||
|
const acknowledgements = post.metadata?.acknowledgements || [];
|
||||||
|
|
||||||
|
const acknowledgedAt = useMemo(() => {
|
||||||
|
if (acknowledgements.length > 0) {
|
||||||
|
const ack = acknowledgements.find((item) => item.user_id === currentUser.id);
|
||||||
|
|
||||||
|
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, serverUrl]);
|
||||||
|
|
||||||
|
const handleOnLongPress = useCallback(() => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderContent = () => (
|
||||||
|
<>
|
||||||
|
{!isTablet && (
|
||||||
|
<View style={style.listHeader}>
|
||||||
|
<FormattedText
|
||||||
|
id='mobile.participants.header'
|
||||||
|
defaultMessage={'Thread Participants'}
|
||||||
|
style={style.listHeaderText}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<UsersList
|
||||||
|
channelId={post.channelId}
|
||||||
|
location={location}
|
||||||
|
userAcknowledgements={userAcknowledgements}
|
||||||
|
userIds={userIds}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapPoints: BottomSheetProps['snapPoints'] = [1, bottomSheetSnapPoint(Math.min(userIds.length, 5), USER_ROW_HEIGHT, bottom) + TITLE_HEIGHT];
|
||||||
|
if (userIds.length > 5) {
|
||||||
|
snapPoints.push('80%');
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheet({
|
||||||
|
closeButtonId: 'close-ack-users-list',
|
||||||
|
renderContent,
|
||||||
|
initialSnapIndex: 1,
|
||||||
|
snapPoints,
|
||||||
|
title: intl.formatMessage({id: 'mobile.participants.header', defaultMessage: 'Thread Participants'}),
|
||||||
|
theme,
|
||||||
|
});
|
||||||
|
}, [bottom, intl, isTablet, acknowledgements, theme, location, post.channelId]);
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -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,77 @@
|
|||||||
|
// 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 {Keyboard, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||||
|
|
||||||
|
import UserItem from '@components/user_item';
|
||||||
|
import {Screens} from '@constants';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
import {dismissBottomSheet, openAsBottomSheet} from '@screens/navigation';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
|
export const USER_ROW_HEIGHT = 60;
|
||||||
|
|
||||||
|
const style = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
height: USER_ROW_HEIGHT,
|
||||||
|
},
|
||||||
|
pictureContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
ackContainer: {
|
||||||
|
paddingLeft: 4,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
...typography('Body', 75),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channelId: string;
|
||||||
|
location: string;
|
||||||
|
user: UserModel;
|
||||||
|
userAcknowledgement: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserListItem = ({channelId, location, user, userAcknowledgement}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const theme = useTheme();
|
||||||
|
const openUserProfile = async () => {
|
||||||
|
if (user) {
|
||||||
|
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: user.id, channelId};
|
||||||
|
|
||||||
|
Keyboard.dismiss();
|
||||||
|
openAsBottomSheet({screen, title, theme, closeButtonId, props});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={openUserProfile}>
|
||||||
|
<UserItem
|
||||||
|
FooterComponent={
|
||||||
|
<View style={style.ackContainer}>
|
||||||
|
<Text style={style.time}>
|
||||||
|
{userAcknowledgement}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
containerStyle={style.container}
|
||||||
|
pictureContainerStyle={style.pictureContainer}
|
||||||
|
size={40}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserListItem;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useRef, useState} from 'react';
|
||||||
|
import {ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, PanResponder} from 'react-native';
|
||||||
|
import {FlatList} from 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
import UserListItem from './user_list_item';
|
||||||
|
|
||||||
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channelId: string;
|
||||||
|
location: string;
|
||||||
|
users: UserModel[];
|
||||||
|
userAcknowledgements: Record<string, number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsersList = ({channelId, location, users, userAcknowledgements}: Props) => {
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
const [direction, setDirection] = useState<'down' | 'up'>('down');
|
||||||
|
const listRef = useRef<FlatList>(null);
|
||||||
|
const prevOffset = useRef(0);
|
||||||
|
const panResponder = useRef(PanResponder.create({
|
||||||
|
onMoveShouldSetPanResponderCapture: (evt, g) => {
|
||||||
|
const dir = prevOffset.current < g.dy ? 'down' : 'up';
|
||||||
|
prevOffset.current = g.dy;
|
||||||
|
if (!enabled && dir === 'up') {
|
||||||
|
setEnabled(true);
|
||||||
|
}
|
||||||
|
setDirection(dir);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
})).current;
|
||||||
|
|
||||||
|
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
if (e.nativeEvent.contentOffset.y <= 0 && enabled && direction === 'down') {
|
||||||
|
setEnabled(false);
|
||||||
|
listRef.current?.scrollToOffset({animated: true, offset: 0});
|
||||||
|
}
|
||||||
|
}, [enabled, direction]);
|
||||||
|
|
||||||
|
const renderItem = useCallback(({item}: ListRenderItemInfo<UserModel>) => (
|
||||||
|
<UserListItem
|
||||||
|
channelId={channelId}
|
||||||
|
location={location}
|
||||||
|
user={item}
|
||||||
|
userAcknowledgement={userAcknowledgements[item.id]}
|
||||||
|
/>
|
||||||
|
), [channelId, location, userAcknowledgements]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={users}
|
||||||
|
ref={listRef}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onScroll={onScroll}
|
||||||
|
overScrollMode={'always'}
|
||||||
|
scrollEnabled={enabled}
|
||||||
|
scrollEventThrottle={60}
|
||||||
|
{...panResponder.panHandlers}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsersList;
|
||||||
@@ -12,6 +12,7 @@ import {THREAD} from '@constants/screens';
|
|||||||
import {isEdited as postEdited, isPostFailed} from '@utils/post';
|
import {isEdited as postEdited, isPostFailed} from '@utils/post';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import Acknowledgements from './acknowledgements';
|
||||||
import AddMembers from './add_members';
|
import AddMembers from './add_members';
|
||||||
import Content from './content';
|
import Content from './content';
|
||||||
import Failed from './failed';
|
import Failed from './failed';
|
||||||
@@ -19,10 +20,12 @@ import Message from './message';
|
|||||||
import Reactions from './reactions';
|
import Reactions from './reactions';
|
||||||
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
|
import type UserProfile from '@typings/database/models/servers/user';
|
||||||
import type {SearchPattern} from '@typings/global/markdown';
|
import type {SearchPattern} from '@typings/global/markdown';
|
||||||
|
|
||||||
type BodyProps = {
|
type BodyProps = {
|
||||||
appsEnabled: boolean;
|
appsEnabled: boolean;
|
||||||
|
currentUser: UserProfile;
|
||||||
hasFiles: boolean;
|
hasFiles: boolean;
|
||||||
hasReactions: boolean;
|
hasReactions: boolean;
|
||||||
highlight: boolean;
|
highlight: boolean;
|
||||||
@@ -33,6 +36,7 @@ type BodyProps = {
|
|||||||
isJumboEmoji: boolean;
|
isJumboEmoji: boolean;
|
||||||
isLastReply?: boolean;
|
isLastReply?: boolean;
|
||||||
isPendingOrFailed: boolean;
|
isPendingOrFailed: boolean;
|
||||||
|
isPostAcknowledgementEnabled?: boolean;
|
||||||
isPostAddChannelMember: boolean;
|
isPostAddChannelMember: boolean;
|
||||||
location: string;
|
location: string;
|
||||||
post: PostModel;
|
post: PostModel;
|
||||||
@@ -43,6 +47,13 @@ type BodyProps = {
|
|||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
return {
|
return {
|
||||||
|
ackAndReactionsContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignContent: 'flex-start',
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
messageBody: {
|
messageBody: {
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -75,8 +86,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Body = ({
|
const Body = ({
|
||||||
appsEnabled, hasFiles, hasReactions, highlight, highlightReplyBar,
|
appsEnabled, currentUser, hasFiles, hasReactions, highlight, highlightReplyBar,
|
||||||
isCRTEnabled, isEphemeral, isFirstReply, isJumboEmoji, isLastReply, isPendingOrFailed, isPostAddChannelMember,
|
isCRTEnabled, isEphemeral, isFirstReply, isJumboEmoji, isLastReply, isPendingOrFailed, isPostAcknowledgementEnabled, isPostAddChannelMember,
|
||||||
location, post, searchPatterns, showAddReaction, theme,
|
location, post, searchPatterns, showAddReaction, theme,
|
||||||
}: BodyProps) => {
|
}: BodyProps) => {
|
||||||
const style = getStyleSheet(theme);
|
const style = getStyleSheet(theme);
|
||||||
@@ -158,6 +169,8 @@ const Body = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const acknowledgementsVisible = isPostAcknowledgementEnabled && post.metadata?.priority?.requested_ack;
|
||||||
|
const reactionsVisible = hasReactions && showAddReaction;
|
||||||
if (!hasBeenDeleted) {
|
if (!hasBeenDeleted) {
|
||||||
body = (
|
body = (
|
||||||
<View style={style.messageBody}>
|
<View style={style.messageBody}>
|
||||||
@@ -180,13 +193,26 @@ const Body = ({
|
|||||||
isReplyPost={isReplyPost}
|
isReplyPost={isReplyPost}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{hasReactions && showAddReaction &&
|
{(acknowledgementsVisible || reactionsVisible) && (
|
||||||
<Reactions
|
<View style={style.ackAndReactionsContainer}>
|
||||||
location={location}
|
{acknowledgementsVisible && (
|
||||||
post={post}
|
<Acknowledgements
|
||||||
theme={theme}
|
currentUser={currentUser}
|
||||||
/>
|
hasReactions={hasReactions}
|
||||||
}
|
location={location}
|
||||||
|
post={post}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reactionsVisible && (
|
||||||
|
<Reactions
|
||||||
|
location={location}
|
||||||
|
post={post}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import React, {useCallback, useRef, useState} from 'react';
|
import React, {useCallback, useRef, useState} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
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 {addReaction, removeReaction} from '@actions/remote/reactions';
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
@@ -50,13 +50,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
width: 36,
|
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 (
|
return (
|
||||||
<View style={styles.reactionsContainer}>
|
<>
|
||||||
{
|
{
|
||||||
Array.from(sortedReactions).map((r) => {
|
Array.from(sortedReactions).map((r) => {
|
||||||
const reaction = reactionsByName.get(r);
|
const reaction = reactionsByName.get(r);
|
||||||
@@ -189,7 +182,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
{addMoreReactions}
|
{addMoreReactions}
|
||||||
</View>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
|||||||
|
|
||||||
import {Permissions, Preferences, Screens} from '@constants';
|
import {Permissions, Preferences, Screens} from '@constants';
|
||||||
import {queryFilesForPost} from '@queries/servers/file';
|
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 {queryReactionsForPost} from '@queries/servers/reaction';
|
||||||
import {observeCanManageChannelMembers, observePermissionForPost} from '@queries/servers/role';
|
import {observeCanManageChannelMembers, observePermissionForPost} from '@queries/servers/role';
|
||||||
import {observeIsPostPriorityEnabled} from '@queries/servers/system';
|
|
||||||
import {observeThreadById} from '@queries/servers/thread';
|
import {observeThreadById} from '@queries/servers/thread';
|
||||||
import {observeCurrentUser} from '@queries/servers/user';
|
import {observeCurrentUser} from '@queries/servers/user';
|
||||||
import {areConsecutivePosts, isPostEphemeral} from '@utils/post';
|
import {areConsecutivePosts, isPostEphemeral} from '@utils/post';
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type PostProps = {
|
|||||||
isCRTEnabled?: boolean;
|
isCRTEnabled?: boolean;
|
||||||
isEphemeral: boolean;
|
isEphemeral: boolean;
|
||||||
isFirstReply?: boolean;
|
isFirstReply?: boolean;
|
||||||
|
isPostAcknowledgementEnabled?: boolean;
|
||||||
isSaved?: boolean;
|
isSaved?: boolean;
|
||||||
isLastReply?: boolean;
|
isLastReply?: boolean;
|
||||||
isPostAddChannelMember: boolean;
|
isPostAddChannelMember: boolean;
|
||||||
@@ -109,7 +110,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||||||
|
|
||||||
const Post = ({
|
const Post = ({
|
||||||
appsEnabled, canDelete, currentUser, customEmojiNames, differentThreadSequence, hasFiles, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
|
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,
|
location, post, rootId, hasReactions, searchPatterns, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style,
|
||||||
testID, thread, previousPost,
|
testID, thread, previousPost,
|
||||||
}: PostProps) => {
|
}: PostProps) => {
|
||||||
@@ -302,6 +303,7 @@ const Post = ({
|
|||||||
body = (
|
body = (
|
||||||
<Body
|
<Body
|
||||||
appsEnabled={appsEnabled}
|
appsEnabled={appsEnabled}
|
||||||
|
currentUser={currentUser}
|
||||||
hasFiles={hasFiles}
|
hasFiles={hasFiles}
|
||||||
hasReactions={hasReactions}
|
hasReactions={hasReactions}
|
||||||
highlight={Boolean(highlightedStyle)}
|
highlight={Boolean(highlightedStyle)}
|
||||||
@@ -312,6 +314,7 @@ const Post = ({
|
|||||||
isJumboEmoji={isJumboEmoji}
|
isJumboEmoji={isJumboEmoji}
|
||||||
isLastReply={isLastReply}
|
isLastReply={isLastReply}
|
||||||
isPendingOrFailed={isPendingOrFailed}
|
isPendingOrFailed={isPendingOrFailed}
|
||||||
|
isPostAcknowledgementEnabled={isPostAcknowledgementEnabled}
|
||||||
isPostAddChannelMember={isPostAddChannelMember}
|
isPostAddChannelMember={isPostAddChannelMember}
|
||||||
location={location}
|
location={location}
|
||||||
post={post}
|
post={post}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type Props = {
|
|||||||
highlightedId?: PostModel['id'];
|
highlightedId?: PostModel['id'];
|
||||||
highlightPinnedOrSaved?: boolean;
|
highlightPinnedOrSaved?: boolean;
|
||||||
isCRTEnabled?: boolean;
|
isCRTEnabled?: boolean;
|
||||||
|
isPostAcknowledgementEnabled?: boolean;
|
||||||
isTimezoneEnabled: boolean;
|
isTimezoneEnabled: boolean;
|
||||||
lastViewedAt: number;
|
lastViewedAt: number;
|
||||||
location: string;
|
location: string;
|
||||||
@@ -97,6 +98,7 @@ const PostList = ({
|
|||||||
highlightedId,
|
highlightedId,
|
||||||
highlightPinnedOrSaved = true,
|
highlightPinnedOrSaved = true,
|
||||||
isCRTEnabled,
|
isCRTEnabled,
|
||||||
|
isPostAcknowledgementEnabled,
|
||||||
isTimezoneEnabled,
|
isTimezoneEnabled,
|
||||||
lastViewedAt,
|
lastViewedAt,
|
||||||
location,
|
location,
|
||||||
@@ -276,6 +278,7 @@ const PostList = ({
|
|||||||
appsEnabled,
|
appsEnabled,
|
||||||
customEmojiNames,
|
customEmojiNames,
|
||||||
isCRTEnabled,
|
isCRTEnabled,
|
||||||
|
isPostAcknowledgementEnabled,
|
||||||
highlight: highlightedId === post.id,
|
highlight: highlightedId === post.id,
|
||||||
highlightPinnedOrSaved,
|
highlightPinnedOrSaved,
|
||||||
isSaved: post.isSaved,
|
isSaved: post.isSaved,
|
||||||
@@ -294,7 +297,7 @@ const PostList = ({
|
|||||||
return (<Post {...postProps}/>);
|
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) => {
|
const scrollToIndex = useCallback((index: number, animated = true, applyOffset = true) => {
|
||||||
listRef.current?.scrollToIndex({
|
listRef.current?.scrollToIndex({
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const style = StyleSheet.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: PostPriorityData['priority'];
|
label: PostPriority['priority'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostPriorityLabel = ({label}: Props) => {
|
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,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useMemo} from 'react';
|
import React, {ReactNode, useMemo} from 'react';
|
||||||
import {IntlShape, useIntl} from 'react-intl';
|
import {IntlShape, useIntl} from 'react-intl';
|
||||||
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
@@ -19,10 +19,12 @@ import {getUserCustomStatus, isBot, isCustomStatusExpired, isGuest, isShared} fr
|
|||||||
import type UserModel from '@typings/database/models/servers/user';
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
type AtMentionItemProps = {
|
type AtMentionItemProps = {
|
||||||
|
FooterComponent?: ReactNode;
|
||||||
user?: UserProfile | UserModel;
|
user?: UserProfile | UserModel;
|
||||||
containerStyle?: StyleProp<ViewStyle>;
|
containerStyle?: StyleProp<ViewStyle>;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
showFullName: boolean;
|
showFullName: boolean;
|
||||||
|
size?: number;
|
||||||
testID?: string;
|
testID?: string;
|
||||||
isCustomStatusEnabled: boolean;
|
isCustomStatusEnabled: boolean;
|
||||||
pictureContainerStyle?: StyleProp<ViewStyle>;
|
pictureContainerStyle?: StyleProp<ViewStyle>;
|
||||||
@@ -66,6 +68,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
rowInfoBaseContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
rowInfoContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
rowInfo: {
|
rowInfo: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -90,9 +99,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||||||
|
|
||||||
const UserItem = ({
|
const UserItem = ({
|
||||||
containerStyle,
|
containerStyle,
|
||||||
|
FooterComponent,
|
||||||
user,
|
user,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
showFullName,
|
showFullName,
|
||||||
|
size = 24,
|
||||||
testID,
|
testID,
|
||||||
isCustomStatusEnabled,
|
isCustomStatusEnabled,
|
||||||
pictureContainerStyle,
|
pictureContainerStyle,
|
||||||
@@ -133,63 +144,68 @@ const UserItem = ({
|
|||||||
<View style={[style.rowPicture, pictureContainerStyle]}>
|
<View style={[style.rowPicture, pictureContainerStyle]}>
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
author={user}
|
author={user}
|
||||||
size={24}
|
size={size}
|
||||||
showStatus={false}
|
showStatus={false}
|
||||||
testID={`${userItemTestId}.profile_picture`}
|
testID={`${userItemTestId}.profile_picture`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View style={style.rowInfoBaseContainer}>
|
||||||
style={[style.rowInfo, {maxWidth: shared ? '75%' : '85%'}]}
|
<View style={style.rowInfoContainer}>
|
||||||
>
|
<View
|
||||||
{bot && <BotTag testID={`${userItemTestId}.bot.tag`}/>}
|
style={[style.rowInfo, {maxWidth: shared ? '75%' : '85%'}]}
|
||||||
{guest && <GuestTag testID={`${userItemTestId}.guest.tag`}/>}
|
|
||||||
{Boolean(name.length) &&
|
|
||||||
<Text
|
|
||||||
style={style.rowFullname}
|
|
||||||
numberOfLines={1}
|
|
||||||
testID={`${userItemTestId}.display_name`}
|
|
||||||
>
|
>
|
||||||
{name}
|
{bot && <BotTag testID={`${userItemTestId}.bot.tag`}/>}
|
||||||
</Text>
|
{guest && <GuestTag testID={`${userItemTestId}.guest.tag`}/>}
|
||||||
}
|
{Boolean(name.length) &&
|
||||||
{isCurrentUser &&
|
<Text
|
||||||
<FormattedText
|
style={style.rowFullname}
|
||||||
id='suggestion.mention.you'
|
numberOfLines={1}
|
||||||
defaultMessage=' (you)'
|
testID={`${userItemTestId}.display_name`}
|
||||||
style={style.rowUsername}
|
>
|
||||||
testID={`${userItemTestId}.current_user_indicator`}
|
{name}
|
||||||
/>
|
</Text>
|
||||||
}
|
}
|
||||||
{Boolean(user) && (
|
{isCurrentUser &&
|
||||||
<Text
|
<FormattedText
|
||||||
style={usernameTextStyle}
|
id='suggestion.mention.you'
|
||||||
numberOfLines={1}
|
defaultMessage=' (you)'
|
||||||
testID={`${userItemTestId}.username`}
|
style={style.rowUsername}
|
||||||
>
|
testID={`${userItemTestId}.current_user_indicator`}
|
||||||
{` @${user!.username}`}
|
/>
|
||||||
</Text>
|
}
|
||||||
)}
|
{Boolean(user) && (
|
||||||
|
<Text
|
||||||
|
style={usernameTextStyle}
|
||||||
|
numberOfLines={1}
|
||||||
|
testID={`${userItemTestId}.username`}
|
||||||
|
>
|
||||||
|
{` @${user!.username}`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && (
|
||||||
|
<CustomStatusEmoji
|
||||||
|
customStatus={customStatus!}
|
||||||
|
style={style.icon}
|
||||||
|
testID={userItemTestId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{shared && (
|
||||||
|
<ChannelIcon
|
||||||
|
name={name}
|
||||||
|
isActive={false}
|
||||||
|
isArchived={false}
|
||||||
|
isInfo={true}
|
||||||
|
isUnread={false}
|
||||||
|
size={18}
|
||||||
|
shared={true}
|
||||||
|
type={General.DM_CHANNEL}
|
||||||
|
style={style.icon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{FooterComponent}
|
||||||
</View>
|
</View>
|
||||||
{Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && (
|
|
||||||
<CustomStatusEmoji
|
|
||||||
customStatus={customStatus!}
|
|
||||||
style={style.icon}
|
|
||||||
testID={userItemTestId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{shared && (
|
|
||||||
<ChannelIcon
|
|
||||||
name={name}
|
|
||||||
isActive={false}
|
|
||||||
isArchived={false}
|
|
||||||
isInfo={true}
|
|
||||||
isUnread={false}
|
|
||||||
size={18}
|
|
||||||
shared={true}
|
|
||||||
type={General.DM_CHANNEL}
|
|
||||||
style={style.icon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 CODE_REGEX = /(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)| *(`{3,}|~{3,})[ .]*(\S+)? *\n([\s\S]*?\s*)\3 *(?:\n+|$)/g;
|
||||||
|
|
||||||
|
export const MENTIONS_REGEX = /(?:\B|\b_+)@([a-z0-9.\-_]+)/gi;
|
||||||
|
|
||||||
|
export const SPECIAL_MENTIONS_REGEX = /(?:\B|\b_+)@(channel|all|here)(?!(\.|-|_)*[^\W_])/gi;
|
||||||
|
|
||||||
export const MAX_LIST_HEIGHT = 230;
|
export const MAX_LIST_HEIGHT = 230;
|
||||||
export const MAX_LIST_TABLET_DIFF = 90;
|
export const MAX_LIST_TABLET_DIFF = 90;
|
||||||
|
|
||||||
@@ -30,4 +34,6 @@ export default {
|
|||||||
CODE_REGEX,
|
CODE_REGEX,
|
||||||
DATE_MENTION_SEARCH_REGEX,
|
DATE_MENTION_SEARCH_REGEX,
|
||||||
MAX_LIST_HEIGHT,
|
MAX_LIST_HEIGHT,
|
||||||
|
MENTIONS_REGEX,
|
||||||
|
SPECIAL_MENTIONS_REGEX,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const ONBOARDING = 'Onboarding';
|
|||||||
export const PERMALINK = 'Permalink';
|
export const PERMALINK = 'Permalink';
|
||||||
export const PINNED_MESSAGES = 'PinnedMessages';
|
export const PINNED_MESSAGES = 'PinnedMessages';
|
||||||
export const POST_OPTIONS = 'PostOptions';
|
export const POST_OPTIONS = 'PostOptions';
|
||||||
|
export const POST_PRIORITY_PICKER = 'PostPriorityPicker';
|
||||||
export const REACTIONS = 'Reactions';
|
export const REACTIONS = 'Reactions';
|
||||||
export const REVIEW_APP = 'ReviewApp';
|
export const REVIEW_APP = 'ReviewApp';
|
||||||
export const SAVED_MESSAGES = 'SavedMessages';
|
export const SAVED_MESSAGES = 'SavedMessages';
|
||||||
@@ -109,6 +110,7 @@ export default {
|
|||||||
PERMALINK,
|
PERMALINK,
|
||||||
PINNED_MESSAGES,
|
PINNED_MESSAGES,
|
||||||
POST_OPTIONS,
|
POST_OPTIONS,
|
||||||
|
POST_PRIORITY_PICKER,
|
||||||
REACTIONS,
|
REACTIONS,
|
||||||
REVIEW_APP,
|
REVIEW_APP,
|
||||||
SAVED_MESSAGES,
|
SAVED_MESSAGES,
|
||||||
@@ -165,6 +167,7 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
|
|||||||
BOTTOM_SHEET,
|
BOTTOM_SHEET,
|
||||||
EMOJI_PICKER,
|
EMOJI_PICKER,
|
||||||
POST_OPTIONS,
|
POST_OPTIONS,
|
||||||
|
POST_PRIORITY_PICKER,
|
||||||
THREAD_OPTIONS,
|
THREAD_OPTIONS,
|
||||||
REACTIONS,
|
REACTIONS,
|
||||||
USER_PROFILE,
|
USER_PROFILE,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import Calls from '@constants/calls';
|
|||||||
|
|
||||||
const WebsocketEvents = {
|
const WebsocketEvents = {
|
||||||
POSTED: 'posted',
|
POSTED: 'posted',
|
||||||
|
POST_ACKNOWLEDGEMENT_ADDED: 'post_acknowledgement_added',
|
||||||
|
POST_ACKNOWLEDGEMENT_REMOVED: 'post_acknowledgement_removed',
|
||||||
POST_EDITED: 'post_edited',
|
POST_EDITED: 'post_edited',
|
||||||
POST_DELETED: 'post_deleted',
|
POST_DELETED: 'post_deleted',
|
||||||
POST_UNREAD: 'post_unread',
|
POST_UNREAD: 'post_unread',
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ export const transformPostRecord = ({action, database, value}: TransformerArgs):
|
|||||||
post.updateAt = raw.update_at;
|
post.updateAt = raw.update_at;
|
||||||
post.isPinned = Boolean(raw.is_pinned);
|
post.isPinned = Boolean(raw.is_pinned);
|
||||||
post.message = raw.message;
|
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.userId = raw.user_id;
|
||||||
post.originalId = raw.original_id;
|
post.originalId = raw.original_id;
|
||||||
post.pendingPostId = raw.pending_post_id;
|
post.pendingPostId = raw.pending_post_id;
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Database, Model, Q, Query} from '@nozbe/watermelondb';
|
import {Database, Model, Q, Query} from '@nozbe/watermelondb';
|
||||||
import {of as of$} from 'rxjs';
|
import {of as of$, combineLatest} from 'rxjs';
|
||||||
import {switchMap} from 'rxjs/operators';
|
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
|
||||||
|
import {queryGroupsByNames} from './group';
|
||||||
import {querySavedPostsPreferences} from './preference';
|
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 PostModel from '@typings/database/models/servers/post';
|
||||||
import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
||||||
@@ -230,3 +232,53 @@ export const observeSavedPostsByIds = (database: Database, postIds: string[]) =>
|
|||||||
switchMap((prefs) => of$(new Set(prefs.map((p) => p.name)))),
|
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 combineLatest([featureFlag, cfg]).pipe(
|
||||||
|
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 combineLatest([user, enabledForAll, enabledForGuests]).pipe(
|
||||||
|
switchMap(([u, forAll, forGuests]) => {
|
||||||
|
if (u?.isGuest) {
|
||||||
|
return of$(forAll && forGuests);
|
||||||
|
}
|
||||||
|
return of$(forAll);
|
||||||
|
}),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsersCountFromMentions = 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]);
|
||||||
|
let count = usersCount;
|
||||||
|
groups.forEach((group) => {
|
||||||
|
count += group.memberCount;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {Database, Q} from '@nozbe/watermelondb';
|
|||||||
import {of as of$, Observable, combineLatest} from 'rxjs';
|
import {of as of$, Observable, combineLatest} from 'rxjs';
|
||||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||||
|
|
||||||
import {Config, Preferences} from '@constants';
|
import {Preferences} from '@constants';
|
||||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||||
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
||||||
import {isMinimumServerVersion} from '@utils/helpers';
|
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> => {
|
export const observeLicense = (database: Database): Observable<ClientLicense | undefined> => {
|
||||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.LICENSE).observe().pipe(
|
return querySystemValue(database, SYSTEM_IDENTIFIERS.LICENSE).observe().pipe(
|
||||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: undefined}))),
|
switchMap((result) => (result.length ? result[0].observe() : of$({value: undefined}))),
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||||||
case Screens.POST_OPTIONS:
|
case Screens.POST_OPTIONS:
|
||||||
screen = withServerDatabase(require('@screens/post_options').default);
|
screen = withServerDatabase(require('@screens/post_options').default);
|
||||||
break;
|
break;
|
||||||
|
case Screens.POST_PRIORITY_PICKER:
|
||||||
|
screen = withServerDatabase(require('@screens/post_priority_picker').default);
|
||||||
|
break;
|
||||||
case Screens.REACTIONS:
|
case Screens.REACTIONS:
|
||||||
screen = withServerDatabase(require('@screens/reactions').default);
|
screen = withServerDatabase(require('@screens/reactions').default);
|
||||||
break;
|
break;
|
||||||
|
|||||||
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, 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, isTablet: boolean) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: theme.centerChannelBg,
|
||||||
|
borderTopColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
|
borderTopWidth: 1,
|
||||||
|
paddingTop: 20,
|
||||||
|
paddingBottom: Platform.select({ios: (isTablet ? 20 : 32), android: 20}),
|
||||||
|
},
|
||||||
|
buttonsContainer: {
|
||||||
|
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, useIsTablet());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BottomSheetFooter {...props}>
|
||||||
|
<View style={style.container}>
|
||||||
|
<View style={style.buttonsContainer}>
|
||||||
|
<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>
|
||||||
|
</View>
|
||||||
|
</BottomSheetFooter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostPriorityPickerFooter;
|
||||||
21
app/screens/post_priority_picker/index.ts
Normal file
21
app/screens/post_priority_picker/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 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, {POST_PRIORITY_PICKER_BUTTON} 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));
|
||||||
|
export {POST_PRIORITY_PICKER_BUTTON};
|
||||||
@@ -3,32 +3,40 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import OptionItem, {OptionItemProps} from '@components/option_item';
|
import OptionItem, {OptionItemProps, OptionType} from '@components/option_item';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
optionLabelTextStyle: {
|
labelContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
},
|
||||||
|
optionLabelText: {
|
||||||
color: theme.centerChannelColor,
|
color: theme.centerChannelColor,
|
||||||
...typography('Body', 200, 'Regular'),
|
...typography('Body', 200, 'Regular'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const PostPriorityPickerItem = (props: Omit<OptionItemProps, 'type'>) => {
|
type Props = Omit<OptionItemProps, 'type'> & {
|
||||||
|
type?: OptionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PickerOption = (props: Props) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const style = getStyle(theme);
|
const style = getStyleSheet(theme);
|
||||||
|
|
||||||
const testID = `post_priority_picker_item.${props.value || 'standard'}`;
|
const testID = `post_priority_picker_item.${props.value || 'standard'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionItem
|
<OptionItem
|
||||||
optionLabelTextStyle={style.optionLabelTextStyle}
|
labelContainerStyle={style.labelContainer}
|
||||||
|
optionLabelTextStyle={style.optionLabelText}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
type='select'
|
|
||||||
{...props}
|
{...props}
|
||||||
|
type={props.type || 'select'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PostPriorityPickerItem;
|
export default PickerOption;
|
||||||
258
app/screens/post_priority_picker/post_priority_picker.tsx
Normal file
258
app/screens/post_priority_picker/post_priority_picker.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
// 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 {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 Footer from './footer';
|
||||||
|
import PickerOption from './picker_option';
|
||||||
|
|
||||||
|
import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
|
||||||
|
|
||||||
|
export const POST_PRIORITY_PICKER_BUTTON = 'close-post-priority-picker';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
componentId: string;
|
||||||
|
isPostAcknowledgementEnabled: boolean;
|
||||||
|
isPersistenNotificationsEnabled: boolean;
|
||||||
|
postPriority: PostPriority;
|
||||||
|
updatePostPriority: (data: PostPriority) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
}: Props) => {
|
||||||
|
const {bottom} = useSafeAreaInsets();
|
||||||
|
const intl = useIntl();
|
||||||
|
const isTablet = useIsTablet();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const style = getStyleSheet(theme);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
return dismissBottomSheet(Screens.POST_PRIORITY_PICKER);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useNavButtonPressed(POST_PRIORITY_PICKER_BUTTON, componentId, close, []);
|
||||||
|
|
||||||
|
const [data, setData] = useState<PostPriority>(postPriority);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
close();
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const renderAcknowledgementOption = () => (
|
||||||
|
<View style={style.toggleOptionContainer}>
|
||||||
|
<PickerOption
|
||||||
|
action={handleUpdateRequestedAck}
|
||||||
|
label={
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'post_priority.picker.label.request_ack',
|
||||||
|
defaultMessage: 'Request acknowledgement',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'post_priority.picker.label.request_ack.description',
|
||||||
|
defaultMessage: 'An acknowledgement button appears with your message.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
icon='check-circle-outline'
|
||||||
|
type='toggle'
|
||||||
|
selected={data.requested_ack}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPersistentNotificationsOption = () => (
|
||||||
|
<View style={style.toggleOptionContainer}>
|
||||||
|
<PickerOption
|
||||||
|
action={handleUpdatePersistentNotifications}
|
||||||
|
label={
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'post_priority.picker.label.persistent_notifications',
|
||||||
|
defaultMessage: 'Send persistent notifications',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'post_priority.picker.label.persistent_notifications.description',
|
||||||
|
defaultMessage: 'Recipients are notified every five minutes until they acknowledge or reply.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
icon='bell-ring-outline'
|
||||||
|
type='toggle'
|
||||||
|
selected={data.persistent_notifications}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
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({
|
||||||
|
id: 'post_priority.picker.label.standard',
|
||||||
|
defaultMessage: 'Standard',
|
||||||
|
})}
|
||||||
|
selected={data.priority === ''}
|
||||||
|
value={PostPriorityType.STANDARD}
|
||||||
|
/>
|
||||||
|
<PickerOption
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<PickerOption
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
{(isPostAcknowledgementEnabled) && (
|
||||||
|
<>
|
||||||
|
<View style={style.optionsSeparator}/>
|
||||||
|
{renderAcknowledgementOption()}
|
||||||
|
{displayPersistentNotifications && renderPersistentNotificationsOption()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFooter = (props: BottomSheetFooterProps) => (
|
||||||
|
<Footer
|
||||||
|
{...props}
|
||||||
|
onCancel={close}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BottomSheet
|
||||||
|
renderContent={renderContent}
|
||||||
|
closeButtonId={POST_PRIORITY_PICKER_BUTTON}
|
||||||
|
componentId={Screens.POST_PRIORITY_PICKER}
|
||||||
|
footerComponent={renderFooter}
|
||||||
|
initialSnapIndex={1}
|
||||||
|
snapPoints={snapPoints}
|
||||||
|
testID='post_options'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(PostPriorityPicker);
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
import {Post} from '@constants';
|
import {Post} from '@constants';
|
||||||
import {POST_TIME_TO_FAIL} from '@constants/post';
|
import {POST_TIME_TO_FAIL} from '@constants/post';
|
||||||
import {DEFAULT_LOCALE} from '@i18n';
|
import {DEFAULT_LOCALE} from '@i18n';
|
||||||
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
import {displayUsername} from '@utils/user';
|
import {displayUsername} from '@utils/user';
|
||||||
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
@@ -85,3 +86,7 @@ export const getLastFetchedAtFromPosts = (posts?: Post[]) => {
|
|||||||
return Math.max(maxTimestamp, timestamp);
|
return Math.max(maxTimestamp, timestamp);
|
||||||
}, 0) || 0;
|
}, 0) || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const moreThan5minAgo = (time: number) => {
|
||||||
|
return Date.now() - time > toMilliseconds({minutes: 5});
|
||||||
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function getComponents(inColor: string): {red: number; green: number; blu
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeStyleSheetFromTheme<T extends NamedStyles<T>>(getStyleFromTheme: (a: Theme) => T): (a: Theme) => T {
|
export function makeStyleSheetFromTheme<T extends NamedStyles<T>>(getStyleFromTheme: (a: Theme, ...args: unknown[]) => T): (a: Theme, ...args: unknown[]) => T {
|
||||||
let lastTheme: Theme;
|
let lastTheme: Theme;
|
||||||
let style: T;
|
let style: T;
|
||||||
return (theme: Theme) => {
|
return (theme: Theme) => {
|
||||||
|
|||||||
@@ -776,6 +776,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.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.join": "Join",
|
||||||
"permalink.show_dialog_warn.title": "Join private channel",
|
"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.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",
|
"pinned_messages.empty.title": "No pinned messages yet",
|
||||||
"plus_menu.browse_channels.title": "Browse Channels",
|
"plus_menu.browse_channels.title": "Browse Channels",
|
||||||
@@ -798,12 +808,17 @@
|
|||||||
"post_info.guest": "Guest",
|
"post_info.guest": "Guest",
|
||||||
"post_info.system": "System",
|
"post_info.system": "System",
|
||||||
"post_message_view.edited": "(edited)",
|
"post_message_view.edited": "(edited)",
|
||||||
|
"post_priority.button.acknowledge": "Acknowledge",
|
||||||
"post_priority.label.important": "IMPORTANT",
|
"post_priority.label.important": "IMPORTANT",
|
||||||
"post_priority.label.urgent": "URGENT",
|
"post_priority.label.urgent": "URGENT",
|
||||||
|
"post_priority.picker.apply": "Apply",
|
||||||
|
"post_priority.picker.cancel": "Cancel",
|
||||||
"post_priority.picker.beta": "BETA",
|
"post_priority.picker.beta": "BETA",
|
||||||
"post_priority.picker.label.important": "Important",
|
"post_priority.picker.label.important": "Important",
|
||||||
"post_priority.picker.label.standard": "Standard",
|
"post_priority.picker.label.standard": "Standard",
|
||||||
"post_priority.picker.label.urgent": "Urgent",
|
"post_priority.picker.label.urgent": "Urgent",
|
||||||
|
"post_priority.picker.label.request_ack": "Request acknowledgement",
|
||||||
|
"post_priority.picker.label.request_ack.description": "An acknowledgement button appears with your message.",
|
||||||
"post_priority.picker.title": "Message priority",
|
"post_priority.picker.title": "Message priority",
|
||||||
"post.options.title": "Options",
|
"post.options.title": "Options",
|
||||||
"post.reactions.title": "Reactions",
|
"post.reactions.title": "Reactions",
|
||||||
|
|||||||
5
types/api/config.d.ts
vendored
5
types/api/config.d.ts
vendored
@@ -153,6 +153,11 @@ interface ClientConfig {
|
|||||||
PluginsEnabled: string;
|
PluginsEnabled: string;
|
||||||
PostEditTimeLimit: string;
|
PostEditTimeLimit: string;
|
||||||
PostPriority: string;
|
PostPriority: string;
|
||||||
|
PostAcknowledgements: string;
|
||||||
|
AllowPersistentNotifications: string;
|
||||||
|
PersistentNotificationMaxRecipients: string;
|
||||||
|
PersistentNotificationInterval: string;
|
||||||
|
AllowPersistentNotificationsForGuests: string;
|
||||||
PrivacyPolicyLink: string;
|
PrivacyPolicyLink: string;
|
||||||
ReportAProblemLink: string;
|
ReportAProblemLink: string;
|
||||||
RequireEmailVerification: 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 PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
|
||||||
|
|
||||||
type PostPriorityData = {
|
type PostAcknowledgement = {
|
||||||
priority: ''|'urgent'|'important';
|
post_id: string;
|
||||||
|
user_id: string;
|
||||||
|
acknowledged_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostPriority = {
|
||||||
|
priority: '' | 'urgent' | 'important';
|
||||||
|
requested_ack?: boolean;
|
||||||
|
persistent_notifications?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PostEmbed = {
|
type PostEmbed = {
|
||||||
@@ -38,12 +46,13 @@ type PostImage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type PostMetadata = {
|
type PostMetadata = {
|
||||||
|
acknowledgements?: PostAcknowledgement[];
|
||||||
embeds?: PostEmbed[];
|
embeds?: PostEmbed[];
|
||||||
emojis?: CustomEmoji[];
|
emojis?: CustomEmoji[];
|
||||||
files?: FileInfo[];
|
files?: FileInfo[];
|
||||||
images?: Dictionary<PostImage>;
|
images?: Dictionary<PostImage>;
|
||||||
reactions?: Reaction[];
|
reactions?: Reaction[];
|
||||||
priority?: PostPriorityData;
|
priority?: PostPriority;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Post = {
|
type Post = {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ declare class DraftModel extends Model {
|
|||||||
|
|
||||||
/** files : The files field will hold an array of files object that have not yet been uploaded and persisted within the FILE table */
|
/** files : The files field will hold an array of files object that have not yet been uploaded and persisted within the FILE table */
|
||||||
files: FileInfo[];
|
files: FileInfo[];
|
||||||
|
|
||||||
|
metadata?: PostMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DraftModel;
|
export default DraftModel;
|
||||||
|
|||||||
1
types/database/raw_values.d.ts
vendored
1
types/database/raw_values.d.ts
vendored
@@ -21,6 +21,7 @@ type Draft = {
|
|||||||
files?: FileInfo[];
|
files?: FileInfo[];
|
||||||
message?: string;
|
message?: string;
|
||||||
root_id: string;
|
root_id: string;
|
||||||
|
metadata?: PostMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MyTeam = {
|
type MyTeam = {
|
||||||
|
|||||||
Reference in New Issue
Block a user