diff --git a/app/actions/local/user.ts b/app/actions/local/user.ts index 21ddbd83ef..bbae451914 100644 --- a/app/actions/local/user.ts +++ b/app/actions/local/user.ts @@ -97,27 +97,37 @@ export const updateRecentCustomStatuses = async (serverUrl: string, customStatus export const updateLocalUser = async ( serverUrl: string, userDetails: Partial & { status?: string}, + userId?: string, ) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const user = await getCurrentUser(database); + + let user: UserModel | undefined; + + if (userId) { + user = await getUserById(database, userId); + } else { + user = await getCurrentUser(database); + } + if (user) { + const u = user; await database.write(async () => { - await user.update((userRecord: UserModel) => { - userRecord.authService = userDetails.auth_service ?? user.authService; - userRecord.email = userDetails.email ?? user.email; - userRecord.firstName = userDetails.first_name ?? user.firstName; - userRecord.lastName = userDetails.last_name ?? user.lastName; - userRecord.lastPictureUpdate = userDetails.last_picture_update ?? user.lastPictureUpdate; - userRecord.locale = userDetails.locale ?? user.locale; - userRecord.nickname = userDetails.nickname ?? user.nickname; - userRecord.notifyProps = userDetails.notify_props ?? user.notifyProps; - userRecord.position = userDetails?.position ?? user.position; - userRecord.props = userDetails.props ?? user.props; - userRecord.roles = userDetails.roles ?? user.roles; - userRecord.status = userDetails?.status ?? user.status; - userRecord.timezone = userDetails.timezone ?? user.timezone; - userRecord.username = userDetails.username ?? user.username; + await u.update((userRecord: UserModel) => { + userRecord.authService = userDetails.auth_service ?? u.authService; + userRecord.email = userDetails.email ?? u.email; + userRecord.firstName = userDetails.first_name ?? u.firstName; + userRecord.lastName = userDetails.last_name ?? u.lastName; + userRecord.lastPictureUpdate = userDetails.last_picture_update ?? u.lastPictureUpdate; + userRecord.locale = userDetails.locale ?? u.locale; + userRecord.nickname = userDetails.nickname ?? u.nickname; + userRecord.notifyProps = userDetails.notify_props ?? u.notifyProps; + userRecord.position = userDetails?.position ?? u.position; + userRecord.props = userDetails.props ?? u.props; + userRecord.roles = userDetails.roles ?? u.roles; + userRecord.status = userDetails?.status ?? u.status; + userRecord.timezone = userDetails.timezone ?? u.timezone; + userRecord.username = userDetails.username ?? u.username; }); }); } diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 9d3a92003e..8bc9ba0b3f 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -7,6 +7,7 @@ import {DeviceEventEmitter} from 'react-native'; import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category'; import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel'; import {switchToGlobalThreads} from '@actions/local/thread'; +import {updateLocalUser} from '@actions/local/user'; import {loadCallForChannel} from '@calls/actions/calls'; import {DeepLink, Events, General, Preferences, Screens} from '@constants'; import DatabaseManager from '@database/manager'; @@ -15,7 +16,7 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; import AppsManager from '@managers/apps_manager'; import NetworkManager from '@managers/network_manager'; import {getActiveServer} from '@queries/app/servers'; -import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, queryChannelsById} from '@queries/servers/channel'; +import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, deleteChannelMembership, queryChannelsById} from '@queries/servers/channel'; import {queryDisplayNamePreferences} from '@queries/servers/preference'; import {getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system'; import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team'; @@ -35,9 +36,10 @@ import {setDirectChannelVisible} from './preference'; import {fetchRolesIfNeeded} from './role'; import {forceLogoutIfNecessary} from './session'; import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team'; -import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user'; +import {fetchProfilesInChannel, fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user'; import type {Client} from '@client/rest'; +import type ClientError from '@client/rest/error'; import type {Model} from '@nozbe/watermelondb'; import type ChannelModel from '@typings/database/models/servers/channel'; import type {IntlShape} from 'react-intl'; @@ -49,6 +51,99 @@ export type MyChannelsRequest = { error?: unknown; } +export type ChannelMembersRequest = { + members?: ChannelMembership[]; + error?: unknown; +} + +export async function removeMemberFromChannel(serverUrl: string, channelId: string, userId: string) { + try { + const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const client = NetworkManager.getClient(serverUrl); + + await client.removeFromChannel(userId, channelId); + await deleteChannelMembership(operator, userId, channelId); + + return {error: undefined}; + } catch (error) { + logError('removeMemberFromChannel', error); + forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); + return {error}; + } +} + +export async function fetchChannelMembersByIds(serverUrl: string, channelId: string, userIds: string[], fetchOnly = false): Promise { + try { + const client = NetworkManager.getClient(serverUrl); + const members = await client.getChannelMembersByIds(channelId, userIds); + + if (!fetchOnly) { + const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + if (operator && members.length) { + const memberships = members.map((u) => ({ + channel_id: channelId, + user_id: u.user_id, + scheme_admin: u.scheme_admin, + })); + await operator.handleChannelMembership({ + channelMemberships: memberships, + prepareRecordsOnly: false, + }); + } + } + + return {members}; + } catch (error) { + logError('fetchChannelMembersByIds', error); + forceLogoutIfNecessary(serverUrl, error as ClientError); + return {error}; + } +} +export async function updateChannelMemberSchemeRoles(serverUrl: string, channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean, fetchOnly = false) { + try { + const client = NetworkManager.getClient(serverUrl); + await client.updateChannelMemberSchemeRoles(channelId, userId, isSchemeUser, isSchemeAdmin); + + if (!fetchOnly) { + return getMemberInChannel(serverUrl, channelId, userId); + } + + return {error: undefined}; + } catch (error) { + logError('updateChannelMemberSchemeRoles', error); + forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); + return {error}; + } +} + +export async function getMemberInChannel(serverUrl: string, channelId: string, userId: string, fetchOnly = false) { + try { + const client = NetworkManager.getClient(serverUrl); + const member = await client.getMemberInChannel(channelId, userId); + + if (!fetchOnly) { + updateLocalUser(serverUrl, member, userId); + } + return {member, error: undefined}; + } catch (error) { + logError('getMemberInChannel', error); + forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); + return {error}; + } +} + +export async function fetchChannelMemberships(serverUrl: string, channelId: string, options: GetUsersOptions, fetchOnly = false) { + const {users = []} = await fetchProfilesInChannel(serverUrl, channelId, undefined, options, fetchOnly); + const userIds = users.map((u) => u.id); + + // MM-49896 https://mattermost.atlassian.net/browse/MM-49896 + // We are not sure the getChannelMembers API returns the same members + // from getProfilesInChannel. This guarantees a 1:1 match of the + // user IDs + const {members = []} = await fetchChannelMembersByIds(serverUrl, channelId, userIds, true); + return {users, members}; +} + export async function addMembersToChannel(serverUrl: string, channelId: string, userIds: string[], postRootId = '', fetchOnly = false) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index a1d6dc9c78..cc6153fc2e 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -74,7 +74,7 @@ export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise { +export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions, fetchOnly = false): Promise { let client: Client; try { client = NetworkManager.getClient(serverUrl); @@ -83,7 +83,7 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin } try { - const users = await client.getProfilesInChannel(channelId); + const users = await client.getProfilesInChannel(channelId, options); const uniqueUsers = Array.from(new Set(users)); const filteredUsers = uniqueUsers.filter((u) => u.id !== excludeUserId); if (!fetchOnly) { @@ -108,6 +108,7 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin return {channelId, users: filteredUsers}; } catch (error) { + logError('fetchProfilesInChannel', error); forceLogoutIfNecessary(serverUrl, error as ClientError); return {channelId, error}; } @@ -198,7 +199,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st const data: ProfilesInChannelRequest[] = []; for await (const cIds of channels) { - const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, true)); + const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, undefined, true)); const response = await Promise.all(requests); data.push(...response); } @@ -530,7 +531,7 @@ export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, pag } }; -export const searchProfiles = async (serverUrl: string, term: string, options: any = {}, fetchOnly = false) => { +export const searchProfiles = async (serverUrl: string, term: string, options: SearchUserOptions, fetchOnly = false) => { let client: Client; try { client = NetworkManager.getClient(serverUrl); @@ -563,6 +564,7 @@ export const searchProfiles = async (serverUrl: string, term: string, options: a return {data: users}; } catch (error) { + logError('searchProfiles', error); forceLogoutIfNecessary(serverUrl, error as ClientError); return {error}; } diff --git a/app/client/rest/channels.ts b/app/client/rest/channels.ts index 15499a6077..b5adb465a0 100644 --- a/app/client/rest/channels.ts +++ b/app/client/rest/channels.ts @@ -40,6 +40,8 @@ export interface ClientChannelsMix { searchChannels: (teamId: string, term: string) => Promise; searchArchivedChannels: (teamId: string, term: string) => Promise; searchAllChannels: (term: string, teamIds: string[], archivedOnly?: boolean) => Promise; + updateChannelMemberSchemeRoles: (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => Promise; + getMemberInChannel: (channelId: string, userId: string) => Promise; } const ClientChannels = (superclass: any) => class extends superclass { @@ -327,6 +329,25 @@ const ClientChannels = (superclass: any) => class extends superclass { {method: 'post', body}, ); }; + + // Update a channel member's scheme_admin/scheme_user properties. Typically + // this should either be scheme_admin=false, scheme_user=true for ordinary + // channel member, or scheme_admin=true, scheme_user=true for a channel + // admin. + updateChannelMemberSchemeRoles = (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => { + const body = {scheme_user: isSchemeUser, scheme_admin: isSchemeAdmin}; + return this.doFetch( + `${this.getChannelMembersRoute(channelId)}/${userId}/schemeRoles`, + {method: 'put', body}, + ); + }; + + getMemberInChannel = (channelId: string, userId: string) => { + return this.doFetch( + `${this.getChannelMembersRoute(channelId)}/${userId}`, + {method: 'get'}, + ); + }; }; export default ClientChannels; diff --git a/app/client/rest/users.ts b/app/client/rest/users.ts index eada89f632..aac8d355e7 100644 --- a/app/client/rest/users.ts +++ b/app/client/rest/users.ts @@ -24,7 +24,7 @@ export interface ClientUsersMix { getProfilesInTeam: (teamId: string, page?: number, perPage?: number, sort?: string, options?: Record) => Promise; getProfilesNotInTeam: (teamId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise; getProfilesWithoutTeam: (page?: number, perPage?: number, options?: Record) => Promise; - getProfilesInChannel: (channelId: string, page?: number, perPage?: number, sort?: string) => Promise; + getProfilesInChannel: (channelId: string, options?: GetUsersOptions) => Promise; getProfilesInGroupChannels: (channelsIds: string[]) => Promise<{[x: string]: UserProfile[]}>; getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise; getMe: () => Promise; @@ -37,7 +37,7 @@ export interface ClientUsersMix { getSessions: (userId: string) => Promise; checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>; attachDevice: (deviceId: string) => Promise; - searchUsers: (term: string, options: any) => Promise; + searchUsers: (term: string, options: SearchUserOptions) => Promise; getStatusesByIds: (userIds: string[]) => Promise; getStatus: (userId: string) => Promise; updateStatus: (status: UserStatus) => Promise; @@ -250,10 +250,10 @@ const ClientUsers = (superclass: any) => class extends superclass { ); }; - getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => { + getProfilesInChannel = async (channelId: string, options: GetUsersOptions) => { this.analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId}); - const queryStringObj = {in_channel: channelId, page, per_page: perPage, sort}; + const queryStringObj = {in_channel: channelId, ...options}; return this.doFetch( `${this.getUsersRoute()}${buildQueryString(queryStringObj)}`, {method: 'get'}, diff --git a/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx b/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx index e4544209f9..009c18c398 100644 --- a/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx +++ b/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx @@ -23,7 +23,7 @@ type Props = { testID?: string; } -const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => { +const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => { const intl = useIntl(); const serverUrl = useServerUrl(); const isTablet = useIsTablet(); @@ -183,4 +183,4 @@ const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type, ); }; -export default LeaveChanelLabel; +export default LeaveChannelLabel; diff --git a/app/components/channel_actions/manage_members_label/index.tsx b/app/components/channel_actions/manage_members_label/index.tsx new file mode 100644 index 0000000000..207c7c370a --- /dev/null +++ b/app/components/channel_actions/manage_members_label/index.tsx @@ -0,0 +1,33 @@ +// 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 {of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; + +import {queryUsersById} from '@queries/servers/user'; + +import ManageMembersLabel from './manage_members_label'; + +import type {WithDatabaseArgs} from '@typings/database/database'; + +type OwnProps = WithDatabaseArgs & { + isDefaultChannel: boolean; + userId: string; +} + +const enhanced = withObservables(['isDefaultChannel', 'userId'], ({isDefaultChannel, userId, database}: OwnProps) => { + const users = queryUsersById(database, [userId]).observe(); + const canRemoveUser = users.pipe( + switchMap((u) => { + return of$(!isDefaultChannel || (isDefaultChannel && u[0].isGuest)); + }), + ); + + return { + canRemoveUser, + }; +}); + +export default withDatabase(enhanced(ManageMembersLabel)); diff --git a/app/components/channel_actions/manage_members_label/manage_members_label.tsx b/app/components/channel_actions/manage_members_label/manage_members_label.tsx new file mode 100644 index 0000000000..e7f33b9fc7 --- /dev/null +++ b/app/components/channel_actions/manage_members_label/manage_members_label.tsx @@ -0,0 +1,152 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {defineMessages, useIntl} from 'react-intl'; +import {Alert, DeviceEventEmitter} from 'react-native'; + +import {fetchChannelStats, removeMemberFromChannel, updateChannelMemberSchemeRoles} from '@actions/remote/channel'; +import OptionItem from '@components/option_item'; +import {Events, Members} from '@constants'; +import {useServerUrl} from '@context/server'; +import {t} from '@i18n'; +import {dismissBottomSheet} from '@screens/navigation'; +import {alertErrorWithFallback} from '@utils/draft'; + +import type {ManageOptionsTypes} from '@constants/members'; + +const {MAKE_CHANNEL_ADMIN, MAKE_CHANNEL_MEMBER, REMOVE_USER} = Members.ManageOptions; + +const messages = defineMessages({ + role_change_error: { + id: t('mobile.manage_members.change_role.error'), + defaultMessage: 'An error occurred while trying to update the role. Please check your connection and try again.', + }, + make_channel_admin: { + id: t('mobile.manage_members.make_channel_admin'), + defaultMessage: 'Make Channel Admin', + }, + make_channel_member: { + id: t('mobile.manage_members.make_channel_member'), + defaultMessage: 'Make Channel Member', + }, + remove_title: { + id: t('mobile.manage_members.remove_member'), + defaultMessage: 'Remove From Channel', + }, + remove_message: { + id: t('mobile.manage_members.message'), + defaultMessage: 'Are you sure you want to remove the selected member from the channel?', + }, + remove_cancel: { + id: t('mobile.manage_members.cancel'), + defaultMessage: 'Cancel', + }, + remove_confirm: { + id: t('mobile.manage_members.remove'), + defaultMessage: 'Remove', + }, +}); + +type Props = { + canRemoveUser: boolean; + channelId: string; + manageOption: ManageOptionsTypes; + testID?: string; + userId: string; +} + +const ManageMembersLabel = ({canRemoveUser, channelId, manageOption, testID, userId}: Props) => { + const intl = useIntl(); + const {formatMessage} = intl; + const serverUrl = useServerUrl(); + + const handleRemoveUser = useCallback(async () => { + removeMemberFromChannel(serverUrl, channelId, userId); + fetchChannelStats(serverUrl, channelId, false); + await dismissBottomSheet(); + DeviceEventEmitter.emit(Events.REMOVE_USER_FROM_CHANNEL, userId); + }, [channelId, serverUrl, userId]); + + const removeFromChannel = useCallback(() => { + Alert.alert( + formatMessage(messages.remove_title), + formatMessage(messages.remove_message), + [{ + text: formatMessage(messages.remove_cancel), + style: 'cancel', + }, { + text: formatMessage(messages.remove_confirm), + style: 'destructive', + onPress: handleRemoveUser, + }], {cancelable: false}, + ); + }, [formatMessage, handleRemoveUser]); + + const updateChannelMemberSchemeRole = useCallback(async (schemeAdmin: boolean) => { + const result = await updateChannelMemberSchemeRoles(serverUrl, channelId, userId, true, schemeAdmin); + if (result.error) { + alertErrorWithFallback(intl, result.error, messages.role_change_error); + } + await dismissBottomSheet(); + DeviceEventEmitter.emit(Events.MANAGE_USER_CHANGE_ROLE, {userId, schemeAdmin}); + }, [channelId, userId, intl, serverUrl]); + + const onAction = useCallback(() => { + switch (manageOption) { + case REMOVE_USER: + removeFromChannel(); + break; + case MAKE_CHANNEL_ADMIN: + updateChannelMemberSchemeRole(true); + break; + case MAKE_CHANNEL_MEMBER: + updateChannelMemberSchemeRole(false); + break; + default: + break; + } + }, [manageOption, removeFromChannel, updateChannelMemberSchemeRole]); + + let actionText; + let icon; + let isDestructive = false; + switch (manageOption) { + case REMOVE_USER: + actionText = (formatMessage(messages.remove_title)); + icon = 'trash-can-outline'; + isDestructive = true; + break; + case MAKE_CHANNEL_ADMIN: + actionText = formatMessage(messages.make_channel_admin); + icon = 'account-outline'; + break; + case MAKE_CHANNEL_MEMBER: + actionText = formatMessage(messages.make_channel_member); + icon = 'account-outline'; + break; + default: + break; + } + + if (manageOption === REMOVE_USER && !canRemoveUser) { + return null; + } + + if (!actionText) { + return null; + } + + return ( + + ); +}; + +export default ManageMembersLabel; diff --git a/app/components/post_list/post/index.ts b/app/components/post_list/post/index.ts index 9863abade2..2989962c8d 100644 --- a/app/components/post_list/post/index.ts +++ b/app/components/post_list/post/index.ts @@ -102,7 +102,7 @@ const withPost = withObservables( const isEphemeral = of$(isPostEphemeral(post)); if (post.props?.add_channel_member && isPostEphemeral(post)) { - isPostAddChannelMember = observeCanManageChannelMembers(database, post, currentUser); + isPostAddChannelMember = observeCanManageChannelMembers(database, post.channelId, currentUser); } let highlightReplyBar = of$(false); diff --git a/app/components/user_list/__snapshots__/index.test.tsx.snap b/app/components/user_list/__snapshots__/index.test.tsx.snap index 22a2b45a71..45fc81c90d 100644 --- a/app/components/user_list/__snapshots__/index.test.tsx.snap +++ b/app/components/user_list/__snapshots__/index.test.tsx.snap @@ -928,3 +928,506 @@ exports[`components/channel_list_row should show results no tutorial 1`] = ` `; + +exports[`components/channel_list_row should show results no tutorial 2 users 1`] = ` + + + + + + + J + + + + + + + + + + + + + + + + + johndoe + + + + + + + + + + + + + + + + R + + + + + + + + + + + + + + + + + rocky + + + + + + + + + + + + + + + + + + +`; diff --git a/app/components/user_list/index.test.tsx b/app/components/user_list/index.test.tsx index 122c491fda..fd0ae5d99c 100644 --- a/app/components/user_list/index.test.tsx +++ b/app/components/user_list/index.test.tsx @@ -38,6 +38,34 @@ describe('components/channel_list_row', () => { push_status: 'away', }, }; + + const user2: UserProfile = { + id: '2', + create_at: 1111, + update_at: 1111, + delete_at: 0, + username: 'rocky', + auth_service: '', + email: 'rocky@doe.com', + nickname: '', + first_name: '', + last_name: '', + position: '', + roles: '', + locale: '', + notify_props: { + channel: 'true', + comments: 'never', + desktop: 'mention', + desktop_sound: 'true', + email: 'true', + first_name: 'true', + mention_keys: '', + push: 'mention', + push_status: 'away', + }, + }; + beforeAll(async () => { const server = await TestHelper.setupServerDatabase(); database = server.database; @@ -91,6 +119,30 @@ describe('components/channel_list_row', () => { expect(wrapper.toJSON()).toMatchSnapshot(); }); + it('should show results no tutorial 2 users', () => { + const wrapper = renderWithEverything( + { + // noop + }} + fetchMore={() => { + // noop + }} + loading={true} + selectedIds={{}} + showNoResults={true} + tutorialWatched={true} + />, + {database}, + ); + + expect(wrapper.toJSON()).toMatchSnapshot(); + }); + it('should show results and tutorial', () => { const wrapper = renderWithEverything( & {section?: SectionListData} + const INITIAL_BATCH_TO_RENDER = 15; const SCROLL_EVENT_THROTTLE = 60; +const messages = defineMessages({ + admins: { + id: t('mobile.manage_members.section_title_admins'), + defaultMessage: 'CHANNEL ADMINS', + }, + members: { + id: t('mobile.manage_members.section_title_members'), + defaultMessage: 'MEMBERS', + }, +}); + const keyboardDismissProp = Platform.select({ android: { onScrollBeginDrag: Keyboard.dismiss, @@ -41,29 +56,58 @@ const sectionKeyExtractor = (profile: UserProfile) => { return profile.username[0].toUpperCase(); }; -export function createProfilesSections(profiles: UserProfile[]) { - const sections: {[key: string]: UserProfile[]} = {}; - const sectionKeys: string[] = []; - for (const profile of profiles) { - const sectionKey = sectionKeyExtractor(profile); +const sectionRoleKeyExtractor = (cAdmin: boolean) => { + // Group items by channel admin or channel member + return cAdmin ? messages.admins : messages.members; +}; - if (!sections[sectionKey]) { - sections[sectionKey] = []; - sectionKeys.push(sectionKey); - } - - sections[sectionKey].push(profile); +export function createProfilesSections(intl: IntlShape, profiles: UserProfile[], members?: ChannelMember[]) { + if (!profiles.length) { + return []; } - sectionKeys.sort(); + const sections = new Map(); - return sectionKeys.map((sectionKey, index) => { - return { - id: sectionKey, - first: index === 0, - data: sections[sectionKey], - }; - }); + if (members?.length) { + // when channel members are provided, build the sections by admins and members + const membersDictionary = new Map(); + const membersSections = new Map(); + const {formatMessage} = intl; + members.forEach((m) => membersDictionary.set(m.user_id, m)); + profiles.forEach((p) => { + const member = membersDictionary.get(p.id); + const sectionKey = sectionRoleKeyExtractor(member.scheme_admin!); + const sectionValue = membersSections.get(sectionKey) || []; + + // combine UserProfile and ChannelMember objects so can get channel member scheme_admin permission + const section = [...sectionValue, {...p, ...member}]; + membersSections.set(sectionKey, section); + }); + sections.set(formatMessage(messages.admins), membersSections.get(messages.admins)); + sections.set(formatMessage(messages.members), membersSections.get(messages.members)); + } else { + // when channel members are not provided, build the sections alphabetically + profiles.forEach((p) => { + const sectionKey = sectionKeyExtractor(p); + const sectionValue = sections.get(sectionKey) || []; + const section = [...sectionValue, p]; + sections.set(sectionKey, section); + }); + } + + const results = []; + let index = 0; + for (const [k, v] of sections) { + if (v) { + results.push({ + first: index === 0, + id: k, + data: v, + }); + } + index++; + } + return results; } const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { @@ -103,11 +147,14 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { type Props = { profiles: UserProfile[]; + channelMembers?: ChannelMember[]; currentUserId: string; teammateNameDisplay: string; handleSelectProfile: (user: UserProfile) => void; fetchMore: () => void; loading: boolean; + manageMode?: boolean; + showManageMode?: boolean; showNoResults: boolean; selectedIds: {[id: string]: UserProfile}; testID?: string; @@ -117,12 +164,15 @@ type Props = { export default function UserList({ profiles, + channelMembers, selectedIds, currentUserId, teammateNameDisplay, handleSelectProfile, fetchMore, loading, + manageMode = false, + showManageMode = false, showNoResults, term, testID, @@ -139,11 +189,16 @@ export default function UserList({ ], [style, keyboardHeight]); const data = useMemo(() => { + if (profiles.length === 0 && !loading) { + return []; + } + if (term) { return profiles; } - return createProfilesSections(profiles); - }, [term, profiles]); + + return createProfilesSections(intl, profiles, channelMembers); + }, [channelMembers, loading, profiles, term]); const openUserProfile = useCallback(async (profile: UserProfile) => { const {user} = await storeProfile(serverUrl, profile); @@ -162,29 +217,34 @@ export default function UserList({ } }, []); - const renderItem = useCallback(({item, index, section}: ListRenderItemInfo & {section?: SectionListData}) => { + const renderItem = useCallback(({item, index, section}: RenderItemType) => { // The list will re-render when the selection changes because it's passed into the list as extraData const selected = Boolean(selectedIds[item.id]); const canAdd = Object.keys(selectedIds).length < General.MAX_USERS_IN_GM; + const isChAdmin = item.scheme_admin || false; + return ( ); - }, [selectedIds, currentUserId, handleSelectProfile, teammateNameDisplay, tutorialWatched]); + }, [selectedIds, handleSelectProfile, showManageMode, manageMode, teammateNameDisplay, tutorialWatched]); const renderLoading = useCallback(() => { if (!loading) { diff --git a/app/components/user_list_row/index.tsx b/app/components/user_list_row/index.tsx index b908e17a02..85bcc2b919 100644 --- a/app/components/user_list_row/index.tsx +++ b/app/components/user_list_row/index.tsx @@ -12,6 +12,7 @@ import { import {storeProfileLongPressTutorial} from '@actions/app/global'; import CompassIcon from '@components/compass_icon'; +import FormattedText from '@components/formatted_text'; import ProfilePicture from '@components/profile_picture'; import {BotTag, GuestTag} from '@components/tag'; import TouchableWithFeedback from '@components/touchable_with_feedback'; @@ -19,23 +20,27 @@ import TutorialHighlight from '@components/tutorial_highlight'; import TutorialLongPress from '@components/tutorial_highlight/long_press'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {t} from '@i18n'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; import {typography} from '@utils/typography'; import {displayUsername, isGuest} from '@utils/user'; type Props = { + highlight?: boolean; id: string; isMyUser: boolean; - highlight?: boolean; - user: UserProfile; - teammateNameDisplay: string; - testID: string; - onPress?: (user: UserProfile) => void; + isChannelAdmin: boolean; + manageMode: boolean; onLongPress: (user: UserProfile) => void; + onPress?: (user: UserProfile) => void; selectable: boolean; disabled?: boolean; selected: boolean; + showManageMode: boolean; + teammateNameDisplay: string; + testID: string; tutorialWatched?: boolean; + user: UserProfile; } const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { @@ -84,6 +89,15 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { alignItems: 'center', justifyContent: 'center', }, + selectorManage: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + }, + manageText: { + color: changeOpacity(theme.centerChannelColor, 0.64), + ...typography('Body', 100, 'Regular'), + }, tutorial: { top: Platform.select({ios: -74, default: -94}), }, @@ -100,24 +114,26 @@ function UserListRow({ id, isMyUser, highlight, - user, - teammateNameDisplay, - testID, + isChannelAdmin, onPress, onLongPress, - tutorialWatched = false, + manageMode = false, selectable, disabled, selected, + showManageMode = false, + teammateNameDisplay, + testID, + tutorialWatched = false, + user, }: Props) { const theme = useTheme(); - const intl = useIntl(); const isTablet = useIsTablet(); const [showTutorial, setShowTutorial] = useState(false); const [itemBounds, setItemBounds] = useState({startX: 0, startY: 0, endX: 0, endY: 0}); const viewRef = useRef(null); const style = getStyleFromTheme(theme); - const {formatMessage} = intl; + const {formatMessage, locale} = useIntl(); const {username} = user; const startTutorial = () => { @@ -152,13 +168,41 @@ function UserListRow({ }, [highlight, tutorialWatched, isTablet]); const handlePress = useCallback(() => { + if (isMyUser && manageMode) { + return; + } onPress?.(user); - }, [onPress, user]); + }, [onPress, isMyUser, manageMode, user]); const handleLongPress = useCallback(() => { onLongPress?.(user); }, [onLongPress, user]); + const manageModeIcon = useMemo(() => { + if (!showManageMode || isMyUser) { + return null; + } + + const color = changeOpacity(theme.centerChannelColor, 0.64); + const i18nId = isChannelAdmin ? t('mobile.manage_members.admin') : t('mobile.manage_members.member'); + const defaultMessage = isChannelAdmin ? 'Admin' : 'Member'; + + return ( + + + + + ); + }, [isChannelAdmin, showManageMode, theme]); + const onLayout = useCallback(() => { startTutorial(); }, []); @@ -189,7 +233,7 @@ function UserListRow({ }, {username}); } - const teammateDisplay = displayUsername(user, intl.locale, teammateNameDisplay); + const teammateDisplay = displayUsername(user, locale, teammateNameDisplay); const showTeammateDisplay = teammateDisplay !== username; const userItemTestID = `${testID}.${id}`; @@ -257,7 +301,7 @@ function UserListRow({ } - {icon} + {manageMode ? manageModeIcon : icon} {showTutorial && @@ -267,7 +311,7 @@ function UserListRow({ onLayout={onLayout} > diff --git a/app/constants/events.ts b/app/constants/events.ts index 5d34955cef..27ed326847 100644 --- a/app/constants/events.ts +++ b/app/constants/events.ts @@ -15,6 +15,8 @@ export default keyMirror({ LEAVE_TEAM: null, LOADING_CHANNEL_POSTS: null, NOTIFICATION_ERROR: null, + REMOVE_USER_FROM_CHANNEL: null, + MANAGE_USER_CHANGE_ROLE: null, SERVER_LOGOUT: null, SERVER_VERSION_CHANGED: null, SESSION_EXPIRED: null, diff --git a/app/constants/index.ts b/app/constants/index.ts index 8483e6d24e..d7a407f40d 100644 --- a/app/constants/index.ts +++ b/app/constants/index.ts @@ -19,6 +19,7 @@ import Integrations from './integrations'; import Launch from './launch'; import License from './license'; import List from './list'; +import Members from './members'; import Navigation from './navigation'; import Network from './network'; import NotificationLevel from './notification_level'; @@ -57,6 +58,7 @@ export { Launch, License, List, + Members, Navigation, Network, NotificationLevel, diff --git a/app/constants/members.ts b/app/constants/members.ts new file mode 100644 index 0000000000..87fc416b5e --- /dev/null +++ b/app/constants/members.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from '@utils/key_mirror'; +const ManageOptions = keyMirror({ + REMOVE_USER: null, + MAKE_CHANNEL_ADMIN: null, + MAKE_CHANNEL_MEMBER: null, +}); + +export type ManageOptionsTypes = keyof typeof ManageOptions + +export default { + ManageOptions, +}; + diff --git a/app/constants/screens.ts b/app/constants/screens.ts index b4d60e4b9c..6880459e5f 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -33,6 +33,7 @@ export const IN_APP_NOTIFICATION = 'InAppNotification'; export const JOIN_TEAM = 'JoinTeam'; export const LATEX = 'Latex'; export const LOGIN = 'Login'; +export const MANAGE_CHANNEL_MEMBERS = 'ManageChannelMembers'; export const MENTIONS = 'Mentions'; export const MFA = 'MFA'; export const ONBOARDING = 'Onboarding'; @@ -101,6 +102,7 @@ export default { JOIN_TEAM, LATEX, LOGIN, + MANAGE_CHANNEL_MEMBERS, MENTIONS, MFA, ONBOARDING, @@ -148,6 +150,7 @@ export const MODAL_SCREENS_WITHOUT_BACK = new Set([ EDIT_SERVER, FIND_CHANNELS, GALLERY, + MANAGE_CHANNEL_MEMBERS, INVITE, PERMALINK, ]); diff --git a/app/constants/snack_bar.ts b/app/constants/snack_bar.ts index 79a0a5c302..aedc6d7905 100644 --- a/app/constants/snack_bar.ts +++ b/app/constants/snack_bar.ts @@ -9,6 +9,7 @@ export const SNACK_BAR_TYPE = keyMirror({ LINK_COPIED: null, MESSAGE_COPIED: null, MUTE_CHANNEL: null, + REMOVE_CHANNEL_USER: null, UNFAVORITE_CHANNEL: null, UNMUTE_CHANNEL: null, }); @@ -45,6 +46,12 @@ export const SNACK_BAR_CONFIG: Record = { iconName: 'bell-off-outline', canUndo: true, }, + REMOVE_CHANNEL_USER: { + id: t('snack.bar.remove.user'), + defaultMessage: '1 member was removed from the channel', + iconName: 'check', + canUndo: true, + }, UNFAVORITE_CHANNEL: { id: t('snack.bar.unfavorite.channel'), defaultMessage: 'This channel was unfavorited', diff --git a/app/queries/servers/role.ts b/app/queries/servers/role.ts index 358cd9de62..58417da697 100644 --- a/app/queries/servers/role.ts +++ b/app/queries/servers/role.ts @@ -88,8 +88,8 @@ export function observePermissionForPost(database: Database, post: PostModel, us ); } -export function observeCanManageChannelMembers(database: Database, post: PostModel, user: UserModel) { - return observeChannel(database, post.channelId).pipe( +export function observeCanManageChannelMembers(database: Database, channelId: string, user: UserModel) { + return observeChannel(database, channelId).pipe( switchMap((c) => { if (!c || c.deleteAt !== 0 || isDMorGM(c) || c.name === General.DEFAULT_CHANNEL) { return of$(false); diff --git a/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx b/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx index 895b4cc561..f9069c2c1e 100644 --- a/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx +++ b/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx @@ -74,7 +74,7 @@ const DirectChannel = ({channel, currentUserId, isBot, members, theme}: Props) = useEffect(() => { const channelMembers = members?.filter((m) => m.userId !== currentUserId); if (!channelMembers?.length) { - fetchProfilesInChannel(serverUrl, channel.id, currentUserId, false); + fetchProfilesInChannel(serverUrl, channel.id, currentUserId, undefined, false); } }, []); diff --git a/app/screens/channel_info/options/index.tsx b/app/screens/channel_info/options/index.tsx index ef47a4d7ef..d2c856ec6f 100644 --- a/app/screens/channel_info/options/index.tsx +++ b/app/screens/channel_info/options/index.tsx @@ -9,10 +9,9 @@ import {isTypeDMorGM} from '@utils/channel'; import EditChannel from './edit_channel'; import IgnoreMentions from './ignore_mentions'; +import Members from './members'; import PinnedMessages from './pinned_messages'; -// import Members from './members'; - type Props = { channelId: string; type?: ChannelType; @@ -29,11 +28,9 @@ const Options = ({channelId, type, callsEnabled}: Props) => { } {/**/} - {/* Add back in after MM-47653 is resolved. https://mattermost.atlassian.net/browse/MM-47653 {type !== General.DM_CHANNEL && } - */} {callsEnabled && !isDMorGM && // if calls is not enabled, copy link will show in the channel actions { const info = observeChannelInfo(database, channelId); + + const displayName = observeChannel(database, channelId).pipe( + switchMap((c) => of$(c?.displayName))); + const count = info.pipe( switchMap((i) => of$(i?.memberCount || 0)), ); return { + displayName, count, }; }); diff --git a/app/screens/channel_info/options/members/members.tsx b/app/screens/channel_info/options/members/members.tsx index 778abbb1b5..5af7286edc 100644 --- a/app/screens/channel_info/options/members/members.tsx +++ b/app/screens/channel_info/options/members/members.tsx @@ -7,20 +7,32 @@ import {Platform} from 'react-native'; import OptionItem from '@components/option_item'; import {Screens} from '@constants'; +import {useTheme} from '@context/theme'; import {goToScreen} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity} from '@utils/theme'; type Props = { channelId: string; + displayName: string; count: number; } -const Members = ({channelId, count}: Props) => { +const Members = ({displayName, channelId, count}: Props) => { const {formatMessage} = useIntl(); + const theme = useTheme(); const title = formatMessage({id: 'channel_info.members', defaultMessage: 'Members'}); - const goToChannelMembers = preventDoubleTap(() => { - goToScreen(Screens.CHANNEL_ADD_PEOPLE, title, {channelId}); + const options = { + topBar: { + subtitle: { + color: changeOpacity(theme.sidebarHeaderTextColor, 0.72), + text: displayName, + }, + }, + }; + + goToScreen(Screens.MANAGE_CHANNEL_MEMBERS, title, {channelId}, options); }); return ( diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 051d446f13..957e320670 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -149,6 +149,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { case Screens.LOGIN: screen = withIntl(require('@screens/login').default); break; + case Screens.MANAGE_CHANNEL_MEMBERS: + screen = withServerDatabase(require('@screens/manage_channel_members').default); + break; case Screens.MFA: screen = withIntl(require('@screens/mfa').default); break; diff --git a/app/screens/invite/invite.tsx b/app/screens/invite/invite.tsx index 8f8640fea1..dda58a93a4 100644 --- a/app/screens/invite/invite.tsx +++ b/app/screens/invite/invite.tsx @@ -138,7 +138,7 @@ export default function Invite({ return; } - const {data} = await searchProfiles(serverUrl, searchTerm.toLowerCase()); + const {data} = await searchProfiles(serverUrl, searchTerm.toLowerCase(), {}); const results: SearchResult[] = data ?? []; if (!results.length && isEmail(searchTerm.trim())) { diff --git a/app/screens/manage_channel_members/index.tsx b/app/screens/manage_channel_members/index.tsx new file mode 100644 index 0000000000..ae1d739069 --- /dev/null +++ b/app/screens/manage_channel_members/index.tsx @@ -0,0 +1,40 @@ +// 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 {of as of$, combineLatest, switchMap} from 'rxjs'; + +import {Permissions, Tutorial} from '@constants'; +import {observeTutorialWatched} from '@queries/app/global'; +import {observeCurrentChannel} from '@queries/servers/channel'; +import {observeCanManageChannelMembers, observePermissionForChannel} from '@queries/servers/role'; +import {observeCurrentChannelId, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system'; +import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user'; + +import ManageChannelMembers from './manage_channel_members'; + +import type {WithDatabaseArgs} from '@typings/database/database'; + +const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { + const currentUser = observeCurrentUser(database); + const currentChannelId = observeCurrentChannelId(database); + const currentChannel = observeCurrentChannel(database); + + const canManageAndRemoveMembers = combineLatest([currentChannelId, currentUser]).pipe( + switchMap(([cId, u]) => (cId && u ? observeCanManageChannelMembers(database, cId, u) : of$(false)))); + + const canChangeMemberRoles = combineLatest([currentChannel, currentUser, canManageAndRemoveMembers]).pipe( + switchMap(([c, u, m]) => (of$(c) && of$(u) && of$(m) && observePermissionForChannel(database, c, u, Permissions.MANAGE_CHANNEL_ROLES, true)))); + + return { + currentUserId: observeCurrentUserId(database), + currentTeamId: observeCurrentTeamId(database), + canManageAndRemoveMembers, + teammateNameDisplay: observeTeammateNameDisplay(database), + tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS), + canChangeMemberRoles, + }; +}); + +export default withDatabase(enhanced(ManageChannelMembers)); diff --git a/app/screens/manage_channel_members/manage_channel_members.tsx b/app/screens/manage_channel_members/manage_channel_members.tsx new file mode 100644 index 0000000000..81e1199e27 --- /dev/null +++ b/app/screens/manage_channel_members/manage_channel_members.tsx @@ -0,0 +1,281 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {defineMessages, useIntl} from 'react-intl'; +import {DeviceEventEmitter, Keyboard, Platform, StyleSheet, View} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; + +import {fetchChannelMemberships} from '@actions/remote/channel'; +import {fetchUsersByIds, searchProfiles} from '@actions/remote/user'; +import Search from '@components/search'; +import UserList from '@components/user_list'; +import {Events, General, Screens} from '@constants'; +import {useServerUrl} from '@context/server'; +import {useTheme} from '@context/theme'; +import {debounce} from '@helpers/api/general'; +import useNavButtonPressed from '@hooks/navigation_button_pressed'; +import {t} from '@i18n'; +import {openAsBottomSheet, setButtons} from '@screens/navigation'; +import NavigationStore from '@store/navigation_store'; +import {showRemoveChannelUserSnackbar} from '@utils/snack_bar'; +import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme'; +import {filterProfilesMatchingTerm} from '@utils/user'; + +import type {AvailableScreens} from '@typings/screens/navigation'; + +type Props = { + canManageAndRemoveMembers: boolean; + channelId: string; + componentId: AvailableScreens; + currentTeamId: string; + currentUserId: string; + teammateNameDisplay: string; + tutorialWatched: boolean; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + searchBar: { + marginLeft: 12, + marginRight: Platform.select({ios: 4, default: 12}), + marginVertical: 12, + }, +}); + +const messages = defineMessages({ + button_manage: { + id: t('mobile.manage_members.manage'), + defaultMessage: 'Manage', + }, + button_done: { + id: t('mobile.manage_members.done'), + defaultMessage: 'Done', + }, +}); + +const MANAGE_BUTTON = 'manage-button'; +const EMPTY: UserProfile[] = []; +const EMPTY_MEMBERS: ChannelMembership[] = []; +const EMPTY_IDS = {}; +const {USER_PROFILE} = Screens; +const CLOSE_BUTTON_ID = 'close-user-profile'; + +export default function ManageChannelMembers({ + canManageAndRemoveMembers, + channelId, + componentId, + currentTeamId, + currentUserId, + teammateNameDisplay, + tutorialWatched, +}: Props) { + const serverUrl = useServerUrl(); + const theme = useTheme(); + const {formatMessage} = useIntl(); + + const searchTimeoutId = useRef(null); + const mounted = useRef(false); + + const [isManageMode, setIsManageMode] = useState(false); + const [profiles, setProfiles] = useState(EMPTY); + const [channelMembers, setChannelMembers] = useState(EMPTY_MEMBERS); + const [searchResults, setSearchResults] = useState(EMPTY); + const [loading, setLoading] = useState(false); + const [term, setTerm] = useState(''); + + const loadedProfiles = (users: UserProfile[], members: ChannelMembership[]) => { + if (mounted.current) { + setLoading(false); + setProfiles(users); + setChannelMembers(members); + } + }; + + const clearSearch = useCallback(() => { + setTerm(''); + setSearchResults(EMPTY); + }, []); + + const getProfiles = useCallback(debounce(async () => { + const hasTerm = Boolean(term); + if (!loading && !hasTerm && mounted.current) { + setLoading(true); + const options = {sort: 'admin', active: true}; + const {users, members} = await fetchChannelMemberships(serverUrl, channelId, options, true); + if (users.length) { + loadedProfiles(users, members); + } + setLoading(false); + } + }, 100), [channelId, loading, serverUrl, term]); + + const handleSelectProfile = useCallback(async (profile: UserProfile) => { + await fetchUsersByIds(serverUrl, [profile.id]); + const title = formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const props = { + channelId, + closeButtonId: CLOSE_BUTTON_ID, + location: USER_PROFILE, + manageMode: isManageMode, + userId: profile.id, + canManageAndRemoveMembers, + }; + + Keyboard.dismiss(); + openAsBottomSheet({screen: USER_PROFILE, title, theme, closeButtonId: CLOSE_BUTTON_ID, props}); + }, [canManageAndRemoveMembers, channelId, isManageMode]); + + const searchUsers = useCallback(async (searchTerm: string) => { + const lowerCasedTerm = searchTerm.toLowerCase(); + setLoading(true); + + const options: SearchUserOptions = {team_id: currentTeamId, in_channel_id: channelId, allow_inactive: false}; + const {data = EMPTY} = await searchProfiles(serverUrl, lowerCasedTerm, options); + + setSearchResults(data); + setLoading(false); + }, [serverUrl, channelId, currentTeamId]); + + const search = useCallback(() => { + searchUsers(term); + }, [searchUsers, term]); + + const onSearch = useCallback((text: string) => { + if (!text) { + clearSearch(); + return; + } + + setTerm(text); + if (searchTimeoutId.current) { + clearTimeout(searchTimeoutId.current); + } + + searchTimeoutId.current = setTimeout(() => { + searchUsers(text); + }, General.SEARCH_TIMEOUT_MILLISECONDS); + }, [searchUsers, clearSearch]); + + const updateNavigationButtons = useCallback((manage: boolean) => { + setButtons(componentId, { + rightButtons: [{ + color: theme.sidebarHeaderTextColor, + enabled: true, + id: MANAGE_BUTTON, + showAsAction: 'always', + testID: 'manage_members.button', + text: formatMessage(manage ? messages.button_done : messages.button_manage), + }], + }); + }, [theme.sidebarHeaderTextColor]); + + const toggleManageEnabled = useCallback(() => { + updateNavigationButtons(!isManageMode); + setIsManageMode((prev) => !prev); + }, [isManageMode, updateNavigationButtons]); + + const handleRemoveUser = useCallback(async (userId: string) => { + const pIndex = profiles.findIndex((user) => user.id === userId); + const mIndex = channelMembers.findIndex((m) => m.user_id === userId); + if (pIndex !== -1) { + const newProfiles = [...profiles]; + newProfiles.splice(pIndex, 1); + setProfiles(newProfiles); + + const newMembers = [...channelMembers]; + newMembers.splice(mIndex, 1); + setChannelMembers(newMembers); + + await NavigationStore.waitUntilScreensIsRemoved(USER_PROFILE); + showRemoveChannelUserSnackbar(); + } + }, [profiles, channelMembers]); + + const handleUserChangeRole = useCallback(async ({userId, schemeAdmin}: {userId: string; schemeAdmin: boolean}) => { + const clone = channelMembers.map((m) => { + if (m.user_id === userId) { + m.scheme_admin = schemeAdmin; + return m; + } + return m; + }); + + setChannelMembers(clone); + }, [channelMembers]); + + const data = useMemo(() => { + const isSearch = Boolean(term); + if (isSearch) { + return filterProfilesMatchingTerm(searchResults, term); + } + return profiles; + }, [term, searchResults, profiles]); + + useNavButtonPressed(MANAGE_BUTTON, componentId, toggleManageEnabled, [toggleManageEnabled]); + + useEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + }; + }, []); + + useEffect(() => { + if (canManageAndRemoveMembers) { + updateNavigationButtons(false); + } + }, [canManageAndRemoveMembers]); + + useEffect(() => { + const removeUserListener = DeviceEventEmitter.addListener(Events.REMOVE_USER_FROM_CHANNEL, handleRemoveUser); + const changeUserRoleListener = DeviceEventEmitter.addListener(Events.MANAGE_USER_CHANGE_ROLE, handleUserChangeRole); + return (() => { + removeUserListener?.remove(); + changeUserRoleListener?.remove(); + }); + }, [handleRemoveUser, handleUserChangeRole]); + + return ( + + + + + + {/* TODO: https://mattermost.atlassian.net/browse/MM-48830 */} + {/* fix flashing No Results page when results are present */} + + + ); +} diff --git a/app/screens/snack_bar/index.tsx b/app/screens/snack_bar/index.tsx index ec218850b9..a1e4404e05 100644 --- a/app/screens/snack_bar/index.tsx +++ b/app/screens/snack_bar/index.tsx @@ -40,7 +40,7 @@ const SNACK_BAR_WIDTH = 96; const SNACK_BAR_HEIGHT = 56; const SNACK_BAR_BOTTOM_RATIO = 0.04; -const caseScreens: AvailableScreens[] = [Screens.PERMALINK, Screens.MENTIONS, Screens.SAVED_MESSAGES]; +const caseScreens: AvailableScreens[] = [Screens.PERMALINK, Screens.MANAGE_CHANNEL_MEMBERS, Screens.MENTIONS, Screens.SAVED_MESSAGES]; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { diff --git a/app/screens/user_profile/index.ts b/app/screens/user_profile/index.ts index 7ef852886b..47dda6e597 100644 --- a/app/screens/user_profile/index.ts +++ b/app/screens/user_profile/index.ts @@ -3,15 +3,16 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; -import {of as of$} from 'rxjs'; +import {of as of$, combineLatest} from 'rxjs'; import {map, switchMap} from 'rxjs/operators'; -import {General, Preferences} from '@constants'; +import {General, Permissions, Preferences} from '@constants'; import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference'; import {observeChannel} from '@queries/servers/channel'; import {queryDisplayNamePreferences} from '@queries/servers/preference'; +import {observeCanManageChannelMembers, observePermissionForChannel} from '@queries/servers/role'; import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system'; -import {observeTeammateNameDisplay, observeUser, observeUserIsChannelAdmin, observeUserIsTeamAdmin} from '@queries/servers/user'; +import {observeTeammateNameDisplay, observeCurrentUser, observeUser, observeUserIsChannelAdmin, observeUserIsTeamAdmin} from '@queries/servers/user'; import {isSystemAdmin} from '@utils/user'; import UserProfile from './user_profile'; @@ -24,11 +25,15 @@ type EnhancedProps = WithDatabaseArgs & { } const enhanced = withObservables([], ({channelId, database, userId}: EnhancedProps) => { + const currentUser = observeCurrentUser(database); const currentUserId = observeCurrentUserId(database); const channel = channelId ? observeChannel(database, channelId) : of$(undefined); const user = observeUser(database, userId); const teammateDisplayName = observeTeammateNameDisplay(database); const isChannelAdmin = channelId ? observeUserIsChannelAdmin(database, userId, channelId) : of$(false); + const isDefaultChannel = channel ? channel.pipe( + switchMap((c) => of$(c?.name === General.DEFAULT_CHANNEL)), + ) : of$(false); const isDirectMessage = channelId ? channel.pipe( switchMap((c) => of$(c?.type === General.DM_CHANNEL)), ) : of$(false); @@ -42,12 +47,21 @@ const enhanced = withObservables([], ({channelId, database, userId}: EnhancedPro const isMilitaryTime = preferences.pipe(map((prefs) => getDisplayNamePreferenceAsBool(prefs, Preferences.USE_MILITARY_TIME))); const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses'); + // can remove member + const canManageAndRemoveMembers = combineLatest([channel, currentUser]).pipe( + switchMap(([c, u]) => (c && u ? observeCanManageChannelMembers(database, c.id, u) : of$(false)))); + + const canChangeMemberRoles = combineLatest([channel, currentUser, canManageAndRemoveMembers]).pipe( + switchMap(([c, u, m]) => (of$(c?.id) && of$(u) && of$(m) && observePermissionForChannel(database, c, u, Permissions.MANAGE_CHANNEL_ROLES, true)))); + return { + canManageAndRemoveMembers, currentUserId, enablePostIconOverride, enablePostUsernameOverride, isChannelAdmin, isCustomStatusEnabled, + isDefaultChannel, isDirectMessage, isMilitaryTime, isSystemAdmin: systemAdmin, @@ -55,6 +69,7 @@ const enhanced = withObservables([], ({channelId, database, userId}: EnhancedPro teamId, teammateDisplayName, user, + canChangeMemberRoles, }; }); diff --git a/app/screens/user_profile/manage_user_options.tsx b/app/screens/user_profile/manage_user_options.tsx new file mode 100644 index 0000000000..0371f4ccbf --- /dev/null +++ b/app/screens/user_profile/manage_user_options.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {View} from 'react-native'; + +import ManageMembersLabel from '@components/channel_actions/manage_members_label'; +import {Members} from '@constants'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +export const DIVIDER_MARGIN = 8; +const {MAKE_CHANNEL_ADMIN, MAKE_CHANNEL_MEMBER, REMOVE_USER} = Members.ManageOptions; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + divider: { + alignSelf: 'center', + backgroundColor: changeOpacity(theme.centerChannelColor, 0.16), + height: 1, + marginVertical: DIVIDER_MARGIN, + paddingHorizontal: 20, + width: '100%', + }, + }; +}); + +type Props = { + channelId: string; + isDefaultChannel: boolean; + isChannelAdmin: boolean; + canChangeMemberRoles: boolean; + userId: string; +} + +const ManageUserOptions = ({channelId, isChannelAdmin, isDefaultChannel, userId, canChangeMemberRoles}: Props) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + return ( + <> + + {canChangeMemberRoles && + + } + + + ); +}; + +export default ManageUserOptions; diff --git a/app/screens/user_profile/title/avatar.tsx b/app/screens/user_profile/title/avatar.tsx index 26c364853b..41e7be2569 100644 --- a/app/screens/user_profile/title/avatar.tsx +++ b/app/screens/user_profile/title/avatar.tsx @@ -3,7 +3,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import FastImage from 'react-native-fast-image'; import Animated from 'react-native-reanimated'; @@ -18,19 +18,26 @@ const AnimatedFastImage = Animated.createAnimatedComponent(FastImage); type Props = { enablePostIconOverride: boolean; forwardRef?: React.RefObject; + imageSize?: number; user: UserModel; userIconOverride?: string; } -const styles = StyleSheet.create({ - avatar: { - borderRadius: 48, - height: 96, - width: 96, - }, -}); +const DEFAULT_IMAGE_SIZE = 96; + +const getStyles = (size?: number) => { + return StyleSheet.create({ + avatar: { + borderRadius: 48, + height: size || DEFAULT_IMAGE_SIZE, + width: size || DEFAULT_IMAGE_SIZE, + }, + }); +}; + +const UserProfileAvatar = ({enablePostIconOverride, forwardRef, imageSize, user, userIconOverride}: Props) => { + const styles = useMemo(() => getStyles(imageSize), [imageSize]); -const UserProfileAvatar = ({enablePostIconOverride, forwardRef, user, userIconOverride}: Props) => { if (enablePostIconOverride && userIconOverride) { return ( @@ -48,7 +55,7 @@ const UserProfileAvatar = ({enablePostIconOverride, forwardRef, user, userIconOv author={user} forwardRef={forwardRef} showStatus={true} - size={96} + size={imageSize || DEFAULT_IMAGE_SIZE} statusSize={24} testID={`user_profile_avatar.${user.id}.profile_picture`} /> diff --git a/app/screens/user_profile/title/index.tsx b/app/screens/user_profile/title/index.tsx index 4253f7bc6d..137813e3af 100644 --- a/app/screens/user_profile/title/index.tsx +++ b/app/screens/user_profile/title/index.tsx @@ -25,6 +25,8 @@ import type {GalleryItemType} from '@typings/screens/gallery'; type Props = { enablePostIconOverride: boolean; enablePostUsernameOverride: boolean; + headerText?: string; + imageSize?: number; isChannelAdmin: boolean; isSystemAdmin: boolean; isTeamAdmin: boolean; @@ -34,6 +36,8 @@ type Props = { usernameOverride?: string; } +export const HEADER_TEXT_HEIGHT = 30; + const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { flexDirection: 'row', @@ -52,14 +56,20 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ color: changeOpacity(theme.centerChannelColor, 0.64), ...typography('Body', 200), }, + heading: { + height: HEADER_TEXT_HEIGHT, + color: theme.centerChannelColor, + marginBottom: 20, + ...typography('Heading', 600, 'SemiBold'), + }, tablet: { marginTop: 20, }, })); const UserProfileTitle = ({ - enablePostIconOverride, enablePostUsernameOverride, - isChannelAdmin, isSystemAdmin, isTeamAdmin, + enablePostIconOverride, enablePostUsernameOverride, headerText, + imageSize, isChannelAdmin, isSystemAdmin, isTeamAdmin, teammateDisplayName, user, userIconOverride, usernameOverride, }: Props) => { const galleryIdentifier = `${user.id}-avatarPreview`; @@ -118,45 +128,56 @@ const UserProfileTitle = ({ const prefix = hideUsername ? '@' : ''; return ( - - - - - - - - - - + <> + {headerText && - {`${prefix}${displayName}`} + {headerText} - {!hideUsername && - - {`@${user.username}`} - - } + } + + + + + + + + + + + + {`${prefix}${displayName}`} + + {!hideUsername && + + {`@${user.username}`} + + } + - + ); }; diff --git a/app/screens/user_profile/user_info.tsx b/app/screens/user_profile/user_info.tsx new file mode 100644 index 0000000000..7905d7f7d5 --- /dev/null +++ b/app/screens/user_profile/user_info.tsx @@ -0,0 +1,54 @@ +// 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 {getUserCustomStatus} from '@utils/user'; + +import UserProfileCustomStatus from './custom_status'; +import UserProfileLabel from './label'; + +import type {UserModel} from '@database/models/server'; + +type Props = { + localTime?: string; + showCustomStatus: boolean; + showLocalTime: boolean; + showNickname: boolean; + showPosition: boolean; + user: UserModel; +} + +const UserInfo = ({localTime, showCustomStatus, showLocalTime, showNickname, showPosition, user}: Props) => { + const {formatMessage} = useIntl(); + const customStatus = getUserCustomStatus(user); + + return ( + <> + {showCustomStatus && } + {showNickname && + + } + {showPosition && + + } + {showLocalTime && + + } + + ); +}; + +export default UserInfo; diff --git a/app/screens/user_profile/user_profile.tsx b/app/screens/user_profile/user_profile.tsx index dcaf576faf..20187bbbe6 100644 --- a/app/screens/user_profile/user_profile.tsx +++ b/app/screens/user_profile/user_profile.tsx @@ -4,37 +4,41 @@ import moment from 'moment'; import mtz from 'moment-timezone'; import React, {useEffect, useMemo} from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessages, useIntl} from 'react-intl'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {fetchTeamAndChannelMembership} from '@actions/remote/user'; import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; -import {getLocaleFromLanguage} from '@i18n'; +import {getLocaleFromLanguage, t} from '@i18n'; import BottomSheet from '@screens/bottom_sheet'; import {bottomSheetSnapPoint} from '@utils/helpers'; import {getUserCustomStatus, getUserTimezone, isCustomStatusExpired} from '@utils/user'; -import UserProfileCustomStatus from './custom_status'; -import UserProfileLabel from './label'; +import ManageUserOptions, {DIVIDER_MARGIN} from './manage_user_options'; import UserProfileOptions, {OptionsType} from './options'; -import UserProfileTitle from './title'; +import UserProfileTitle, {HEADER_TEXT_HEIGHT} from './title'; +import UserInfo from './user_info'; import type UserModel from '@typings/database/models/servers/user'; import type {AvailableScreens} from '@typings/screens/navigation'; type Props = { + canChangeMemberRoles: boolean; channelId?: string; closeButtonId: string; currentUserId: string; enablePostIconOverride: boolean; enablePostUsernameOverride: boolean; isChannelAdmin: boolean; + canManageAndRemoveMembers?: boolean; isCustomStatusEnabled: boolean; isDirectMessage: boolean; + isDefaultChannel: boolean; isMilitaryTime: boolean; isSystemAdmin: boolean; isTeamAdmin: boolean; + manageMode?: boolean; location: AvailableScreens; teamId: string; teammateDisplayName: string; @@ -47,13 +51,39 @@ const TITLE_HEIGHT = 118; const OPTIONS_HEIGHT = 82; const SINGLE_OPTION_HEIGHT = 68; const LABEL_HEIGHT = 58; +const EXTRA_HEIGHT = 60; +const MANAGE_ICON_HEIGHT = 72; + +const messages = defineMessages({ + manageMember: { + id: t('mobile.manage_members.manage_member'), + defaultMessage: 'Manage member', + }, +}); const channelContextScreens: AvailableScreens[] = [Screens.CHANNEL, Screens.THREAD]; const UserProfile = ({ - channelId, closeButtonId, currentUserId, enablePostIconOverride, enablePostUsernameOverride, - isChannelAdmin, isCustomStatusEnabled, isDirectMessage, isMilitaryTime, isSystemAdmin, isTeamAdmin, - location, teamId, teammateDisplayName, - user, userIconOverride, usernameOverride, + canChangeMemberRoles, + canManageAndRemoveMembers, + channelId, + closeButtonId, + currentUserId, + enablePostIconOverride, + enablePostUsernameOverride, + isChannelAdmin, + isCustomStatusEnabled, + isDefaultChannel, + isDirectMessage, + isMilitaryTime, + isSystemAdmin, + isTeamAdmin, + location, + manageMode = false, + teamId, + teammateDisplayName, + user, + userIconOverride, + usernameOverride, }: Props) => { const {formatMessage, locale} = useIntl(); const serverUrl = useServerUrl(); @@ -76,37 +106,48 @@ const UserProfile = ({ } const showCustomStatus = isCustomStatusEnabled && Boolean(customStatus) && !user.isBot && !isCustomStatusExpired(user); - const showUserProfileOptions = (!isDirectMessage || !channelContext) && !override; - const showNickname = Boolean(user.nickname) && !override && !user.isBot; - const showPosition = Boolean(user.position) && !override && !user.isBot; - const showLocalTime = Boolean(localTime) && !override && !user.isBot; + const showUserProfileOptions = (!isDirectMessage || !channelContext) && !override && !manageMode; + const showNickname = Boolean(user.nickname) && !override && !user.isBot && !manageMode; + const showPosition = Boolean(user.position) && !override && !user.isBot && !manageMode; + const showLocalTime = Boolean(localTime) && !override && !user.isBot && !manageMode; + + const headerText = manageMode ? formatMessage(messages.manageMember) : undefined; const snapPoints = useMemo(() => { let title = TITLE_HEIGHT; + + if (headerText) { + title += HEADER_TEXT_HEIGHT; + } + if (showUserProfileOptions) { title += showOptions === 'all' ? OPTIONS_HEIGHT : SINGLE_OPTION_HEIGHT; } - let labels = 0; - if (showCustomStatus) { - labels += 1; + const optionsCount = [ + showCustomStatus, + showNickname, + showPosition, + showLocalTime, + ].reduce((acc, v) => { + return v ? acc + 1 : acc; + }, 0); + + if (manageMode) { + title += DIVIDER_MARGIN * 2; + if (canChangeMemberRoles) { + title += SINGLE_OPTION_HEIGHT; // roles button + } + if (canManageAndRemoveMembers) { + title += SINGLE_OPTION_HEIGHT; // roles button + } } - if (showNickname) { - labels += 1; - } - - if (showPosition) { - labels += 1; - } - - if (showLocalTime) { - labels += 1; - } + const extraHeight = manageMode ? 0 : EXTRA_HEIGHT; return [ 1, - bottomSheetSnapPoint(labels, LABEL_HEIGHT, bottom) + title, + bottomSheetSnapPoint(optionsCount, LABEL_HEIGHT, bottom) + title + extraHeight, ]; }, [ showUserProfileOptions, showCustomStatus, showNickname, @@ -125,6 +166,8 @@ const UserProfile = ({ } - {showCustomStatus && } - {showNickname && - + {!manageMode && + } - {showPosition && - - } - {showLocalTime && - + {manageMode && channelId && (canManageAndRemoveMembers || canChangeMemberRoles) && + } ); diff --git a/app/utils/snack_bar/index.ts b/app/utils/snack_bar/index.ts index 62f8afdbec..1f1c08b29d 100644 --- a/app/utils/snack_bar/index.ts +++ b/app/utils/snack_bar/index.ts @@ -30,3 +30,10 @@ export const showFavoriteChannelSnackbar = (favorited: boolean, onAction: () => barType: favorited ? SNACK_BAR_TYPE.FAVORITE_CHANNEL : SNACK_BAR_TYPE.UNFAVORITE_CHANNEL, }); }; + +export const showRemoveChannelUserSnackbar = () => { + return showSnackBar({ + barType: SNACK_BAR_TYPE.REMOVE_CHANNEL_USER, + sourceScreen: Screens.MANAGE_CHANNEL_MEMBERS, + }); +}; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index c6ea19b1a2..44e484b93e 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1,976 +1,991 @@ { - "about.date": "Build Date:", - "about.enterpriseEditione1": "Enterprise Edition", - "about.enterpriseEditionLearn": "Learn more about Enterprise Edition at ", - "about.enterpriseEditionSt": "Modern communication from behind your firewall.", - "about.hash": "Build Hash:", - "about.hashee": "EE Build Hash:", - "about.teamEditionLearn": "Join the Mattermost community at", - "about.teamEditionSt": "All your team communication in one place, instantly searchable and accessible anywhere.", - "about.teamEditiont0": "Team Edition", - "about.teamEditiont1": "Enterprise Edition", - "account.logout": "Log out", - "account.logout_from": "Log out of {serverName}", - "account.settings": "Settings", - "account.your_profile": "Your Profile", - "alert.channel_deleted.description": "The channel {displayName} has been archived.", - "alert.channel_deleted.title": "Archived channel", - "alert.push_proxy_error.description": "Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.", - "alert.push_proxy_error.title": "Notifications cannot be received from this server", - "alert.push_proxy_unknown.description": "This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.", - "alert.push_proxy_unknown.title": "Notifications could not be received from this server", - "alert.push_proxy.button": "Okay", - "alert.removed_from_channel.description": "You have been removed from channel {displayName}.", - "alert.removed_from_channel.title": "Removed from channel", - "alert.removed_from_team.description": "You have been removed from team {displayName}.", - "alert.removed_from_team.title": "Removed from team", - "announcment_banner.dismiss": "Dismiss announcement", - "announcment_banner.okay": "Okay", - "api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.", - "api.channel.add_member.added": "{addedUsername} added to the channel by {username}.", - "api.channel.guest_join_channel.post_and_forget": "{username} joined the channel as a guest.", - "apps.error": "Error: {error}", - "apps.error.command.field_missing": "Required fields missing: `{fieldName}`.", - "apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.", - "apps.error.command.same_option": "Option repeated for field `{fieldName}`: `{option}`.", - "apps.error.command.same_user": "User repeated for field `{fieldName}`: `{option}`.", - "apps.error.command.unknown_channel": "Unknown channel for field `{fieldName}`: `{option}`.", - "apps.error.command.unknown_option": "Unknown option for field `{fieldName}`: `{option}`.", - "apps.error.command.unknown_user": "Unknown user for field `{fieldName}`: `{option}`.", - "apps.error.form.no_form": "`form` is not defined.", - "apps.error.form.no_lookup": "`lookup` is not defined.", - "apps.error.form.no_source": "`source` is not defined.", - "apps.error.form.no_submit": "`submit` is not defined", - "apps.error.form.refresh": "There has been an error fetching the select fields. Contact the app developer. Details: {details}", - "apps.error.form.refresh_no_refresh": "Called refresh on no refresh field.", - "apps.error.form.submit.pretext": "There has been an error submitting the modal. Contact the app developer. Details: {details}", - "apps.error.lookup.error_preparing_request": "Error preparing lookup request: {errorMessage}", - "apps.error.malformed_binding": "This binding is not properly formed. Contact the App developer.", - "apps.error.parser": "Parsing error: {error}", - "apps.error.parser.empty_value": "Empty values are not allowed.", - "apps.error.parser.execute_non_leaf": "You must select a subcommand.", - "apps.error.parser.missing_binding": "Missing command bindings.", - "apps.error.parser.missing_field_value": "Field value is missing.", - "apps.error.parser.missing_list_end": "Expected list closing token.", - "apps.error.parser.missing_quote": "Matching double quote expected before end of input.", - "apps.error.parser.missing_source": "Form has neither submit nor source.", - "apps.error.parser.missing_submit": "No submit call in binding or form.", - "apps.error.parser.missing_tick": "Matching tick quote expected before end of input.", - "apps.error.parser.multiple_equal": "Multiple `=` signs are not allowed.", - "apps.error.parser.no_argument_pos_x": "Unable to identify argument.", - "apps.error.parser.no_bindings": "No command bindings.", - "apps.error.parser.no_form": "No form found.", - "apps.error.parser.no_match": "`{command}`: No matching command found in this workspace.", - "apps.error.parser.no_slash_start": "Command must start with a `/`.", - "apps.error.parser.unexpected_character": "Unexpected character.", - "apps.error.parser.unexpected_comma": "Unexpected comma.", - "apps.error.parser.unexpected_error": "Unexpected error.", - "apps.error.parser.unexpected_flag": "Command does not accept flag `{flagName}`.", - "apps.error.parser.unexpected_squared_bracket": "Unexpected list opening.", - "apps.error.parser.unexpected_state": "Unreachable: Unexpected state in matchBinding: `{state}`.", - "apps.error.parser.unexpected_whitespace": "Unreachable: Unexpected whitespace.", - "apps.error.responses.form.no_form": "Response type is `form`, but no form was included in the response.", - "apps.error.responses.navigate.no_url": "Response type is `navigate`, but no url was included in response.", - "apps.error.responses.unexpected_error": "Received an unexpected error.", - "apps.error.responses.unexpected_type": "App response type was not expected. Response type: {type}", - "apps.error.responses.unknown_field_error": "Received an error for an unknown field. Field name: `{field}`. Error: `{error}`.", - "apps.error.responses.unknown_type": "App response type not supported. Response type: {type}.", - "apps.error.unknown": "Unknown error occurred.", - "apps.suggestion.dynamic.error": "Dynamic select error", - "apps.suggestion.errors.parser_error": "Parsing error", - "apps.suggestion.no_dynamic": "No data was returned for dynamic suggestions", - "apps.suggestion.no_static": "No matching options.", - "apps.suggestion.no_suggestion": "No matching suggestions.", - "archivedChannelMessage": "You are viewing an **archived channel**. New messages cannot be posted.", - "autocomplete_selector.unknown_channel": "Unknown channel", - "browse_channels.archivedChannels": "Archived Channels", - "browse_channels.dropdownTitle": "Show", - "browse_channels.noMore": "No more channels to join", - "browse_channels.publicChannels": "Public Channels", - "browse_channels.sharedChannels": "Shared Channels", - "browse_channels.showArchivedChannels": "Show: Archived Channels", - "browse_channels.showPublicChannels": "Show: Public Channels", - "browse_channels.showSharedChannels": "Show: Shared Channels", - "browse_channels.title": "Browse channels", - "camera_type.photo.option": "Capture Photo", - "camera_type.video.option": "Record Video", - "center_panel.archived.closeChannel": "Close Channel", - "channel_header.directchannel.you": "{displayName} (you)", - "channel_header.info": "View info", - "channel_header.member_count": "{count, plural, one {# member} other {# members}}", - "channel_info.alert_retry": "Try Again", - "channel_info.alertNo": "No", - "channel_info.alertYes": "Yes", - "channel_info.archive": "Archive Channel", - "channel_info.archive_description.can_view_archived": "This will archive the channel from the team. Channel contents will still be accessible by channel members.\n\nAre you sure you wish to archive the {term} {name}?", - "channel_info.archive_description.cannot_view_archived": "This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.\n\nAre you sure you wish to archive the {term} {name}?", - "channel_info.archive_failed": "An error occurred trying to archive the channel {displayName}", - "channel_info.archive_title": "Archive {term}", - "channel_info.close": "Close", - "channel_info.close_dm": "Close direct message", - "channel_info.close_dm_channel": "Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.", - "channel_info.close_gm": "Close group message", - "channel_info.close_gm_channel": "Are you sure you want to close this group message? This will remove it from your home screen, but you can always open it again.", - "channel_info.convert_failed": "We were unable to convert {displayName} to a private channel.", - "channel_info.convert_private": "Convert to private channel", - "channel_info.convert_private_description": "When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?", - "channel_info.convert_private_success": "{displayName} is now a private channel.", - "channel_info.convert_private_title": "Convert {displayName} to a private channel?", - "channel_info.copied": "Copied", - "channel_info.copy_link": "Copy Link", - "channel_info.custom_status": "Custom status:", - "channel_info.edit_header": "Edit Header", - "channel_info.error_close": "Close", - "channel_info.favorite": "Favorite", - "channel_info.favorited": "Favorited", - "channel_info.header": "Header:", - "channel_info.ignore_mentions": "Ignore @channel, @here, @all", - "channel_info.leave": "Leave", - "channel_info.leave_channel": "Leave channel", - "channel_info.leave_private_channel": "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.", - "channel_info.leave_public_channel": "Are you sure you want to leave the public channel {displayName}? You can always rejoin.", - "channel_info.local_time": "Local Time", - "channel_info.members": "Members", - "channel_info.mention": "Mention", - "channel_info.mobile_notifications": "Mobile Notifications", - "channel_info.muted": "Mute", - "channel_info.nickname": "Nickname", - "channel_info.notification.all": "All", - "channel_info.notification.default": "Default", - "channel_info.notification.mention": "Mentions", - "channel_info.notification.none": "Never", - "channel_info.pinned_messages": "Pinned Messages", - "channel_info.position": "Position", - "channel_info.private_channel": "Private Channel", - "channel_info.public_channel": "Public Channel", - "channel_info.send_a_mesasge": "Send a message", - "channel_info.send_mesasge": "Send message", - "channel_info.set_header": "Set Header", - "channel_info.unarchive": "Unarchive Channel", - "channel_info.unarchive_description": "Are you sure you want to unarchive the {term} {name}?", - "channel_info.unarchive_failed": "An error occurred trying to unarchive the channel {displayName}", - "channel_info.unarchive_title": "Unarchive {term}", - "channel_intro.createdBy": "Created by {user} on {date}", - "channel_intro.createdOn": "Created on {date}", - "channel_list.channels_category": "Channels", - "channel_list.dms_category": "Direct messages", - "channel_list.favorites_category": "Favorites", - "channel_list.find_channels": "Find channels...", - "channel_loader.someone": "Someone", - "channel_modal.descriptionHelp": "Describe how this channel should be used.", - "channel_modal.header": "Header", - "channel_modal.headerEx": "Use Markdown to format header text", - "channel_modal.headerHelp": "Specify text to appear in the channel header beside the channel name. For example, include frequently used links by typing link text [Link Title](http://example.com).", - "channel_modal.makePrivate.description": "When a channel is set to private, only invited team members can access and participate in that channel", - "channel_modal.makePrivate.label": "Make Private", - "channel_modal.name": "Name", - "channel_modal.nameEx": "Bugs, Marketing", - "channel_modal.optional": "(optional)", - "channel_modal.purpose": "Purpose", - "channel_modal.purposeEx": "A channel to file bugs and improvements", - "combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.", - "combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.", - "combined_system_message.added_to_channel.one_you": "You were **added to the channel** by {actor}.", - "combined_system_message.added_to_channel.two": "{firstUser} and {secondUser} **added to the channel** by {actor}.", - "combined_system_message.added_to_team.many_expanded": "{users} and {lastUser} were **added to the team** by {actor}.", - "combined_system_message.added_to_team.one": "{firstUser} **added to the team** by {actor}.", - "combined_system_message.added_to_team.one_you": "You were **added to the team** by {actor}.", - "combined_system_message.added_to_team.two": "{firstUser} and {secondUser} **added to the team** by {actor}.", - "combined_system_message.joined_channel.many_expanded": "{users} and {lastUser} **joined the channel**.", - "combined_system_message.joined_channel.one": "{firstUser} **joined the channel**.", - "combined_system_message.joined_channel.one_you": "You **joined the channel**.", - "combined_system_message.joined_channel.two": "{firstUser} and {secondUser} **joined the channel**.", - "combined_system_message.joined_team.many_expanded": "{users} and {lastUser} **joined the team**.", - "combined_system_message.joined_team.one": "{firstUser} **joined the team**.", - "combined_system_message.joined_team.one_you": "You **joined the team**.", - "combined_system_message.joined_team.two": "{firstUser} and {secondUser} **joined the team**.", - "combined_system_message.left_channel.many_expanded": "{users} and {lastUser} **left the channel**.", - "combined_system_message.left_channel.one": "{firstUser} **left the channel**.", - "combined_system_message.left_channel.one_you": "You **left the channel**.", - "combined_system_message.left_channel.two": "{firstUser} and {secondUser} **left the channel**.", - "combined_system_message.left_team.many_expanded": "{users} and {lastUser} **left the team**.", - "combined_system_message.left_team.one": "{firstUser} **left the team**.", - "combined_system_message.left_team.one_you": "You **left the team**.", - "combined_system_message.left_team.two": "{firstUser} and {secondUser} **left the team**.", - "combined_system_message.removed_from_channel.many_expanded": "{users} and {lastUser} were **removed from the channel**.", - "combined_system_message.removed_from_channel.one": "{firstUser} was **removed from the channel**.", - "combined_system_message.removed_from_channel.one_you": "You were **removed from the channel**.", - "combined_system_message.removed_from_channel.two": "{firstUser} and {secondUser} were **removed from the channel**.", - "combined_system_message.removed_from_team.many_expanded": "{users} and {lastUser} were **removed from the team**.", - "combined_system_message.removed_from_team.one": "{firstUser} was **removed from the team**.", - "combined_system_message.removed_from_team.one_you": "You were **removed from the team**.", - "combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.", - "combined_system_message.you": "You", - "connection_banner.connected": "Connection restored", - "connection_banner.connecting": "Connecting...", - "connection_banner.not_connected": "No internet connection", - "connection_banner.not_reachable": "The server is not reachable", - "create_direct_message.title": "Create Direct Message", - "create_post.deactivated": "You are viewing an archived channel with a deactivated user.", - "create_post.thread_reply": "Reply to this thread...", - "create_post.write": "Write to {channelDisplayName}", - "custom_status.expiry_dropdown.custom": "Custom", - "custom_status.expiry_dropdown.date_and_time": "Date and Time", - "custom_status.expiry_dropdown.dont_clear": "Don't clear", - "custom_status.expiry_dropdown.four_hours": "4 hours", - "custom_status.expiry_dropdown.one_hour": "1 hour", - "custom_status.expiry_dropdown.thirty_minutes": "30 minutes", - "custom_status.expiry_dropdown.this_week": "This week", - "custom_status.expiry_dropdown.today": "Today", - "custom_status.expiry_time.today": "Today", - "custom_status.expiry_time.tomorrow": "Tomorrow", - "custom_status.expiry.at": "at", - "custom_status.expiry.until": "Until", - "custom_status.failure_message": "Failed to update status. Try again", - "custom_status.set_status": "Set a custom status", - "custom_status.suggestions.in_a_meeting": "In a meeting", - "custom_status.suggestions.on_a_vacation": "On a vacation", - "custom_status.suggestions.out_for_lunch": "Out for lunch", - "custom_status.suggestions.out_sick": "Out sick", - "custom_status.suggestions.recent_title": "Recent", - "custom_status.suggestions.title": "Suggestions", - "custom_status.suggestions.working_from_home": "Working from home", - "date_separator.today": "Today", - "date_separator.yesterday": "Yesterday", - "default_skin_tone": "Default Skin Tone", - "display_settings.clock.military": "24-hour", - "display_settings.clock.standard": "12-hour", - "display_settings.clockDisplay": "Clock Display", - "display_settings.crt": "Collapsed Reply Threads", - "display_settings.crt.off": "Off", - "display_settings.crt.on": "On", - "display_settings.theme": "Theme", - "display_settings.timezone": "Timezone", - "display_settings.tz.auto": "Auto", - "display_settings.tz.manual": "Manual", - "download.error": "Unable to download the file. Try again later", - "edit_post.editPost": "Edit the post...", - "edit_post.save": "Save", - "edit_server.description": "Specify a display name for this server", - "edit_server.display_help": "Server: {url}", - "edit_server.save": "Save", - "edit_server.saving": "Saving", - "edit_server.title": "Edit server name", - "emoji_picker.activities": "Activities", - "emoji_picker.animals-nature": "Animals & Nature", - "emoji_picker.custom": "Custom", - "emoji_picker.flags": "Flags", - "emoji_picker.food-drink": "Food & Drink", - "emoji_picker.objects": "Objects", - "emoji_picker.people-body": "People & Body", - "emoji_picker.recent": "Recently Used", - "emoji_picker.searchResults": "Search Results", - "emoji_picker.smileys-emotion": "Smileys & Emotion", - "emoji_picker.symbols": "Symbols", - "emoji_picker.travel-places": "Travel & Places", - "emoji_skin.dark_skin_tone": "dark skin tone", - "emoji_skin.default": "default skin tone", - "emoji_skin.light_skin_tone": "light skin tone", - "emoji_skin.medium_dark_skin_tone": "medium dark skin tone", - "emoji_skin.medium_light_skin_tone": "medium light skin tone", - "emoji_skin.medium_skin_tone": "medium skin tone", - "extension.no_memberships.description": "To share content, you'll need to be a member of a team on a Mattermost server.", - "extension.no_memberships.title": "Not a member of any team yet", - "extension.no_servers.description": "To share content, you'll need to be logged in to a Mattermost server.", - "extension.no_servers.title": "Not connected to any servers", - "file_upload.fileAbove": "Files must be less than {max}", - "find_channels.directory": "Directory", - "find_channels.new_channel": "New Channel", - "find_channels.open_dm": "Open a DM", - "find_channels.title": "Find Channels", - "friendly_date.daysAgo": "{count} {count, plural, one {day} other {days}} ago", - "friendly_date.hoursAgo": "{count} {count, plural, one {hour} other {hours}} ago", - "friendly_date.minsAgo": "{count} {count, plural, one {min} other {mins}} ago", - "friendly_date.monthsAgo": "{count} {count, plural, one {month} other {months}} ago", - "friendly_date.now": "Now", - "friendly_date.yearsAgo": "{count} {count, plural, one {year} other {years}} ago", - "friendly_date.yesterday": "Yesterday", - "gallery.copy_link.failed": "Failed to copy link to clipboard", - "gallery.downloading": "Downloading...", - "gallery.footer.channel_name": "Shared in {channelName}", - "gallery.image_saved": "Image saved", - "gallery.open_file": "Open file", - "gallery.opening": "Opening...", - "gallery.preparing": "Preparing...", - "gallery.save_failed": "Unable to save the file", - "gallery.unsupported": "Preview isn't supported for this file type. Try downloading or sharing to open it in another app.", - "gallery.video_saved": "Video saved", - "general_settings.about": "About {appTitle}", - "general_settings.advanced_settings": "Advanced Settings", - "general_settings.display": "Display", - "general_settings.help": "Help", - "general_settings.notifications": "Notifications", - "general_settings.report_problem": "Report a problem", - "get_post_link_modal.title": "Copy Link", - "global_threads.allThreads": "All Your Threads", - "global_threads.emptyThreads.message": "Any threads you are mentioned in or have participated in will show here along with any threads you have followed.", - "global_threads.emptyThreads.title": "No followed threads yet", - "global_threads.emptyUnreads.message": "Looks like you're all caught up.", - "global_threads.emptyUnreads.title": "No unread threads", - "global_threads.markAllRead.cancel": "Cancel", - "global_threads.markAllRead.markRead": "Mark read", - "global_threads.markAllRead.message": "This will clear any unread status for all of your threads shown here", - "global_threads.markAllRead.title": "Are you sure you want to mark all threads as read?", - "global_threads.options.mark_as_read": "Mark as Read", - "global_threads.options.open_in_channel": "Open in Channel", - "global_threads.options.title": "Thread Actions", - "global_threads.unreads": "Unreads", - "home.header.plus_menu": "Options", - "integration_selector.multiselect.submit": "Done", - "interactive_dialog.submit": "Submit", - "intro.add_people": "Add People", - "intro.channel_info": "Info", - "intro.created_by": "created by {creator} on {date}.", - "intro.direct_message": "This is the start of your conversation with {teammate}. Messages and files shared here are not shown to anyone else.", - "intro.group_message": "This is the start of your conversation with this group. Messages and files shared here are not shown to anyone else outside of the group.", - "intro.private_channel": "Private Channel", - "intro.public_channel": "Public Channel", - "intro.townsquare": "Welcome to {name}. Everyone automatically becomes a member of this channel when they join the team.", - "intro.welcome": "Welcome to {displayName} channel.", - "intro.welcome.private": "Only invited members can see messages posted in this private channel.", - "intro.welcome.public": "Add some more team members to the channel or start a conversation below.", - "invite_people_to_team.message": "Here’s a link to collaborate and communicate with us on Mattermost.", - "invite_people_to_team.title": "Join the {team} team", - "invite.members.already_member": "This person is already a team member", - "invite.members.user_is_guest": "Contact your admin to make this guest a full member", - "invite.search.email_invite": "invite", - "invite.search.no_results": "No one found matching", - "invite.searchPlaceholder": "Type a name or email address…", - "invite.send_error": "Something went wrong while trying to send invitations. Please check your network connection and try again.", - "invite.send_invite": "Send", - "invite.sendInvitationsTo": "Send invitations to…", - "invite.shareLink": "Share link", - "invite.summary.done": "Done", - "invite.summary.email_invite": "An invitation email has been sent", - "invite.summary.error": "{invitationsCount, plural, one {Invitation} other {Invitations}} could not be sent successfully", - "invite.summary.member_invite": "Invited as a member of {teamDisplayName}", - "invite.summary.not_sent": "{notSentCount, plural, one {Invitation wasn’t} other {Invitations weren’t}} sent", - "invite.summary.report.notSent": "{count} {count, plural, one {invitation} other {invitations}} not sent", - "invite.summary.report.sent": "{count} successful {count, plural, one {invitation} other {invitations}}", - "invite.summary.sent": "Your {sentCount, plural, one {invitation has} other {invitations have}} been sent", - "invite.summary.smtp_failure": "SMTP is not configured in System Console", - "invite.summary.some_not_sent": "{notSentCount, plural, one {An invitation was} other {Some invitations were}} not sent", - "invite.summary.try_again": "Try again", - "invite.title": "Invite", - "invite.title.summary": "Invite summary", - "join_team.error.group_error": "You need to be a member of a linked group to join this team.", - "join_team.error.message": "There has been an error joining the team", - "join_team.error.title": "Error joining a team", - "last_users_message.added_to_channel.type": "were **added to the channel** by {actor}.", - "last_users_message.added_to_team.type": "were **added to the team** by {actor}.", - "last_users_message.first": "{firstUser} and ", - "last_users_message.joined_channel.type": "**joined the channel**.", - "last_users_message.joined_team.type": "**joined the team**.", - "last_users_message.left_channel.type": "**left the channel**.", - "last_users_message.left_team.type": "**left the team**.", - "last_users_message.others": "{numOthers} others ", - "last_users_message.removed_from_channel.type": "were **removed from the channel**.", - "last_users_message.removed_from_team.type": "were **removed from the team**.", - "load_categories_error.message": "There was a problem loading content for this server.", - "load_categories_error.title": "Couldn't load categories for {serverName}", - "load_channels_error.message": "There was a problem loading content for this team.", - "load_channels_error.title": "Couldn't load {teamDisplayName}", - "load_teams_error.message": "There was a problem loading content for this server.", - "load_teams_error.title": "Couldn't load {serverName}", - "login_mfa.enterToken": "To complete the sign in process, please enter the code from your mobile device's authenticator app.", - "login_mfa.token": "Enter MFA Token", - "login_mfa.tokenReq": "Please enter an MFA token", - "login.email": "Email", - "login.forgot": "Forgot your password?", - "login.invalid_credentials": "The email and password combination is incorrect", - "login.ldapUsername": "AD/LDAP Username", - "login.or": "or", - "login.password": "Password", - "login.signIn": "Log In", - "login.signingIn": "Logging In", - "login.username": "Username", - "markdown.latex.error": "Latex render error", - "mentions.empty.paragraph": "You'll see messages here when someone mentions you or uses terms you're monitoring.", - "mentions.empty.title": "No Mentions yet", - "mobile.about.appVersion": "App Version: {version} (Build {number})", - "mobile.account.settings.save": "Save", - "mobile.action_menu.select": "Select an option", - "mobile.add_team.create_team": "Create a new team", - "mobile.add_team.join_team": "Join Another Team", - "mobile.android.back_handler_exit": "Press back again to exit", - "mobile.android.photos_permission_denied_description": "Upload photos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo library.", - "mobile.android.photos_permission_denied_title": "{applicationName} would like to access your photos", - "mobile.announcement_banner.title": "Announcement", - "mobile.calls_call_ended": "Call ended", - "mobile.calls_call_screen": "Call", - "mobile.calls_call_thread": "Call Thread", - "mobile.calls_current_call": "Current call", - "mobile.calls_disable": "Disable calls", - "mobile.calls_dismiss": "Dismiss", - "mobile.calls_enable": "Enable calls", - "mobile.calls_end_call_title": "End call", - "mobile.calls_end_msg_channel": "Are you sure you want to end a call with {numParticipants} participants in {displayName}?", - "mobile.calls_end_msg_channel_default": "Are you sure you want to end the call?", - "mobile.calls_end_msg_dm": "Are you sure you want to end a call with {displayName}?", - "mobile.calls_end_permission_msg": "You don't have permission to end the call. Please ask the call creator to end the call.", - "mobile.calls_end_permission_title": "Error", - "mobile.calls_ended_at": "Ended at", - "mobile.calls_error_message": "Error: {error}", - "mobile.calls_error_title": "Error", - "mobile.calls_host": "host", - "mobile.calls_host_rec": "You are recording this meeting. Consider letting everyone know that this meeting is being recorded.", - "mobile.calls_host_rec_stopped": "You can find the recording in this call's chat thread once it's finished processing.", - "mobile.calls_host_rec_stopped_title": "Recording has stopped. Processing...", - "mobile.calls_host_rec_title": "You are recording", - "mobile.calls_join_call": "Join call", - "mobile.calls_lasted": "Lasted {duration}", - "mobile.calls_leave": "Leave", - "mobile.calls_leave_call": "Leave call", - "mobile.calls_limit_msg": "The maximum number of participants per call is {maxParticipants}. Contact your System Admin to increase the limit.", - "mobile.calls_limit_msg_GA": "Upgrade to Cloud Professional or Cloud Enterprise to enable group calls with more than {maxParticipants} participants.", - "mobile.calls_limit_reached": "Participant limit reached", - "mobile.calls_lower_hand": "Lower hand", - "mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.", - "mobile.calls_more": "More", - "mobile.calls_mute": "Mute", - "mobile.calls_name_is_talking": "{name} is talking", - "mobile.calls_name_started_call": "{name} started a call", - "mobile.calls_noone_talking": "No one is talking", - "mobile.calls_not_available_msg": "Please contact your System Admin to enable the feature.", - "mobile.calls_not_available_option": "(Not available)", - "mobile.calls_not_available_title": "Calls is not enabled", - "mobile.calls_not_connected": "You're not connected to a call in the current channel.", - "mobile.calls_ok": "OK", - "mobile.calls_okay": "Okay", - "mobile.calls_open_channel": "Open Channel", - "mobile.calls_participant_limit_title_GA": "This call is at capacity", - "mobile.calls_participant_rec": "The host has started recording this meeting. By staying in the meeting you give consent to being recorded.", - "mobile.calls_participant_rec_title": "Recording is in progress", - "mobile.calls_raise_hand": "Raise hand", - "mobile.calls_react": "React", - "mobile.calls_rec": "rec", - "mobile.calls_record": "Record", - "mobile.calls_request_message": "Calls are currently running in test mode and only system admins can start them. Reach out directly to your system admin for assistance", - "mobile.calls_request_title": "Calls is not currently enabled", - "mobile.calls_see_logs": "See server logs", - "mobile.calls_speaker": "Speaker", - "mobile.calls_start_call": "Start Call", - "mobile.calls_start_call_exists": "A call is already ongoing in the channel.", - "mobile.calls_stop_recording": "Stop Recording", - "mobile.calls_unmute": "Unmute", - "mobile.calls_viewing_screen": "You are viewing {name}'s screen", - "mobile.calls_you": "(you)", - "mobile.camera_photo_permission_denied_description": "Take photos and upload them to your server or save them to your device. Open Settings to grant {applicationName} read and write access to your camera.", - "mobile.camera_photo_permission_denied_title": "{applicationName} would like to access your camera", - "mobile.camera_type.title": "Camera options", - "mobile.channel_info.alertNo": "No", - "mobile.channel_info.alertYes": "Yes", - "mobile.channel_list.recent": "Recent", - "mobile.channel_list.unreads": "Unreads", - "mobile.commands.error_title": "Error Executing Command", - "mobile.components.select_server_view.connect": "Connect", - "mobile.components.select_server_view.connecting": "Connecting", - "mobile.components.select_server_view.displayHelp": "Choose a display name for your server", - "mobile.components.select_server_view.displayName": "Display Name", - "mobile.components.select_server_view.enterServerUrl": "Enter Server URL", - "mobile.components.select_server_view.msg_connect": "Let’s Connect to a Server", - "mobile.components.select_server_view.msg_description": "A Server is your team's communication hub which is accessed through a unique URL", - "mobile.components.select_server_view.msg_welcome": "Welcome", - "mobile.components.select_server_view.proceed": "Proceed", - "mobile.create_channel": "Create", - "mobile.create_channel.title": "New channel", - "mobile.create_direct_message.max_limit_reached": "Group messages are limited to {maxCount} members", - "mobile.create_direct_message.start": "Start Conversation", - "mobile.create_direct_message.you": "@{username} - you", - "mobile.create_post.read_only": "This channel is read-only.", - "mobile.custom_list.no_results": "No Results", - "mobile.custom_status.choose_emoji": "Choose an emoji", - "mobile.custom_status.clear_after": "Clear After", - "mobile.custom_status.clear_after.title": "Clear Custom Status After", - "mobile.custom_status.modal_confirm": "Done", - "mobile.direct_message.error": "We couldn't open a DM with {displayName}.", - "mobile.display_settings.clockDisplay": "Clock Display", - "mobile.display_settings.crt": "Collapsed Reply Threads", - "mobile.display_settings.theme": "Theme", - "mobile.display_settings.timezone": "Timezone", - "mobile.document_preview.failed_description": "An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n", - "mobile.document_preview.failed_title": "Open Document failed", - "mobile.downloader.disabled_description": "File downloads are disabled on this server. Please contact your System Admin for more details.\n", - "mobile.downloader.disabled_title": "Download disabled", - "mobile.downloader.failed_description": "An error occurred while downloading the file. Please check your internet connection and try again.\n", - "mobile.downloader.failed_title": "Download failed", - "mobile.edit_channel": "Save", - "mobile.edit_post.delete_question": "Are you sure you want to delete this Post?", - "mobile.edit_post.delete_title": "Confirm Post Delete", - "mobile.edit_post.error": "There was a problem editing this message. Please try again.", - "mobile.edit_post.title": "Editing Message", - "mobile.error_handler.button": "Relaunch", - "mobile.error_handler.description": "\nTap relaunch to open the app again. After restart, you can report the problem from the settings menu.\n", - "mobile.error_handler.title": "Unexpected error occurred", - "mobile.file_upload.disabled2": "File uploads from mobile are disabled.", - "mobile.file_upload.max_warning": "Uploads limited to {count} files maximum.", - "mobile.files_paste.error_description": "An error occurred while pasting the file(s). Please try again.", - "mobile.files_paste.error_dismiss": "Dismiss", - "mobile.files_paste.error_title": "Paste failed", - "mobile.gallery.title": "{index} of {total}", - "mobile.integration_selector.loading_channels": "Loading channels...", - "mobile.integration_selector.loading_options": "Loading options...", - "mobile.ios.photos_permission_denied_description": "Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.", - "mobile.ios.photos_permission_denied_title": "{applicationName} would like to access your photos", - "mobile.join_channel.error": "We couldn't join the channel {displayName}.", - "mobile.leave_and_join_confirmation": "Leave & Join", - "mobile.leave_and_join_message": "You are already on a channel call in ~{leaveChannelName}. Do you want to leave your current call and join the call in ~{joinChannelName}?", - "mobile.leave_and_join_title": "Are you sure you want to switch to a different call?", - "mobile.link.error.text": "Unable to open the link.", - "mobile.link.error.title": "Error", - "mobile.login_options.cant_heading": "Can't Log In", - "mobile.login_options.enter_credentials": "Enter your login details below.", - "mobile.login_options.gitlab": "GitLab", - "mobile.login_options.google": "Google", - "mobile.login_options.heading": "Log In to Your Account", - "mobile.login_options.none": "You can't log in to your account yet. At least one login option must be configured. Contact your System Admin for assistance.", - "mobile.login_options.office365": "Office 365", - "mobile.login_options.openid": "Open ID", - "mobile.login_options.saml": "SAML", - "mobile.login_options.select_option": "Select a login option below.", - "mobile.login_options.separator_text": "or log in with", - "mobile.login_options.sso_continue": "Continue with", - "mobile.managed.blocked_by": "Blocked by {vendor}", - "mobile.managed.exit": "Exit", - "mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}.\n\nReason {reason}\n\n\n\nDebug info: {debug}\n\nPlease exit the app.", - "mobile.managed.jailbreak_no_debug_info": "Not available", - "mobile.managed.jailbreak_no_reason": "Not available", - "mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.", - "mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.", - "mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.", - "mobile.managed.secured_by": "Secured by {vendor}", - "mobile.managed.settings": "Go to settings", - "mobile.markdown.code.copy_code": "Copy Code", - "mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}", - "mobile.markdown.image.too_large": "Image exceeds max dimensions of {maxWidth} by {maxHeight}:", - "mobile.markdown.link.copy_url": "Copy URL", - "mobile.mention.copy_mention": "Copy Mention", - "mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}", - "mobile.message_length.message_split_left": "Message exceeds the character limit", - "mobile.message_length.title": "Message Length", - "mobile.no_results_with_term": "No results for “{term}”", - "mobile.no_results_with_term.files": "No files matching “{term}”", - "mobile.no_results_with_term.messages": "No matches found for “{term}”", - "mobile.no_results.spelling": "Check the spelling or try another search.", - "mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.", - "mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.", - "mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify that a browser is installed on the device.", - "mobile.oauth.something_wrong": "Something went wrong", - "mobile.oauth.something_wrong.okButton": "OK", - "mobile.oauth.switch_to_browser": "You are being redirected to your login provider", - "mobile.oauth.switch_to_browser.error_title": "Sign in error", - "mobile.oauth.switch_to_browser.title": "Redirecting...", - "mobile.oauth.try_again": "Try again", - "mobile.onboarding.next": "Next", - "mobile.onboarding.sign_in": "Sign in", - "mobile.onboarding.sign_in_to_get_started": "Sign in to get started", - "mobile.open_dm.error": "We couldn't open a direct message with {displayName}. Please check your connection and try again.", - "mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.", - "mobile.participants.header": "Thread Participants", - "mobile.permission_denied_dismiss": "Don't Allow", - "mobile.permission_denied_retry": "Settings", - "mobile.post_info.add_reaction": "Add Reaction", - "mobile.post_info.copy_text": "Copy Text", - "mobile.post_info.mark_unread": "Mark as Unread", - "mobile.post_info.pin": "Pin to Channel", - "mobile.post_info.reply": "Reply", - "mobile.post_info.save": "Save", - "mobile.post_info.unpin": "Unpin from Channel", - "mobile.post_info.unsave": "Unsave", - "mobile.post_pre_header.pinned": "Pinned", - "mobile.post_pre_header.pinned_saved": "Pinned and Saved", - "mobile.post_pre_header.saved": "Saved", - "mobile.post_textbox.entire_channel_here.message": "By using @here you are about to send notifications to up to {totalMembers, number} {totalMembers, plural, one {person} other {people}}. Are you sure you want to do this?", - "mobile.post_textbox.entire_channel_here.message.with_timezones": "By using @here you are about to send notifications up to {totalMembers, number} {totalMembers, plural, one {person} other {people}} in {timezones, number} {timezones, plural, one {timezone} other {timezones}}. Are you sure you want to do this?", - "mobile.post_textbox.entire_channel.cancel": "Cancel", - "mobile.post_textbox.entire_channel.confirm": "Confirm", - "mobile.post_textbox.entire_channel.message": "By using @all or @channel you are about to send notifications to {totalMembers, number} {totalMembers, plural, one {person} other {people}}. Are you sure you want to do this?", - "mobile.post_textbox.entire_channel.message.with_timezones": "By using @all or @channel you are about to send notifications to {totalMembers, number} {totalMembers, plural, one {person} other {people}} in {timezones, number} {timezones, plural, one {timezone} other {timezones}}. Are you sure you want to do this?", - "mobile.post_textbox.entire_channel.title": "Confirm sending notifications to entire channel", - "mobile.post_textbox.groups.title": "Confirm sending notifications to groups", - "mobile.post_textbox.uploadFailedDesc": "Some attachments failed to upload to the server. Are you sure you want to post the message?", - "mobile.post_textbox.uploadFailedTitle": "Attachment failure", - "mobile.post.cancel": "Cancel", - "mobile.post.delete_question": "Are you sure you want to delete this post?", - "mobile.post.delete_title": "Delete Post", - "mobile.post.failed_delete": "Delete Message", - "mobile.post.failed_retry": "Try Again", - "mobile.privacy_link": "Privacy Policy", - "mobile.push_notification_reply.button": "Send", - "mobile.push_notification_reply.placeholder": "Write a reply...", - "mobile.push_notification_reply.title": "Reply", - "mobile.rename_channel.display_name_maxLength": "Channel name must be less than {maxLength, number} characters", - "mobile.rename_channel.display_name_minLength": "Channel name must be {minLength, number} or more characters", - "mobile.rename_channel.display_name_required": "Channel name is required", - "mobile.request.invalid_request_method": "Invalid request method", - "mobile.request.invalid_response": "Received invalid response from the server.", - "mobile.reset_status.alert_cancel": "Cancel", - "mobile.reset_status.alert_ok": "OK", - "mobile.reset_status.title_ooo": "Disable \"Out Of Office\"?", - "mobile.routes.code": "{language} Code", - "mobile.routes.code.noLanguage": "Code", - "mobile.routes.custom_status": "Set a custom status", - "mobile.routes.table": "Table", - "mobile.routes.user_profile": "Profile", - "mobile.screen.settings": "Settings", - "mobile.screen.your_profile": "Your Profile", - "mobile.search.jump": "Jump to recent messages", - "mobile.search.modifier.exclude": "exclude search terms", - "mobile.search.modifier.from": "a specific user", - "mobile.search.modifier.in": "a specific channel", - "mobile.search.modifier.phrases": "messages with phrases", - "mobile.search.show_less": "Show less", - "mobile.search.show_more": "Show more", - "mobile.search.team.select": "Select a team to search", - "mobile.server_identifier.exists": "You are already connected to this server.", - "mobile.server_link.error.text": "The link could not be found on this server.", - "mobile.server_link.error.title": "Link Error", - "mobile.server_link.unreachable_channel.error": "This link belongs to a deleted channel or to a channel to which you do not have access.", - "mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.", - "mobile.server_link.unreachable_user.error": "We can't redirect you to the DM. The user specified is unknown.", - "mobile.server_name.exists": "You are using this name for another server.", - "mobile.server_ping_failed": "Cannot connect to the server.", - "mobile.server_requires_client_certificate": "Server requires client certificate for authentication.", - "mobile.server_upgrade.button": "OK", - "mobile.server_upgrade.description": "\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n", - "mobile.server_upgrade.title": "Server upgrade required", - "mobile.server_url.deeplink.emm.denied": "This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server", - "mobile.server_url.empty": "Please enter a valid server URL", - "mobile.server_url.invalid_format": "URL must start with http:// or https://", - "mobile.session_expired": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.", - "mobile.session_expired.title": "Session Expired", - "mobile.set_status.dnd": "Do Not Disturb", - "mobile.storage_permission_denied_description": "Upload files to your server. Open Settings to grant {applicationName} Read and Write access to files on this device.", - "mobile.storage_permission_denied_title": "{applicationName} would like to access your files", - "mobile.suggestion.members": "Members", - "mobile.system_message.channel_archived_message": "{username} archived the channel", - "mobile.system_message.channel_unarchived_message": "{username} unarchived the channel", - "mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}", - "mobile.system_message.update_channel_header_message_and_forget.removed": "{username} removed the channel header (was: {oldHeader})", - "mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} updated the channel header from: {oldHeader} to: {newHeader}", - "mobile.system_message.update_channel_header_message_and_forget.updated_to": "{username} updated the channel header to: {newHeader}", - "mobile.system_message.update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {oldPurpose})", - "mobile.system_message.update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}", - "mobile.system_message.update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {newPurpose}", - "mobile.tos_link": "Terms of Service", - "mobile.user_list.deactivated": "Deactivated", - "mobile.write_storage_permission_denied_description": "Save files to your device. Open Settings to grant {applicationName} write access to files on this device.", - "modal.manual_status.auto_responder.message_": "Would you like to switch your status to \"{status}\" and disable Automatic Replies?", - "modal.manual_status.auto_responder.message_away": "Would you like to switch your status to \"Away\" and disable Automatic Replies?", - "modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to \"Do Not Disturb\" and disable Automatic Replies?", - "modal.manual_status.auto_responder.message_offline": "Would you like to switch your status to \"Offline\" and disable Automatic Replies?", - "modal.manual_status.auto_responder.message_online": "Would you like to switch your status to \"Online\" and disable Automatic Replies?", - "more_messages.text": "{count} new {count, plural, one {message} other {messages}}", - "msg_typing.areTyping": "{users} and {last} are typing...", - "msg_typing.isTyping": "{user} is typing...", - "notification_settings.auto_responder": "Automatic Replies", - "notification_settings.auto_responder.default_message": "Hello, I am out of office and unable to respond to messages.", - "notification_settings.auto_responder.footer.message": "Set a custom message that is automatically sent in response to direct messages, such as an out of office or vacation reply. Enabling this setting changes your status to Out of Office and disables notifications.", - "notification_settings.auto_responder.message": "Message", - "notification_settings.auto_responder.to.enable": "Enable automatic replies", - "notification_settings.email": "Email Notifications", - "notification_settings.email.crt.emailInfo": "When enabled, any reply to a thread you're following will send an email notification", - "notification_settings.email.crt.send": "Thread reply notifications", - "notification_settings.email.emailHelp2": "Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.", - "notification_settings.email.emailInfo": "Email notifications are sent for mentions and direct messages when you are offline or away for more than 5 minutes.", - "notification_settings.email.everyHour": "Every hour", - "notification_settings.email.fifteenMinutes": "Every 15 minutes", - "notification_settings.email.immediately": "Immediately", - "notification_settings.email.never": "Never", - "notification_settings.email.send": "Send email notifications", - "notification_settings.mention.reply": "Send reply notifications for", - "notification_settings.mentions": "Mentions", - "notification_settings.mentions_replies": "Mentions and Replies", - "notification_settings.mentions..keywordsDescription": "Other words that trigger a mention", - "notification_settings.mentions.channelWide": "Channel-wide mentions", - "notification_settings.mentions.keywords": "Keywords", - "notification_settings.mentions.keywords_mention": "Keywords that trigger mentions", - "notification_settings.mentions.keywordsLabel": "Keywords are not case-sensitive. Separate keywords with commas.", - "notification_settings.mentions.sensitiveName": "Your case sensitive first name", - "notification_settings.mentions.sensitiveUsername": "Your non-case sensitive username", - "notification_settings.mobile": "Push Notifications", - "notification_settings.mobile.away": "Away or offline", - "notification_settings.mobile.offline": "Offline", - "notification_settings.mobile.online": "Online, away or offline", - "notification_settings.mobile.trigger_push": "Trigger push notifications when...", - "notification_settings.ooo_auto_responder": "Automatic replies", - "notification_settings.push_notification": "Push Notifications", - "notification_settings.push_threads.following": "Notify me about replies to threads I'm following in this channel", - "notification_settings.push_threads.replies": "Thread replies", - "notification_settings.pushNotification.all_new_messages": "All new messages", - "notification_settings.pushNotification.disabled_long": "Push notifications for mobile devices have been disabled by your System Administrator.", - "notification_settings.pushNotification.mentions_only": "Mentions, direct messages only (default)", - "notification_settings.pushNotification.nothing": "Nothing", - "notification_settings.send_notification.about": "Notify me about...", - "notification_settings.threads_mentions": "Mentions in threads", - "notification_settings.threads_start": "Threads that I start", - "notification_settings.threads_start_participate": "Threads that I start or participate in", - "notification.message_not_found": "Message not found", - "notification.not_channel_member": "This message belongs to a channel where you are not a member.", - "notification.not_team_member": "This message belongs to a team where you are not a member.", - "onboarding.calls": "Start secure audio calls instantly", - "onboarding.calls_description": "When typing isn’t fast enough, switch from channel-based chat to secure audio calls with a single tap.", - "onboarding.integrations": "Integrate with tools you love", - "onboarding.integrations_description": "Go beyond chat with tightly-integrated product solutions matched to common development processes.", - "onboarding.realtime_collaboration": "Collaborate in real‑time", - "onboarding.realtime_collaboration_description": "Persistent channels, direct messaging, and file sharing works seamlessly so you can stay connected, wherever you are.", - "onboarding.welcome": "Welcome", - "onboaring.welcome_description": "Mattermost is an open source platform for developer collaboration. Secure, flexible, and integrated with your tools.", - "password_send.description": "To reset your password, enter the email address you used to sign up", - "password_send.error": "Please enter a valid email address.", - "password_send.generic_error": "We were unable to send you a reset password link. Please contact your System Admin for assistance.", - "password_send.link": "If the account exists, a password reset email will be sent to:", - "password_send.link.title": "Reset Link Sent", - "password_send.reset": "Reset Your Password", - "password_send.return": "Return to Log In", - "permalink.error.access.text": "The message you are trying to view is in a channel you don’t have access to or has been deleted.", - "permalink.error.access.title": "Message not viewable", - "permalink.error.cancel": "Cancel", - "permalink.error.okay": "Okay", - "permalink.error.private_channel_and_team.button": "Join channel and team", - "permalink.error.private_channel_and_team.text": "The message you are trying to view is in a private channel in a team you are not a member of. You have access as an admin. Do you want to join **{channelName}** and the **{teamName}** team to view it?", - "permalink.error.private_channel_and_team.title": "Join private channel and team", - "permalink.error.private_channel.button": "Join channel", - "permalink.error.private_channel.text": "The message you are trying to view is in a private channel you have not been invited to, but you have access as an admin. Do you still want to join **{channelName}**?", - "permalink.error.private_channel.title": "Join private channel", - "permalink.error.public_channel_and_team.button": "Join channel and team", - "permalink.error.public_channel_and_team.text": "The message you are trying to view is in a channel you don’t belong and a team you are not a member of. Do you want to join **{channelName}** and the **{teamName}** team to view it?", - "permalink.error.public_channel_and_team.title": "Join channel and team", - "permalink.error.public_channel.button": "Join channel", - "permalink.error.public_channel.text": "The message you are trying to view is in a channel you don’t belong to. Do you want to join **{channelName}** to view it?", - "permalink.error.public_channel.title": "Join channel", - "permalink.show_dialog_warn.cancel": "Cancel", - "permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?", - "permalink.show_dialog_warn.join": "Join", - "permalink.show_dialog_warn.title": "Join private channel", - "pinned_messages.empty.paragraph": "To pin important messages, long-press on a message and choose Pin To Channel. Pinned messages will be visible to everyone in this channel.", - "pinned_messages.empty.title": "No pinned messages yet", - "plus_menu.browse_channels.title": "Browse Channels", - "plus_menu.create_new_channel.title": "Create New Channel", - "plus_menu.invite_people_to_team.title": "Invite people to the team", - "plus_menu.open_direct_message.title": "Open a Direct Message", - "post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel.", - "post_body.check_for_out_of_channel_mentions.link.and": " and ", - "post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel", - "post_body.check_for_out_of_channel_mentions.link.public": "add them to the channel", - "post_body.check_for_out_of_channel_mentions.message_last": "? They will have access to all message history.", - "post_body.check_for_out_of_channel_mentions.message.multiple": "were mentioned but they are not in the channel. Would you like to ", - "post_body.check_for_out_of_channel_mentions.message.one": "was mentioned but is not in the channel. Would you like to ", - "post_body.commentedOn": "Commented on {name}{apostrophe} message: ", - "post_body.deleted": "(message deleted)", - "post_info.auto_responder": "Automatic Reply", - "post_info.bot": "Bot", - "post_info.del": "Delete", - "post_info.edit": "Edit", - "post_info.guest": "Guest", - "post_info.system": "System", - "post_message_view.edited": "(edited)", - "post_priority.label.important": "IMPORTANT", - "post_priority.label.urgent": "URGENT", - "post_priority.picker.beta": "BETA", - "post_priority.picker.label.important": "Important", - "post_priority.picker.label.standard": "Standard", - "post_priority.picker.label.urgent": "Urgent", - "post_priority.picker.title": "Message priority", - "post.options.title": "Options", - "post.reactions.title": "Reactions", - "posts_view.newMsg": "New Messages", - "public_link_copied": "Link copied to clipboard", - "rate.button.needs_work": "Needs work", - "rate.button.yes": "Love it!", - "rate.dont_ask_again": "Don't ask me again", - "rate.error.text": "There has been an error while opening the review modal.", - "rate.error.title": "Error", - "rate.subtitle": "Let us know what you think.", - "rate.title": "Enjoying Mattermost?", - "saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.", - "saved_messages.empty.title": "No saved messages yet", - "screen.mentions.subtitle": "Messages you've been mentioned in", - "screen.mentions.title": "Recent Mentions", - "screen.saved_messages.subtitle": "All messages you've saved for follow up", - "screen.saved_messages.title": "Saved Messages", - "screen.search.header.files": "Files", - "screen.search.header.messages": "Messages", - "screen.search.modifier.header": "Search options", - "screen.search.placeholder": "Search messages & files", - "screen.search.results.file_options.copy_link": "Copy link", - "screen.search.results.file_options.download": "Download", - "screen.search.results.file_options.open_in_channel": "Open in channel", - "screen.search.results.filter.all_file_types": "All file types", - "screen.search.results.filter.audio": "Audio", - "screen.search.results.filter.code": "Code", - "screen.search.results.filter.documents": "Documents", - "screen.search.results.filter.images": "Images", - "screen.search.results.filter.presentations": "Presentations", - "screen.search.results.filter.spreadsheets": "Spreadsheets", - "screen.search.results.filter.title": "Filter by file type", - "screen.search.results.filter.videos": "Videos", - "screen.search.title": "Search", - "screens.channel_edit": "Edit Channel", - "screens.channel_edit_header": "Edit Channel Header", - "screens.channel_info": "Channel Info", - "screens.channel_info.dm": "Direct message info", - "screens.channel_info.gm": "Group message info", - "search_bar.search": "Search", - "search_bar.search.placeholder": "Search timezone", - "select_team.description": "You are not yet a member of any teams. Select one below to get started.", - "select_team.no_team.description": "To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.", - "select_team.no_team.title": "No teams are available to join", - "select_team.title": "Select a team", - "server_list.push_proxy_error": "Notifications cannot be received from this server because of its configuration. Contact your system admin.", - "server_list.push_proxy_unknown": "Notifications could not be received from this server because of its configuration. Log out and Log in again to retry.", - "server_upgrade.alert_description": "Your server, {serverDisplayName}, is running an unsupported server version. Users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {supportedServerVersion} or later is required.", - "server_upgrade.dismiss": "Dismiss", - "server_upgrade.learn_more": "Learn More", - "server.logout.alert_description": "All associated data will be removed", - "server.logout.alert_title": "Are you sure you want to log out of {displayName}?", - "server.remove.alert_description": "This will remove it from your list of servers. All associated data will be removed", - "server.remove.alert_title": "Are you sure you want to remove {displayName}?", - "server.tutorial.swipe": "Swipe left on a server to see more actions", - "server.websocket.unreachable": "Server is unreachable.", - "servers.create_button": "Add a server", - "servers.default": "Default Server", - "servers.edit": "Edit", - "servers.login": "Log in", - "servers.logout": "Log out", - "servers.remove": "Remove", - "settings_display.clock.mz": "24-hour clock", - "settings_display.clock.mz.desc": "Example: 16:00", - "settings_display.clock.normal.desc": "Example: 4:00 PM", - "settings_display.clock.standard": "12-hour clock", - "settings_display.crt.desc": "When enabled, reply messages are not shown in the channel and you'll be notified about threads you're following in the \"Threads\" view.", - "settings_display.crt.label": "Collapsed Reply Threads", - "settings_display.custom_theme": "Custom Theme", - "settings_display.timezone.automatically": "Set automatically", - "settings_display.timezone.manual": "Change timezone", - "settings_display.timezone.off": "Off", - "settings_display.timezone.select": "Select Timezone", - "settings.about": "About {appTitle}", - "settings.about.build": "{version} (Build {number})", - "settings.about.copyright": "Copyright 2015-{currentYear} Mattermost, Inc. All rights reserved", - "settings.about.database": "Database:", - "settings.about.database.schema": "Database Schema Version:", - "settings.about.licensed": "Licensed to: {company}", - "settings.about.powered_by": "{site} is powered by Mattermost", - "settings.about.server.version.desc": "Server Version:", - "settings.about.server.version.value": "{version} (Build {number})", - "settings.about.serverVersionNoBuild": "{version}", - "settings.about.version": "App Version:", - "settings.advanced_settings": "Advanced Settings", - "settings.advanced.cancel": "Cancel", - "settings.advanced.delete": "Delete", - "settings.advanced.delete_data": "Delete local files", - "settings.advanced.delete_message.confirmation": "\nThis will delete all files downloaded through the app for this server. Please confirm to proceed.\n", - "settings.display": "Display", - "settings.link.error.text": "Unable to open the link.", - "settings.link.error.title": "Error", - "settings.notice_mobile_link": "mobile apps", - "settings.notice_platform_link": "server", - "settings.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.", - "settings.notifications": "Notifications", - "settings.save": "Save", - "share_extension.channel_error": "You are not a member of a team on the selected server. Select another server or open Mattermost to join a team.", - "share_extension.channel_label": "Channel", - "share_extension.count_limit": "You can only share {count, number} {count, plural, one {file} other {files}} on this server", - "share_extension.file_limit.multiple": "Each file must be less than {size}", - "share_extension.file_limit.single": "File must be less than {size}", - "share_extension.max_resolution": "Image exceeds maximum dimensions of 7680 x 4320 px", - "share_extension.message": "Enter a message (optional)", - "share_extension.multiple_label": "{count, number} attachments", - "share_extension.server_label": "Server", - "share_extension.servers_screen.title": "Select server", - "share_extension.share_screen.title": "Share to Mattermost", - "share_extension.upload_disabled": "File uploads are disabled for the selected server", - "share_feedback.button.no": "No, thanks", - "share_feedback.button.yes": "Yes", - "share_feedback.subtitle": "We'd love to hear how we can make your experience better.", - "share_feedback.title": "Would you share your feedback?", - "skintone_selector.tooltip.description": "You can now choose the skin tone you prefer to use for your emojis.", - "skintone_selector.tooltip.title": "Choose your default skin tone", - "smobile.search.recent_title": "Recent searches in {teamName}", - "snack.bar.favorited.channel": "This channel was favorited", - "snack.bar.link.copied": "Link copied to clipboard", - "snack.bar.message.copied": "Text copied to clipboard", - "snack.bar.mute.channel": "This channel was muted", - "snack.bar.undo": "Undo", - "snack.bar.unfavorite.channel": "This channel was unfavorited", - "snack.bar.unmute.channel": "This channel was unmuted", - "status_dropdown.set_away": "Away", - "status_dropdown.set_dnd": "Do Not Disturb", - "status_dropdown.set_offline": "Offline", - "status_dropdown.set_online": "Online", - "status_dropdown.set_ooo": "Out Of Office", - "suggestion.mention.all": "Notifies everyone in this channel", - "suggestion.mention.channel": "Notifies everyone in this channel", - "suggestion.mention.channels": "My Channels", - "suggestion.mention.groups": "Group Mentions", - "suggestion.mention.here": "Notifies everyone online in this channel", - "suggestion.mention.members": "Channel Members", - "suggestion.mention.morechannels": "Other Channels", - "suggestion.mention.nonmembers": "Not in Channel", - "suggestion.mention.special": "Special Mentions", - "suggestion.mention.you": " (you)", - "suggestion.search.direct": "Direct Messages", - "suggestion.search.private": "Private Channels", - "suggestion.search.public": "Public Channels", - "team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.", - "team_list.no_other_teams.title": "No additional teams to join", - "terms_of_service.acceptButton": "Accept", - "terms_of_service.alert_cancel": "Cancel", - "terms_of_service.alert_retry": "Try Again", - "terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.", - "terms_of_service.decline": "Decline", - "terms_of_service.error.description": "It was not possible to get the Terms of Service from the Server.", - "terms_of_service.error.logout": "Logout", - "terms_of_service.error.retry": "Retry", - "terms_of_service.error.title": "Failed to get the ToS.", - "terms_of_service.terms_declined.ok": "OK", - "terms_of_service.terms_declined.text": "You must accept the terms of service to access this server. Please contact your system administrator for more details. You will now be logged out. Log in again to accept the terms of service.", - "terms_of_service.terms_declined.title": "You must accept the terms of service", - "terms_of_service.title": "Terms of Service", - "thread.header.thread": "Thread", - "thread.header.thread_in": "in {channelName}", - "thread.loadingReplies": "Loading replies...", - "thread.noReplies": "No replies yet", - "thread.options.title": "Thread Actions", - "thread.repliesCount": "{repliesCount, number} {repliesCount, plural, one {reply} other {replies}}", - "threads": "Threads", - "threads.deleted": "Original Message Deleted", - "threads.end_of_list.subtitle": "If you're looking for older conversations, try searching instead", - "threads.end_of_list.title": "That's the end of the list!", - "threads.follow": "Follow", - "threads.following": "Following", - "threads.followMessage": "Follow Message", - "threads.followThread": "Follow Thread", - "threads.newReplies": "{count} new {count, plural, one {reply} other {replies}}", - "threads.replies": "{count} {count, plural, one {reply} other {replies}}", - "threads.unfollowMessage": "Unfollow Message", - "threads.unfollowThread": "Unfollow Thread", - "unreads.empty.paragraph": "Turn off the unread filter to show all your channels.", - "unreads.empty.show_all": "Show all", - "unreads.empty.title": "No more unreads", - "unsupported_server.message": "Your server, {serverDisplayName}, is running an unsupported server version. You may experience compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Please contact your System Administrator to upgrade your Mattermost server.", - "unsupported_server.title": "Unsupported server version", - "user_profile.custom_status": "Custom Status", - "user_status.away": "Away", - "user_status.dnd": "Do Not Disturb", - "user_status.offline": "Offline", - "user_status.online": "Online", - "user_status.title": "Status", - "user.edit_profile.email.auth_service": "Login occurs through {service}. Email cannot be updated. Email address used for notifications is {email}.", - "user.edit_profile.email.web_client": "Email must be updated using a web client or desktop application.", - "user.edit_profile.profile_photo.change_photo": "Change profile photo", - "user.settings.general.email": "Email", - "user.settings.general.field_handled_externally": "Some fields below are handled through your login provider. If you want to change them, you’ll need to do so through your login provider.", - "user.settings.general.firstName": "First Name", - "user.settings.general.lastName": "Last Name", - "user.settings.general.nickname": "Nickname", - "user.settings.general.position": "Position", - "user.settings.general.username": "Username", - "user.settings.notifications.email_threads.description": "Notify me about all replies to threads I'm following", - "user.tutorial.long_press": "Long-press on an item to view a user's profile", - "video.download": "Download video", - "video.download_description": "This video must be downloaded to play it.", - "video.failed_description": "An error occurred while trying to play the video.", - "your.servers": "Your servers" + "about.date": "Build Date:", + "about.enterpriseEditione1": "Enterprise Edition", + "about.enterpriseEditionLearn": "Learn more about Enterprise Edition at ", + "about.enterpriseEditionSt": "Modern communication from behind your firewall.", + "about.hash": "Build Hash:", + "about.hashee": "EE Build Hash:", + "about.teamEditionLearn": "Join the Mattermost community at", + "about.teamEditionSt": "All your team communication in one place, instantly searchable and accessible anywhere.", + "about.teamEditiont0": "Team Edition", + "about.teamEditiont1": "Enterprise Edition", + "account.logout": "Log out", + "account.logout_from": "Log out of {serverName}", + "account.settings": "Settings", + "account.your_profile": "Your Profile", + "alert.channel_deleted.description": "The channel {displayName} has been archived.", + "alert.channel_deleted.title": "Archived channel", + "alert.push_proxy_error.description": "Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.", + "alert.push_proxy_error.title": "Notifications cannot be received from this server", + "alert.push_proxy_unknown.description": "This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.", + "alert.push_proxy_unknown.title": "Notifications could not be received from this server", + "alert.push_proxy.button": "Okay", + "alert.removed_from_channel.description": "You have been removed from channel {displayName}.", + "alert.removed_from_channel.title": "Removed from channel", + "alert.removed_from_team.description": "You have been removed from team {displayName}.", + "alert.removed_from_team.title": "Removed from team", + "announcment_banner.dismiss": "Dismiss announcement", + "announcment_banner.okay": "Okay", + "api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.", + "api.channel.add_member.added": "{addedUsername} added to the channel by {username}.", + "api.channel.guest_join_channel.post_and_forget": "{username} joined the channel as a guest.", + "apps.error": "Error: {error}", + "apps.error.command.field_missing": "Required fields missing: `{fieldName}`.", + "apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.", + "apps.error.command.same_option": "Option repeated for field `{fieldName}`: `{option}`.", + "apps.error.command.same_user": "User repeated for field `{fieldName}`: `{option}`.", + "apps.error.command.unknown_channel": "Unknown channel for field `{fieldName}`: `{option}`.", + "apps.error.command.unknown_option": "Unknown option for field `{fieldName}`: `{option}`.", + "apps.error.command.unknown_user": "Unknown user for field `{fieldName}`: `{option}`.", + "apps.error.form.no_form": "`form` is not defined.", + "apps.error.form.no_lookup": "`lookup` is not defined.", + "apps.error.form.no_source": "`source` is not defined.", + "apps.error.form.no_submit": "`submit` is not defined", + "apps.error.form.refresh": "There has been an error fetching the select fields. Contact the app developer. Details: {details}", + "apps.error.form.refresh_no_refresh": "Called refresh on no refresh field.", + "apps.error.form.submit.pretext": "There has been an error submitting the modal. Contact the app developer. Details: {details}", + "apps.error.lookup.error_preparing_request": "Error preparing lookup request: {errorMessage}", + "apps.error.malformed_binding": "This binding is not properly formed. Contact the App developer.", + "apps.error.parser": "Parsing error: {error}", + "apps.error.parser.empty_value": "Empty values are not allowed.", + "apps.error.parser.execute_non_leaf": "You must select a subcommand.", + "apps.error.parser.missing_binding": "Missing command bindings.", + "apps.error.parser.missing_field_value": "Field value is missing.", + "apps.error.parser.missing_list_end": "Expected list closing token.", + "apps.error.parser.missing_quote": "Matching double quote expected before end of input.", + "apps.error.parser.missing_source": "Form has neither submit nor source.", + "apps.error.parser.missing_submit": "No submit call in binding or form.", + "apps.error.parser.missing_tick": "Matching tick quote expected before end of input.", + "apps.error.parser.multiple_equal": "Multiple `=` signs are not allowed.", + "apps.error.parser.no_argument_pos_x": "Unable to identify argument.", + "apps.error.parser.no_bindings": "No command bindings.", + "apps.error.parser.no_form": "No form found.", + "apps.error.parser.no_match": "`{command}`: No matching command found in this workspace.", + "apps.error.parser.no_slash_start": "Command must start with a `/`.", + "apps.error.parser.unexpected_character": "Unexpected character.", + "apps.error.parser.unexpected_comma": "Unexpected comma.", + "apps.error.parser.unexpected_error": "Unexpected error.", + "apps.error.parser.unexpected_flag": "Command does not accept flag `{flagName}`.", + "apps.error.parser.unexpected_squared_bracket": "Unexpected list opening.", + "apps.error.parser.unexpected_state": "Unreachable: Unexpected state in matchBinding: `{state}`.", + "apps.error.parser.unexpected_whitespace": "Unreachable: Unexpected whitespace.", + "apps.error.responses.form.no_form": "Response type is `form`, but no form was included in the response.", + "apps.error.responses.navigate.no_url": "Response type is `navigate`, but no url was included in response.", + "apps.error.responses.unexpected_error": "Received an unexpected error.", + "apps.error.responses.unexpected_type": "App response type was not expected. Response type: {type}", + "apps.error.responses.unknown_field_error": "Received an error for an unknown field. Field name: `{field}`. Error: `{error}`.", + "apps.error.responses.unknown_type": "App response type not supported. Response type: {type}.", + "apps.error.unknown": "Unknown error occurred.", + "apps.suggestion.dynamic.error": "Dynamic select error", + "apps.suggestion.errors.parser_error": "Parsing error", + "apps.suggestion.no_dynamic": "No data was returned for dynamic suggestions", + "apps.suggestion.no_static": "No matching options.", + "apps.suggestion.no_suggestion": "No matching suggestions.", + "archivedChannelMessage": "You are viewing an **archived channel**. New messages cannot be posted.", + "autocomplete_selector.unknown_channel": "Unknown channel", + "browse_channels.archivedChannels": "Archived Channels", + "browse_channels.dropdownTitle": "Show", + "browse_channels.noMore": "No more channels to join", + "browse_channels.publicChannels": "Public Channels", + "browse_channels.sharedChannels": "Shared Channels", + "browse_channels.showArchivedChannels": "Show: Archived Channels", + "browse_channels.showPublicChannels": "Show: Public Channels", + "browse_channels.showSharedChannels": "Show: Shared Channels", + "browse_channels.title": "Browse channels", + "camera_type.photo.option": "Capture Photo", + "camera_type.video.option": "Record Video", + "center_panel.archived.closeChannel": "Close Channel", + "channel_header.directchannel.you": "{displayName} (you)", + "channel_header.info": "View info", + "channel_header.member_count": "{count, plural, one {# member} other {# members}}", + "channel_info.alert_retry": "Try Again", + "channel_info.alertNo": "No", + "channel_info.alertYes": "Yes", + "channel_info.archive": "Archive Channel", + "channel_info.archive_description.can_view_archived": "This will archive the channel from the team. Channel contents will still be accessible by channel members.\n\nAre you sure you wish to archive the {term} {name}?", + "channel_info.archive_description.cannot_view_archived": "This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.\n\nAre you sure you wish to archive the {term} {name}?", + "channel_info.archive_failed": "An error occurred trying to archive the channel {displayName}", + "channel_info.archive_title": "Archive {term}", + "channel_info.close": "Close", + "channel_info.close_dm": "Close direct message", + "channel_info.close_dm_channel": "Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.", + "channel_info.close_gm": "Close group message", + "channel_info.close_gm_channel": "Are you sure you want to close this group message? This will remove it from your home screen, but you can always open it again.", + "channel_info.convert_failed": "We were unable to convert {displayName} to a private channel.", + "channel_info.convert_private": "Convert to private channel", + "channel_info.convert_private_description": "When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?", + "channel_info.convert_private_success": "{displayName} is now a private channel.", + "channel_info.convert_private_title": "Convert {displayName} to a private channel?", + "channel_info.copied": "Copied", + "channel_info.copy_link": "Copy Link", + "channel_info.custom_status": "Custom status:", + "channel_info.edit_header": "Edit Header", + "channel_info.error_close": "Close", + "channel_info.favorite": "Favorite", + "channel_info.favorited": "Favorited", + "channel_info.header": "Header:", + "channel_info.ignore_mentions": "Ignore @channel, @here, @all", + "channel_info.leave": "Leave", + "channel_info.leave_channel": "Leave channel", + "channel_info.leave_private_channel": "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.", + "channel_info.leave_public_channel": "Are you sure you want to leave the public channel {displayName}? You can always rejoin.", + "channel_info.local_time": "Local Time", + "channel_info.members": "Members", + "channel_info.mention": "Mention", + "channel_info.mobile_notifications": "Mobile Notifications", + "channel_info.muted": "Mute", + "channel_info.nickname": "Nickname", + "channel_info.notification.all": "All", + "channel_info.notification.default": "Default", + "channel_info.notification.mention": "Mentions", + "channel_info.notification.none": "Never", + "channel_info.pinned_messages": "Pinned Messages", + "channel_info.position": "Position", + "channel_info.private_channel": "Private Channel", + "channel_info.public_channel": "Public Channel", + "channel_info.send_a_mesasge": "Send a message", + "channel_info.send_mesasge": "Send message", + "channel_info.set_header": "Set Header", + "channel_info.unarchive": "Unarchive Channel", + "channel_info.unarchive_description": "Are you sure you want to unarchive the {term} {name}?", + "channel_info.unarchive_failed": "An error occurred trying to unarchive the channel {displayName}", + "channel_info.unarchive_title": "Unarchive {term}", + "channel_intro.createdBy": "Created by {user} on {date}", + "channel_intro.createdOn": "Created on {date}", + "channel_list.channels_category": "Channels", + "channel_list.dms_category": "Direct messages", + "channel_list.favorites_category": "Favorites", + "channel_list.find_channels": "Find channels...", + "channel_loader.someone": "Someone", + "channel_modal.descriptionHelp": "Describe how this channel should be used.", + "channel_modal.header": "Header", + "channel_modal.headerEx": "Use Markdown to format header text", + "channel_modal.headerHelp": "Specify text to appear in the channel header beside the channel name. For example, include frequently used links by typing link text [Link Title](http://example.com).", + "channel_modal.makePrivate.description": "When a channel is set to private, only invited team members can access and participate in that channel", + "channel_modal.makePrivate.label": "Make Private", + "channel_modal.name": "Name", + "channel_modal.nameEx": "Bugs, Marketing", + "channel_modal.optional": "(optional)", + "channel_modal.purpose": "Purpose", + "channel_modal.purposeEx": "A channel to file bugs and improvements", + "combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.", + "combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.", + "combined_system_message.added_to_channel.one_you": "You were **added to the channel** by {actor}.", + "combined_system_message.added_to_channel.two": "{firstUser} and {secondUser} **added to the channel** by {actor}.", + "combined_system_message.added_to_team.many_expanded": "{users} and {lastUser} were **added to the team** by {actor}.", + "combined_system_message.added_to_team.one": "{firstUser} **added to the team** by {actor}.", + "combined_system_message.added_to_team.one_you": "You were **added to the team** by {actor}.", + "combined_system_message.added_to_team.two": "{firstUser} and {secondUser} **added to the team** by {actor}.", + "combined_system_message.joined_channel.many_expanded": "{users} and {lastUser} **joined the channel**.", + "combined_system_message.joined_channel.one": "{firstUser} **joined the channel**.", + "combined_system_message.joined_channel.one_you": "You **joined the channel**.", + "combined_system_message.joined_channel.two": "{firstUser} and {secondUser} **joined the channel**.", + "combined_system_message.joined_team.many_expanded": "{users} and {lastUser} **joined the team**.", + "combined_system_message.joined_team.one": "{firstUser} **joined the team**.", + "combined_system_message.joined_team.one_you": "You **joined the team**.", + "combined_system_message.joined_team.two": "{firstUser} and {secondUser} **joined the team**.", + "combined_system_message.left_channel.many_expanded": "{users} and {lastUser} **left the channel**.", + "combined_system_message.left_channel.one": "{firstUser} **left the channel**.", + "combined_system_message.left_channel.one_you": "You **left the channel**.", + "combined_system_message.left_channel.two": "{firstUser} and {secondUser} **left the channel**.", + "combined_system_message.left_team.many_expanded": "{users} and {lastUser} **left the team**.", + "combined_system_message.left_team.one": "{firstUser} **left the team**.", + "combined_system_message.left_team.one_you": "You **left the team**.", + "combined_system_message.left_team.two": "{firstUser} and {secondUser} **left the team**.", + "combined_system_message.removed_from_channel.many_expanded": "{users} and {lastUser} were **removed from the channel**.", + "combined_system_message.removed_from_channel.one": "{firstUser} was **removed from the channel**.", + "combined_system_message.removed_from_channel.one_you": "You were **removed from the channel**.", + "combined_system_message.removed_from_channel.two": "{firstUser} and {secondUser} were **removed from the channel**.", + "combined_system_message.removed_from_team.many_expanded": "{users} and {lastUser} were **removed from the team**.", + "combined_system_message.removed_from_team.one": "{firstUser} was **removed from the team**.", + "combined_system_message.removed_from_team.one_you": "You were **removed from the team**.", + "combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.", + "combined_system_message.you": "You", + "connection_banner.connected": "Connection restored", + "connection_banner.connecting": "Connecting...", + "connection_banner.not_connected": "No internet connection", + "connection_banner.not_reachable": "The server is not reachable", + "create_direct_message.title": "Create Direct Message", + "create_post.deactivated": "You are viewing an archived channel with a deactivated user.", + "create_post.thread_reply": "Reply to this thread...", + "create_post.write": "Write to {channelDisplayName}", + "custom_status.expiry_dropdown.custom": "Custom", + "custom_status.expiry_dropdown.date_and_time": "Date and Time", + "custom_status.expiry_dropdown.dont_clear": "Don't clear", + "custom_status.expiry_dropdown.four_hours": "4 hours", + "custom_status.expiry_dropdown.one_hour": "1 hour", + "custom_status.expiry_dropdown.thirty_minutes": "30 minutes", + "custom_status.expiry_dropdown.this_week": "This week", + "custom_status.expiry_dropdown.today": "Today", + "custom_status.expiry_time.today": "Today", + "custom_status.expiry_time.tomorrow": "Tomorrow", + "custom_status.expiry.at": "at", + "custom_status.expiry.until": "Until", + "custom_status.failure_message": "Failed to update status. Try again", + "custom_status.set_status": "Set a custom status", + "custom_status.suggestions.in_a_meeting": "In a meeting", + "custom_status.suggestions.on_a_vacation": "On a vacation", + "custom_status.suggestions.out_for_lunch": "Out for lunch", + "custom_status.suggestions.out_sick": "Out sick", + "custom_status.suggestions.recent_title": "Recent", + "custom_status.suggestions.title": "Suggestions", + "custom_status.suggestions.working_from_home": "Working from home", + "date_separator.today": "Today", + "date_separator.yesterday": "Yesterday", + "default_skin_tone": "Default Skin Tone", + "display_settings.clock.military": "24-hour", + "display_settings.clock.standard": "12-hour", + "display_settings.clockDisplay": "Clock Display", + "display_settings.crt": "Collapsed Reply Threads", + "display_settings.crt.off": "Off", + "display_settings.crt.on": "On", + "display_settings.theme": "Theme", + "display_settings.timezone": "Timezone", + "display_settings.tz.auto": "Auto", + "display_settings.tz.manual": "Manual", + "download.error": "Unable to download the file. Try again later", + "edit_post.editPost": "Edit the post...", + "edit_post.save": "Save", + "edit_server.description": "Specify a display name for this server", + "edit_server.display_help": "Server: {url}", + "edit_server.save": "Save", + "edit_server.saving": "Saving", + "edit_server.title": "Edit server name", + "emoji_picker.activities": "Activities", + "emoji_picker.animals-nature": "Animals & Nature", + "emoji_picker.custom": "Custom", + "emoji_picker.flags": "Flags", + "emoji_picker.food-drink": "Food & Drink", + "emoji_picker.objects": "Objects", + "emoji_picker.people-body": "People & Body", + "emoji_picker.recent": "Recently Used", + "emoji_picker.searchResults": "Search Results", + "emoji_picker.smileys-emotion": "Smileys & Emotion", + "emoji_picker.symbols": "Symbols", + "emoji_picker.travel-places": "Travel & Places", + "emoji_skin.dark_skin_tone": "dark skin tone", + "emoji_skin.default": "default skin tone", + "emoji_skin.light_skin_tone": "light skin tone", + "emoji_skin.medium_dark_skin_tone": "medium dark skin tone", + "emoji_skin.medium_light_skin_tone": "medium light skin tone", + "emoji_skin.medium_skin_tone": "medium skin tone", + "extension.no_memberships.description": "To share content, you'll need to be a member of a team on a Mattermost server.", + "extension.no_memberships.title": "Not a member of any team yet", + "extension.no_servers.description": "To share content, you'll need to be logged in to a Mattermost server.", + "extension.no_servers.title": "Not connected to any servers", + "file_upload.fileAbove": "Files must be less than {max}", + "find_channels.directory": "Directory", + "find_channels.new_channel": "New Channel", + "find_channels.open_dm": "Open a DM", + "find_channels.title": "Find Channels", + "friendly_date.daysAgo": "{count} {count, plural, one {day} other {days}} ago", + "friendly_date.hoursAgo": "{count} {count, plural, one {hour} other {hours}} ago", + "friendly_date.minsAgo": "{count} {count, plural, one {min} other {mins}} ago", + "friendly_date.monthsAgo": "{count} {count, plural, one {month} other {months}} ago", + "friendly_date.now": "Now", + "friendly_date.yearsAgo": "{count} {count, plural, one {year} other {years}} ago", + "friendly_date.yesterday": "Yesterday", + "gallery.copy_link.failed": "Failed to copy link to clipboard", + "gallery.downloading": "Downloading...", + "gallery.footer.channel_name": "Shared in {channelName}", + "gallery.image_saved": "Image saved", + "gallery.open_file": "Open file", + "gallery.opening": "Opening...", + "gallery.preparing": "Preparing...", + "gallery.save_failed": "Unable to save the file", + "gallery.unsupported": "Preview isn't supported for this file type. Try downloading or sharing to open it in another app.", + "gallery.video_saved": "Video saved", + "general_settings.about": "About {appTitle}", + "general_settings.advanced_settings": "Advanced Settings", + "general_settings.display": "Display", + "general_settings.help": "Help", + "general_settings.notifications": "Notifications", + "general_settings.report_problem": "Report a problem", + "get_post_link_modal.title": "Copy Link", + "global_threads.allThreads": "All Your Threads", + "global_threads.emptyThreads.message": "Any threads you are mentioned in or have participated in will show here along with any threads you have followed.", + "global_threads.emptyThreads.title": "No followed threads yet", + "global_threads.emptyUnreads.message": "Looks like you're all caught up.", + "global_threads.emptyUnreads.title": "No unread threads", + "global_threads.markAllRead.cancel": "Cancel", + "global_threads.markAllRead.markRead": "Mark read", + "global_threads.markAllRead.message": "This will clear any unread status for all of your threads shown here", + "global_threads.markAllRead.title": "Are you sure you want to mark all threads as read?", + "global_threads.options.mark_as_read": "Mark as Read", + "global_threads.options.open_in_channel": "Open in Channel", + "global_threads.options.title": "Thread Actions", + "global_threads.unreads": "Unreads", + "home.header.plus_menu": "Options", + "integration_selector.multiselect.submit": "Done", + "interactive_dialog.submit": "Submit", + "intro.add_people": "Add People", + "intro.channel_info": "Info", + "intro.created_by": "created by {creator} on {date}.", + "intro.direct_message": "This is the start of your conversation with {teammate}. Messages and files shared here are not shown to anyone else.", + "intro.group_message": "This is the start of your conversation with this group. Messages and files shared here are not shown to anyone else outside of the group.", + "intro.private_channel": "Private Channel", + "intro.public_channel": "Public Channel", + "intro.townsquare": "Welcome to {name}. Everyone automatically becomes a member of this channel when they join the team.", + "intro.welcome": "Welcome to {displayName} channel.", + "intro.welcome.private": "Only invited members can see messages posted in this private channel.", + "intro.welcome.public": "Add some more team members to the channel or start a conversation below.", + "invite_people_to_team.message": "Here’s a link to collaborate and communicate with us on Mattermost.", + "invite_people_to_team.title": "Join the {team} team", + "invite.members.already_member": "This person is already a team member", + "invite.members.user_is_guest": "Contact your admin to make this guest a full member", + "invite.search.email_invite": "invite", + "invite.search.no_results": "No one found matching", + "invite.searchPlaceholder": "Type a name or email address…", + "invite.send_error": "Something went wrong while trying to send invitations. Please check your network connection and try again.", + "invite.send_invite": "Send", + "invite.sendInvitationsTo": "Send invitations to…", + "invite.shareLink": "Share link", + "invite.summary.done": "Done", + "invite.summary.email_invite": "An invitation email has been sent", + "invite.summary.error": "{invitationsCount, plural, one {Invitation} other {Invitations}} could not be sent successfully", + "invite.summary.member_invite": "Invited as a member of {teamDisplayName}", + "invite.summary.not_sent": "{notSentCount, plural, one {Invitation wasn’t} other {Invitations weren’t}} sent", + "invite.summary.report.notSent": "{count} {count, plural, one {invitation} other {invitations}} not sent", + "invite.summary.report.sent": "{count} successful {count, plural, one {invitation} other {invitations}}", + "invite.summary.sent": "Your {sentCount, plural, one {invitation has} other {invitations have}} been sent", + "invite.summary.smtp_failure": "SMTP is not configured in System Console", + "invite.summary.some_not_sent": "{notSentCount, plural, one {An invitation was} other {Some invitations were}} not sent", + "invite.summary.try_again": "Try again", + "invite.title": "Invite", + "invite.title.summary": "Invite summary", + "join_team.error.group_error": "You need to be a member of a linked group to join this team.", + "join_team.error.message": "There has been an error joining the team", + "join_team.error.title": "Error joining a team", + "last_users_message.added_to_channel.type": "were **added to the channel** by {actor}.", + "last_users_message.added_to_team.type": "were **added to the team** by {actor}.", + "last_users_message.first": "{firstUser} and ", + "last_users_message.joined_channel.type": "**joined the channel**.", + "last_users_message.joined_team.type": "**joined the team**.", + "last_users_message.left_channel.type": "**left the channel**.", + "last_users_message.left_team.type": "**left the team**.", + "last_users_message.others": "{numOthers} others ", + "last_users_message.removed_from_channel.type": "were **removed from the channel**.", + "last_users_message.removed_from_team.type": "were **removed from the team**.", + "load_categories_error.message": "There was a problem loading content for this server.", + "load_categories_error.title": "Couldn't load categories for {serverName}", + "load_channels_error.message": "There was a problem loading content for this team.", + "load_channels_error.title": "Couldn't load {teamDisplayName}", + "load_teams_error.message": "There was a problem loading content for this server.", + "load_teams_error.title": "Couldn't load {serverName}", + "login_mfa.enterToken": "To complete the sign in process, please enter the code from your mobile device's authenticator app.", + "login_mfa.token": "Enter MFA Token", + "login_mfa.tokenReq": "Please enter an MFA token", + "login.email": "Email", + "login.forgot": "Forgot your password?", + "login.invalid_credentials": "The email and password combination is incorrect", + "login.ldapUsername": "AD/LDAP Username", + "login.or": "or", + "login.password": "Password", + "login.signIn": "Log In", + "login.signingIn": "Logging In", + "login.username": "Username", + "markdown.latex.error": "Latex render error", + "mentions.empty.paragraph": "You'll see messages here when someone mentions you or uses terms you're monitoring.", + "mentions.empty.title": "No Mentions yet", + "mobile.about.appVersion": "App Version: {version} (Build {number})", + "mobile.account.settings.save": "Save", + "mobile.action_menu.select": "Select an option", + "mobile.add_team.create_team": "Create a new team", + "mobile.add_team.join_team": "Join Another Team", + "mobile.android.back_handler_exit": "Press back again to exit", + "mobile.android.photos_permission_denied_description": "Upload photos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo library.", + "mobile.android.photos_permission_denied_title": "{applicationName} would like to access your photos", + "mobile.announcement_banner.title": "Announcement", + "mobile.calls_call_ended": "Call ended", + "mobile.calls_call_screen": "Call", + "mobile.calls_call_thread": "Call Thread", + "mobile.calls_current_call": "Current call", + "mobile.calls_disable": "Disable calls", + "mobile.calls_dismiss": "Dismiss", + "mobile.calls_enable": "Enable calls", + "mobile.calls_end_call_title": "End call", + "mobile.calls_end_msg_channel": "Are you sure you want to end a call with {numParticipants} participants in {displayName}?", + "mobile.calls_end_msg_channel_default": "Are you sure you want to end the call?", + "mobile.calls_end_msg_dm": "Are you sure you want to end a call with {displayName}?", + "mobile.calls_end_permission_msg": "You don't have permission to end the call. Please ask the call creator to end the call.", + "mobile.calls_end_permission_title": "Error", + "mobile.calls_ended_at": "Ended at", + "mobile.calls_error_message": "Error: {error}", + "mobile.calls_error_title": "Error", + "mobile.calls_host": "host", + "mobile.calls_host_rec": "You are recording this meeting. Consider letting everyone know that this meeting is being recorded.", + "mobile.calls_host_rec_stopped": "You can find the recording in this call's chat thread once it's finished processing.", + "mobile.calls_host_rec_stopped_title": "Recording has stopped. Processing...", + "mobile.calls_host_rec_title": "You are recording", + "mobile.calls_join_call": "Join call", + "mobile.calls_lasted": "Lasted {duration}", + "mobile.calls_leave": "Leave", + "mobile.calls_leave_call": "Leave call", + "mobile.calls_limit_msg": "The maximum number of participants per call is {maxParticipants}. Contact your System Admin to increase the limit.", + "mobile.calls_limit_msg_GA": "Upgrade to Cloud Professional or Cloud Enterprise to enable group calls with more than {maxParticipants} participants.", + "mobile.calls_limit_reached": "Participant limit reached", + "mobile.calls_lower_hand": "Lower hand", + "mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.", + "mobile.calls_more": "More", + "mobile.calls_mute": "Mute", + "mobile.calls_name_is_talking": "{name} is talking", + "mobile.calls_name_started_call": "{name} started a call", + "mobile.calls_noone_talking": "No one is talking", + "mobile.calls_not_available_msg": "Please contact your System Admin to enable the feature.", + "mobile.calls_not_available_option": "(Not available)", + "mobile.calls_not_available_title": "Calls is not enabled", + "mobile.calls_not_connected": "You're not connected to a call in the current channel.", + "mobile.calls_ok": "OK", + "mobile.calls_okay": "Okay", + "mobile.calls_open_channel": "Open Channel", + "mobile.calls_participant_limit_title_GA": "This call is at capacity", + "mobile.calls_participant_rec": "The host has started recording this meeting. By staying in the meeting you give consent to being recorded.", + "mobile.calls_participant_rec_title": "Recording is in progress", + "mobile.calls_raise_hand": "Raise hand", + "mobile.calls_react": "React", + "mobile.calls_rec": "rec", + "mobile.calls_record": "Record", + "mobile.calls_request_message": "Calls are currently running in test mode and only system admins can start them. Reach out directly to your system admin for assistance", + "mobile.calls_request_title": "Calls is not currently enabled", + "mobile.calls_see_logs": "See server logs", + "mobile.calls_speaker": "Speaker", + "mobile.calls_start_call": "Start Call", + "mobile.calls_start_call_exists": "A call is already ongoing in the channel.", + "mobile.calls_stop_recording": "Stop Recording", + "mobile.calls_unmute": "Unmute", + "mobile.calls_viewing_screen": "You are viewing {name}'s screen", + "mobile.calls_you": "(you)", + "mobile.camera_photo_permission_denied_description": "Take photos and upload them to your server or save them to your device. Open Settings to grant {applicationName} read and write access to your camera.", + "mobile.camera_photo_permission_denied_title": "{applicationName} would like to access your camera", + "mobile.camera_type.title": "Camera options", + "mobile.channel_info.alertNo": "No", + "mobile.channel_info.alertYes": "Yes", + "mobile.channel_list.recent": "Recent", + "mobile.channel_list.unreads": "Unreads", + "mobile.commands.error_title": "Error Executing Command", + "mobile.components.select_server_view.connect": "Connect", + "mobile.components.select_server_view.connecting": "Connecting", + "mobile.components.select_server_view.displayHelp": "Choose a display name for your server", + "mobile.components.select_server_view.displayName": "Display Name", + "mobile.components.select_server_view.enterServerUrl": "Enter Server URL", + "mobile.components.select_server_view.msg_connect": "Let’s Connect to a Server", + "mobile.components.select_server_view.msg_description": "A Server is your team's communication hub which is accessed through a unique URL", + "mobile.components.select_server_view.msg_welcome": "Welcome", + "mobile.components.select_server_view.proceed": "Proceed", + "mobile.create_channel": "Create", + "mobile.create_channel.title": "New channel", + "mobile.create_direct_message.max_limit_reached": "Group messages are limited to {maxCount} members", + "mobile.create_direct_message.start": "Start Conversation", + "mobile.create_direct_message.you": "@{username} - you", + "mobile.create_post.read_only": "This channel is read-only.", + "mobile.custom_list.no_results": "No Results", + "mobile.custom_status.choose_emoji": "Choose an emoji", + "mobile.custom_status.clear_after": "Clear After", + "mobile.custom_status.clear_after.title": "Clear Custom Status After", + "mobile.custom_status.modal_confirm": "Done", + "mobile.direct_message.error": "We couldn't open a DM with {displayName}.", + "mobile.display_settings.clockDisplay": "Clock Display", + "mobile.display_settings.crt": "Collapsed Reply Threads", + "mobile.display_settings.theme": "Theme", + "mobile.display_settings.timezone": "Timezone", + "mobile.document_preview.failed_description": "An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n", + "mobile.document_preview.failed_title": "Open Document failed", + "mobile.downloader.disabled_description": "File downloads are disabled on this server. Please contact your System Admin for more details.\n", + "mobile.downloader.disabled_title": "Download disabled", + "mobile.downloader.failed_description": "An error occurred while downloading the file. Please check your internet connection and try again.\n", + "mobile.downloader.failed_title": "Download failed", + "mobile.edit_channel": "Save", + "mobile.edit_post.delete_question": "Are you sure you want to delete this Post?", + "mobile.edit_post.delete_title": "Confirm Post Delete", + "mobile.edit_post.error": "There was a problem editing this message. Please try again.", + "mobile.edit_post.title": "Editing Message", + "mobile.error_handler.button": "Relaunch", + "mobile.error_handler.description": "\nTap relaunch to open the app again. After restart, you can report the problem from the settings menu.\n", + "mobile.error_handler.title": "Unexpected error occurred", + "mobile.file_upload.disabled2": "File uploads from mobile are disabled.", + "mobile.file_upload.max_warning": "Uploads limited to {count} files maximum.", + "mobile.files_paste.error_description": "An error occurred while pasting the file(s). Please try again.", + "mobile.files_paste.error_dismiss": "Dismiss", + "mobile.files_paste.error_title": "Paste failed", + "mobile.gallery.title": "{index} of {total}", + "mobile.integration_selector.loading_channels": "Loading channels...", + "mobile.integration_selector.loading_options": "Loading options...", + "mobile.ios.photos_permission_denied_description": "Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.", + "mobile.ios.photos_permission_denied_title": "{applicationName} would like to access your photos", + "mobile.join_channel.error": "We couldn't join the channel {displayName}.", + "mobile.leave_and_join_confirmation": "Leave & Join", + "mobile.leave_and_join_message": "You are already on a channel call in ~{leaveChannelName}. Do you want to leave your current call and join the call in ~{joinChannelName}?", + "mobile.leave_and_join_title": "Are you sure you want to switch to a different call?", + "mobile.link.error.text": "Unable to open the link.", + "mobile.link.error.title": "Error", + "mobile.login_options.cant_heading": "Can't Log In", + "mobile.login_options.enter_credentials": "Enter your login details below.", + "mobile.login_options.gitlab": "GitLab", + "mobile.login_options.google": "Google", + "mobile.login_options.heading": "Log In to Your Account", + "mobile.login_options.none": "You can't log in to your account yet. At least one login option must be configured. Contact your System Admin for assistance.", + "mobile.login_options.office365": "Office 365", + "mobile.login_options.openid": "Open ID", + "mobile.login_options.saml": "SAML", + "mobile.login_options.select_option": "Select a login option below.", + "mobile.login_options.separator_text": "or log in with", + "mobile.login_options.sso_continue": "Continue with", + "mobile.manage_members.admin": "Admin", + "mobile.manage_members.cancel": "Cancel", + "mobile.manage_members.change_role.error": "An error occurred while trying to update the role. Please check your connection and try again.", + "mobile.manage_members.done": "Done", + "mobile.manage_members.make_channel_admin": "Make Channel Admin", + "mobile.manage_members.make_channel_member": "Make Channel Member", + "mobile.manage_members.manage": "Manage", + "mobile.manage_members.manage_member": "Manage member", + "mobile.manage_members.member": "Member", + "mobile.manage_members.message": "Are you sure you want to remove the selected member from the channel?", + "mobile.manage_members.remove": "Remove", + "mobile.manage_members.remove_member": "Remove from Channel", + "mobile.manage_members.section_title_admins": "CHANNEL ADMINS", + "mobile.manage_members.section_title_members": "MEMBERS", + "mobile.managed.blocked_by": "Blocked by {vendor}", + "mobile.managed.exit": "Exit", + "mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}.\n\nReason {reason}\n\n\n\nDebug info: {debug}\n\nPlease exit the app.", + "mobile.managed.jailbreak_no_debug_info": "Not available", + "mobile.managed.jailbreak_no_reason": "Not available", + "mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.", + "mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.", + "mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.", + "mobile.managed.secured_by": "Secured by {vendor}", + "mobile.managed.settings": "Go to settings", + "mobile.markdown.code.copy_code": "Copy Code", + "mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}", + "mobile.markdown.image.too_large": "Image exceeds max dimensions of {maxWidth} by {maxHeight}:", + "mobile.markdown.link.copy_url": "Copy URL", + "mobile.mention.copy_mention": "Copy Mention", + "mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}", + "mobile.message_length.message_split_left": "Message exceeds the character limit", + "mobile.message_length.title": "Message Length", + "mobile.no_results_with_term": "No results for “{term}”", + "mobile.no_results_with_term.files": "No files matching “{term}”", + "mobile.no_results_with_term.messages": "No matches found for “{term}”", + "mobile.no_results.spelling": "Check the spelling or try another search.", + "mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.", + "mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.", + "mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify that a browser is installed on the device.", + "mobile.oauth.something_wrong": "Something went wrong", + "mobile.oauth.something_wrong.okButton": "OK", + "mobile.oauth.switch_to_browser": "You are being redirected to your login provider", + "mobile.oauth.switch_to_browser.error_title": "Sign in error", + "mobile.oauth.switch_to_browser.title": "Redirecting...", + "mobile.oauth.try_again": "Try again", + "mobile.onboarding.next": "Next", + "mobile.onboarding.sign_in": "Sign in", + "mobile.onboarding.sign_in_to_get_started": "Sign in to get started", + "mobile.open_dm.error": "We couldn't open a direct message with {displayName}. Please check your connection and try again.", + "mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.", + "mobile.participants.header": "Thread Participants", + "mobile.permission_denied_dismiss": "Don't Allow", + "mobile.permission_denied_retry": "Settings", + "mobile.post_info.add_reaction": "Add Reaction", + "mobile.post_info.copy_text": "Copy Text", + "mobile.post_info.mark_unread": "Mark as Unread", + "mobile.post_info.pin": "Pin to Channel", + "mobile.post_info.reply": "Reply", + "mobile.post_info.save": "Save", + "mobile.post_info.unpin": "Unpin from Channel", + "mobile.post_info.unsave": "Unsave", + "mobile.post_pre_header.pinned": "Pinned", + "mobile.post_pre_header.pinned_saved": "Pinned and Saved", + "mobile.post_pre_header.saved": "Saved", + "mobile.post_textbox.entire_channel_here.message": "By using @here you are about to send notifications to up to {totalMembers, number} {totalMembers, plural, one {person} other {people}}. Are you sure you want to do this?", + "mobile.post_textbox.entire_channel_here.message.with_timezones": "By using @here you are about to send notifications up to {totalMembers, number} {totalMembers, plural, one {person} other {people}} in {timezones, number} {timezones, plural, one {timezone} other {timezones}}. Are you sure you want to do this?", + "mobile.post_textbox.entire_channel.cancel": "Cancel", + "mobile.post_textbox.entire_channel.confirm": "Confirm", + "mobile.post_textbox.entire_channel.message": "By using @all or @channel you are about to send notifications to {totalMembers, number} {totalMembers, plural, one {person} other {people}}. Are you sure you want to do this?", + "mobile.post_textbox.entire_channel.message.with_timezones": "By using @all or @channel you are about to send notifications to {totalMembers, number} {totalMembers, plural, one {person} other {people}} in {timezones, number} {timezones, plural, one {timezone} other {timezones}}. Are you sure you want to do this?", + "mobile.post_textbox.entire_channel.title": "Confirm sending notifications to entire channel", + "mobile.post_textbox.groups.title": "Confirm sending notifications to groups", + "mobile.post_textbox.uploadFailedDesc": "Some attachments failed to upload to the server. Are you sure you want to post the message?", + "mobile.post_textbox.uploadFailedTitle": "Attachment failure", + "mobile.post.cancel": "Cancel", + "mobile.post.delete_question": "Are you sure you want to delete this post?", + "mobile.post.delete_title": "Delete Post", + "mobile.post.failed_delete": "Delete Message", + "mobile.post.failed_retry": "Try Again", + "mobile.privacy_link": "Privacy Policy", + "mobile.push_notification_reply.button": "Send", + "mobile.push_notification_reply.placeholder": "Write a reply...", + "mobile.push_notification_reply.title": "Reply", + "mobile.rename_channel.display_name_maxLength": "Channel name must be less than {maxLength, number} characters", + "mobile.rename_channel.display_name_minLength": "Channel name must be {minLength, number} or more characters", + "mobile.rename_channel.display_name_required": "Channel name is required", + "mobile.request.invalid_request_method": "Invalid request method", + "mobile.request.invalid_response": "Received invalid response from the server.", + "mobile.reset_status.alert_cancel": "Cancel", + "mobile.reset_status.alert_ok": "OK", + "mobile.reset_status.title_ooo": "Disable \"Out Of Office\"?", + "mobile.routes.code": "{language} Code", + "mobile.routes.code.noLanguage": "Code", + "mobile.routes.custom_status": "Set a custom status", + "mobile.routes.table": "Table", + "mobile.routes.user_profile": "Profile", + "mobile.screen.settings": "Settings", + "mobile.screen.your_profile": "Your Profile", + "mobile.search.jump": "Jump to recent messages", + "mobile.search.modifier.exclude": "exclude search terms", + "mobile.search.modifier.from": "a specific user", + "mobile.search.modifier.in": "a specific channel", + "mobile.search.modifier.phrases": "messages with phrases", + "mobile.search.show_less": "Show less", + "mobile.search.show_more": "Show more", + "mobile.search.team.select": "Select a team to search", + "mobile.server_identifier.exists": "You are already connected to this server.", + "mobile.server_link.error.text": "The link could not be found on this server.", + "mobile.server_link.error.title": "Link Error", + "mobile.server_link.unreachable_channel.error": "This link belongs to a deleted channel or to a channel to which you do not have access.", + "mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.", + "mobile.server_link.unreachable_user.error": "We can't redirect you to the DM. The user specified is unknown.", + "mobile.server_name.exists": "You are using this name for another server.", + "mobile.server_ping_failed": "Cannot connect to the server.", + "mobile.server_requires_client_certificate": "Server requires client certificate for authentication.", + "mobile.server_upgrade.button": "OK", + "mobile.server_upgrade.description": "\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n", + "mobile.server_upgrade.title": "Server upgrade required", + "mobile.server_url.deeplink.emm.denied": "This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server", + "mobile.server_url.empty": "Please enter a valid server URL", + "mobile.server_url.invalid_format": "URL must start with http:// or https://", + "mobile.session_expired": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.", + "mobile.session_expired.title": "Session Expired", + "mobile.set_status.dnd": "Do Not Disturb", + "mobile.storage_permission_denied_description": "Upload files to your server. Open Settings to grant {applicationName} Read and Write access to files on this device.", + "mobile.storage_permission_denied_title": "{applicationName} would like to access your files", + "mobile.suggestion.members": "Members", + "mobile.system_message.channel_archived_message": "{username} archived the channel", + "mobile.system_message.channel_unarchived_message": "{username} unarchived the channel", + "mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}", + "mobile.system_message.update_channel_header_message_and_forget.removed": "{username} removed the channel header (was: {oldHeader})", + "mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} updated the channel header from: {oldHeader} to: {newHeader}", + "mobile.system_message.update_channel_header_message_and_forget.updated_to": "{username} updated the channel header to: {newHeader}", + "mobile.system_message.update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {oldPurpose})", + "mobile.system_message.update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}", + "mobile.system_message.update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {newPurpose}", + "mobile.tos_link": "Terms of Service", + "mobile.user_list.deactivated": "Deactivated", + "mobile.write_storage_permission_denied_description": "Save files to your device. Open Settings to grant {applicationName} write access to files on this device.", + "modal.manual_status.auto_responder.message_": "Would you like to switch your status to \"{status}\" and disable Automatic Replies?", + "modal.manual_status.auto_responder.message_away": "Would you like to switch your status to \"Away\" and disable Automatic Replies?", + "modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to \"Do Not Disturb\" and disable Automatic Replies?", + "modal.manual_status.auto_responder.message_offline": "Would you like to switch your status to \"Offline\" and disable Automatic Replies?", + "modal.manual_status.auto_responder.message_online": "Would you like to switch your status to \"Online\" and disable Automatic Replies?", + "more_messages.text": "{count} new {count, plural, one {message} other {messages}}", + "msg_typing.areTyping": "{users} and {last} are typing...", + "msg_typing.isTyping": "{user} is typing...", + "notification_settings.auto_responder": "Automatic Replies", + "notification_settings.auto_responder.default_message": "Hello, I am out of office and unable to respond to messages.", + "notification_settings.auto_responder.footer.message": "Set a custom message that is automatically sent in response to direct messages, such as an out of office or vacation reply. Enabling this setting changes your status to Out of Office and disables notifications.", + "notification_settings.auto_responder.message": "Message", + "notification_settings.auto_responder.to.enable": "Enable automatic replies", + "notification_settings.email": "Email Notifications", + "notification_settings.email.crt.emailInfo": "When enabled, any reply to a thread you're following will send an email notification", + "notification_settings.email.crt.send": "Thread reply notifications", + "notification_settings.email.emailHelp2": "Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.", + "notification_settings.email.emailInfo": "Email notifications are sent for mentions and direct messages when you are offline or away for more than 5 minutes.", + "notification_settings.email.everyHour": "Every hour", + "notification_settings.email.fifteenMinutes": "Every 15 minutes", + "notification_settings.email.immediately": "Immediately", + "notification_settings.email.never": "Never", + "notification_settings.email.send": "Send email notifications", + "notification_settings.mention.reply": "Send reply notifications for", + "notification_settings.mentions": "Mentions", + "notification_settings.mentions_replies": "Mentions and Replies", + "notification_settings.mentions..keywordsDescription": "Other words that trigger a mention", + "notification_settings.mentions.channelWide": "Channel-wide mentions", + "notification_settings.mentions.keywords": "Keywords", + "notification_settings.mentions.keywords_mention": "Keywords that trigger mentions", + "notification_settings.mentions.keywordsLabel": "Keywords are not case-sensitive. Separate keywords with commas.", + "notification_settings.mentions.sensitiveName": "Your case sensitive first name", + "notification_settings.mentions.sensitiveUsername": "Your non-case sensitive username", + "notification_settings.mobile": "Push Notifications", + "notification_settings.mobile.away": "Away or offline", + "notification_settings.mobile.offline": "Offline", + "notification_settings.mobile.online": "Online, away or offline", + "notification_settings.mobile.trigger_push": "Trigger push notifications when...", + "notification_settings.ooo_auto_responder": "Automatic replies", + "notification_settings.push_notification": "Push Notifications", + "notification_settings.push_threads.following": "Notify me about replies to threads I'm following in this channel", + "notification_settings.push_threads.replies": "Thread replies", + "notification_settings.pushNotification.all_new_messages": "All new messages", + "notification_settings.pushNotification.disabled_long": "Push notifications for mobile devices have been disabled by your System Administrator.", + "notification_settings.pushNotification.mentions_only": "Mentions, direct messages only (default)", + "notification_settings.pushNotification.nothing": "Nothing", + "notification_settings.send_notification.about": "Notify me about...", + "notification_settings.threads_mentions": "Mentions in threads", + "notification_settings.threads_start": "Threads that I start", + "notification_settings.threads_start_participate": "Threads that I start or participate in", + "notification.message_not_found": "Message not found", + "notification.not_channel_member": "This message belongs to a channel where you are not a member.", + "notification.not_team_member": "This message belongs to a team where you are not a member.", + "onboarding.calls": "Start secure audio calls instantly", + "onboarding.calls_description": "When typing isn’t fast enough, switch from channel-based chat to secure audio calls with a single tap.", + "onboarding.integrations": "Integrate with tools you love", + "onboarding.integrations_description": "Go beyond chat with tightly-integrated product solutions matched to common development processes.", + "onboarding.realtime_collaboration": "Collaborate in real‑time", + "onboarding.realtime_collaboration_description": "Persistent channels, direct messaging, and file sharing works seamlessly so you can stay connected, wherever you are.", + "onboarding.welcome": "Welcome", + "onboaring.welcome_description": "Mattermost is an open source platform for developer collaboration. Secure, flexible, and integrated with your tools.", + "password_send.description": "To reset your password, enter the email address you used to sign up", + "password_send.error": "Please enter a valid email address.", + "password_send.generic_error": "We were unable to send you a reset password link. Please contact your System Admin for assistance.", + "password_send.link": "If the account exists, a password reset email will be sent to:", + "password_send.link.title": "Reset Link Sent", + "password_send.reset": "Reset Your Password", + "password_send.return": "Return to Log In", + "permalink.error.access.text": "The message you are trying to view is in a channel you don’t have access to or has been deleted.", + "permalink.error.access.title": "Message not viewable", + "permalink.error.cancel": "Cancel", + "permalink.error.okay": "Okay", + "permalink.error.private_channel_and_team.button": "Join channel and team", + "permalink.error.private_channel_and_team.text": "The message you are trying to view is in a private channel in a team you are not a member of. You have access as an admin. Do you want to join **{channelName}** and the **{teamName}** team to view it?", + "permalink.error.private_channel_and_team.title": "Join private channel and team", + "permalink.error.private_channel.button": "Join channel", + "permalink.error.private_channel.text": "The message you are trying to view is in a private channel you have not been invited to, but you have access as an admin. Do you still want to join **{channelName}**?", + "permalink.error.private_channel.title": "Join private channel", + "permalink.error.public_channel_and_team.button": "Join channel and team", + "permalink.error.public_channel_and_team.text": "The message you are trying to view is in a channel you don’t belong and a team you are not a member of. Do you want to join **{channelName}** and the **{teamName}** team to view it?", + "permalink.error.public_channel_and_team.title": "Join channel and team", + "permalink.error.public_channel.button": "Join channel", + "permalink.error.public_channel.text": "The message you are trying to view is in a channel you don’t belong to. Do you want to join **{channelName}** to view it?", + "permalink.error.public_channel.title": "Join channel", + "permalink.show_dialog_warn.cancel": "Cancel", + "permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?", + "permalink.show_dialog_warn.join": "Join", + "permalink.show_dialog_warn.title": "Join private channel", + "pinned_messages.empty.paragraph": "To pin important messages, long-press on a message and choose Pin To Channel. Pinned messages will be visible to everyone in this channel.", + "pinned_messages.empty.title": "No pinned messages yet", + "plus_menu.browse_channels.title": "Browse Channels", + "plus_menu.create_new_channel.title": "Create New Channel", + "plus_menu.invite_people_to_team.title": "Invite people to the team", + "plus_menu.open_direct_message.title": "Open a Direct Message", + "post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They are also not a member of the groups linked to this channel.", + "post_body.check_for_out_of_channel_mentions.link.and": " and ", + "post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel", + "post_body.check_for_out_of_channel_mentions.link.public": "add them to the channel", + "post_body.check_for_out_of_channel_mentions.message_last": "? They will have access to all message history.", + "post_body.check_for_out_of_channel_mentions.message.multiple": "were mentioned but they are not in the channel. Would you like to ", + "post_body.check_for_out_of_channel_mentions.message.one": "was mentioned but is not in the channel. Would you like to ", + "post_body.commentedOn": "Commented on {name}{apostrophe} message: ", + "post_body.deleted": "(message deleted)", + "post_info.auto_responder": "Automatic Reply", + "post_info.bot": "Bot", + "post_info.del": "Delete", + "post_info.edit": "Edit", + "post_info.guest": "Guest", + "post_info.system": "System", + "post_message_view.edited": "(edited)", + "post_priority.label.important": "IMPORTANT", + "post_priority.label.urgent": "URGENT", + "post_priority.picker.beta": "BETA", + "post_priority.picker.label.important": "Important", + "post_priority.picker.label.standard": "Standard", + "post_priority.picker.label.urgent": "Urgent", + "post_priority.picker.title": "Message priority", + "post.options.title": "Options", + "post.reactions.title": "Reactions", + "posts_view.newMsg": "New Messages", + "public_link_copied": "Link copied to clipboard", + "rate.button.needs_work": "Needs work", + "rate.button.yes": "Love it!", + "rate.dont_ask_again": "Don't ask me again", + "rate.error.text": "There has been an error while opening the review modal.", + "rate.error.title": "Error", + "rate.subtitle": "Let us know what you think.", + "rate.title": "Enjoying Mattermost?", + "saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.", + "saved_messages.empty.title": "No saved messages yet", + "screen.mentions.subtitle": "Messages you've been mentioned in", + "screen.mentions.title": "Recent Mentions", + "screen.saved_messages.subtitle": "All messages you've saved for follow up", + "screen.saved_messages.title": "Saved Messages", + "screen.search.header.files": "Files", + "screen.search.header.messages": "Messages", + "screen.search.modifier.header": "Search options", + "screen.search.placeholder": "Search messages & files", + "screen.search.results.file_options.copy_link": "Copy link", + "screen.search.results.file_options.download": "Download", + "screen.search.results.file_options.open_in_channel": "Open in channel", + "screen.search.results.filter.all_file_types": "All file types", + "screen.search.results.filter.audio": "Audio", + "screen.search.results.filter.code": "Code", + "screen.search.results.filter.documents": "Documents", + "screen.search.results.filter.images": "Images", + "screen.search.results.filter.presentations": "Presentations", + "screen.search.results.filter.spreadsheets": "Spreadsheets", + "screen.search.results.filter.title": "Filter by file type", + "screen.search.results.filter.videos": "Videos", + "screen.search.title": "Search", + "screens.channel_edit": "Edit Channel", + "screens.channel_edit_header": "Edit Channel Header", + "screens.channel_info": "Channel Info", + "screens.channel_info.dm": "Direct message info", + "screens.channel_info.gm": "Group message info", + "search_bar.search": "Search", + "search_bar.search.placeholder": "Search timezone", + "select_team.description": "You are not yet a member of any teams. Select one below to get started.", + "select_team.no_team.description": "To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.", + "select_team.no_team.title": "No teams are available to join", + "select_team.title": "Select a team", + "server_list.push_proxy_error": "Notifications cannot be received from this server because of its configuration. Contact your system admin.", + "server_list.push_proxy_unknown": "Notifications could not be received from this server because of its configuration. Log out and Log in again to retry.", + "server_upgrade.alert_description": "Your server, {serverDisplayName}, is running an unsupported server version. Users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {supportedServerVersion} or later is required.", + "server_upgrade.dismiss": "Dismiss", + "server_upgrade.learn_more": "Learn More", + "server.logout.alert_description": "All associated data will be removed", + "server.logout.alert_title": "Are you sure you want to log out of {displayName}?", + "server.remove.alert_description": "This will remove it from your list of servers. All associated data will be removed", + "server.remove.alert_title": "Are you sure you want to remove {displayName}?", + "server.tutorial.swipe": "Swipe left on a server to see more actions", + "server.websocket.unreachable": "Server is unreachable.", + "servers.create_button": "Add a server", + "servers.default": "Default Server", + "servers.edit": "Edit", + "servers.login": "Log in", + "servers.logout": "Log out", + "servers.remove": "Remove", + "settings_display.clock.mz": "24-hour clock", + "settings_display.clock.mz.desc": "Example: 16:00", + "settings_display.clock.normal.desc": "Example: 4:00 PM", + "settings_display.clock.standard": "12-hour clock", + "settings_display.crt.desc": "When enabled, reply messages are not shown in the channel and you'll be notified about threads you're following in the \"Threads\" view.", + "settings_display.crt.label": "Collapsed Reply Threads", + "settings_display.custom_theme": "Custom Theme", + "settings_display.timezone.automatically": "Set automatically", + "settings_display.timezone.manual": "Change timezone", + "settings_display.timezone.off": "Off", + "settings_display.timezone.select": "Select Timezone", + "settings.about": "About {appTitle}", + "settings.about.build": "{version} (Build {number})", + "settings.about.copyright": "Copyright 2015-{currentYear} Mattermost, Inc. All rights reserved", + "settings.about.database": "Database:", + "settings.about.database.schema": "Database Schema Version:", + "settings.about.licensed": "Licensed to: {company}", + "settings.about.powered_by": "{site} is powered by Mattermost", + "settings.about.server.version.desc": "Server Version:", + "settings.about.server.version.value": "{version} (Build {number})", + "settings.about.serverVersionNoBuild": "{version}", + "settings.about.version": "App Version:", + "settings.advanced_settings": "Advanced Settings", + "settings.advanced.cancel": "Cancel", + "settings.advanced.delete": "Delete", + "settings.advanced.delete_data": "Delete local files", + "settings.advanced.delete_message.confirmation": "\nThis will delete all files downloaded through the app for this server. Please confirm to proceed.\n", + "settings.display": "Display", + "settings.link.error.text": "Unable to open the link.", + "settings.link.error.title": "Error", + "settings.notice_mobile_link": "mobile apps", + "settings.notice_platform_link": "server", + "settings.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.", + "settings.notifications": "Notifications", + "settings.save": "Save", + "share_extension.channel_error": "You are not a member of a team on the selected server. Select another server or open Mattermost to join a team.", + "share_extension.channel_label": "Channel", + "share_extension.count_limit": "You can only share {count, number} {count, plural, one {file} other {files}} on this server", + "share_extension.file_limit.multiple": "Each file must be less than {size}", + "share_extension.file_limit.single": "File must be less than {size}", + "share_extension.max_resolution": "Image exceeds maximum dimensions of 7680 x 4320 px", + "share_extension.message": "Enter a message (optional)", + "share_extension.multiple_label": "{count, number} attachments", + "share_extension.server_label": "Server", + "share_extension.servers_screen.title": "Select server", + "share_extension.share_screen.title": "Share to Mattermost", + "share_extension.upload_disabled": "File uploads are disabled for the selected server", + "share_feedback.button.no": "No, thanks", + "share_feedback.button.yes": "Yes", + "share_feedback.subtitle": "We'd love to hear how we can make your experience better.", + "share_feedback.title": "Would you share your feedback?", + "skintone_selector.tooltip.description": "You can now choose the skin tone you prefer to use for your emojis.", + "skintone_selector.tooltip.title": "Choose your default skin tone", + "smobile.search.recent_title": "Recent searches in {teamName}", + "snack.bar.favorited.channel": "This channel was favorited", + "snack.bar.link.copied": "Link copied to clipboard", + "snack.bar.message.copied": "Text copied to clipboard", + "snack.bar.mute.channel": "This channel was muted", + "snack.bar.remove.user": "1 member was removed from the channel", + "snack.bar.undo": "Undo", + "snack.bar.unfavorite.channel": "This channel was unfavorited", + "snack.bar.unmute.channel": "This channel was unmuted", + "status_dropdown.set_away": "Away", + "status_dropdown.set_dnd": "Do Not Disturb", + "status_dropdown.set_offline": "Offline", + "status_dropdown.set_online": "Online", + "status_dropdown.set_ooo": "Out Of Office", + "suggestion.mention.all": "Notifies everyone in this channel", + "suggestion.mention.channel": "Notifies everyone in this channel", + "suggestion.mention.channels": "My Channels", + "suggestion.mention.groups": "Group Mentions", + "suggestion.mention.here": "Notifies everyone online in this channel", + "suggestion.mention.members": "Channel Members", + "suggestion.mention.morechannels": "Other Channels", + "suggestion.mention.nonmembers": "Not in Channel", + "suggestion.mention.special": "Special Mentions", + "suggestion.mention.you": " (you)", + "suggestion.search.direct": "Direct Messages", + "suggestion.search.private": "Private Channels", + "suggestion.search.public": "Public Channels", + "team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.", + "team_list.no_other_teams.title": "No additional teams to join", + "terms_of_service.acceptButton": "Accept", + "terms_of_service.alert_cancel": "Cancel", + "terms_of_service.alert_retry": "Try Again", + "terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.", + "terms_of_service.decline": "Decline", + "terms_of_service.error.description": "It was not possible to get the Terms of Service from the Server.", + "terms_of_service.error.logout": "Logout", + "terms_of_service.error.retry": "Retry", + "terms_of_service.error.title": "Failed to get the ToS.", + "terms_of_service.terms_declined.ok": "OK", + "terms_of_service.terms_declined.text": "You must accept the terms of service to access this server. Please contact your system administrator for more details. You will now be logged out. Log in again to accept the terms of service.", + "terms_of_service.terms_declined.title": "You must accept the terms of service", + "terms_of_service.title": "Terms of Service", + "thread.header.thread": "Thread", + "thread.header.thread_in": "in {channelName}", + "thread.loadingReplies": "Loading replies...", + "thread.noReplies": "No replies yet", + "thread.options.title": "Thread Actions", + "thread.repliesCount": "{repliesCount, number} {repliesCount, plural, one {reply} other {replies}}", + "threads": "Threads", + "threads.deleted": "Original Message Deleted", + "threads.end_of_list.subtitle": "If you're looking for older conversations, try searching instead", + "threads.end_of_list.title": "That's the end of the list!", + "threads.follow": "Follow", + "threads.following": "Following", + "threads.followMessage": "Follow Message", + "threads.followThread": "Follow Thread", + "threads.newReplies": "{count} new {count, plural, one {reply} other {replies}}", + "threads.replies": "{count} {count, plural, one {reply} other {replies}}", + "threads.unfollowMessage": "Unfollow Message", + "threads.unfollowThread": "Unfollow Thread", + "unreads.empty.paragraph": "Turn off the unread filter to show all your channels.", + "unreads.empty.show_all": "Show all", + "unreads.empty.title": "No more unreads", + "unsupported_server.message": "Your server, {serverDisplayName}, is running an unsupported server version. You may experience compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Please contact your System Administrator to upgrade your Mattermost server.", + "unsupported_server.title": "Unsupported server version", + "user_profile.custom_status": "Custom Status", + "user_status.away": "Away", + "user_status.dnd": "Do Not Disturb", + "user_status.offline": "Offline", + "user_status.online": "Online", + "user_status.title": "Status", + "user.edit_profile.email.auth_service": "Login occurs through {service}. Email cannot be updated. Email address used for notifications is {email}.", + "user.edit_profile.email.web_client": "Email must be updated using a web client or desktop application.", + "user.edit_profile.profile_photo.change_photo": "Change profile photo", + "user.settings.general.email": "Email", + "user.settings.general.field_handled_externally": "Some fields below are handled through your login provider. If you want to change them, you’ll need to do so through your login provider.", + "user.settings.general.firstName": "First Name", + "user.settings.general.lastName": "Last Name", + "user.settings.general.nickname": "Nickname", + "user.settings.general.position": "Position", + "user.settings.general.username": "Username", + "user.settings.notifications.email_threads.description": "Notify me about all replies to threads I'm following", + "user.tutorial.long_press": "Long-press on an item to view a user's profile", + "video.download": "Download video", + "video.download_description": "This video must be downloaded to play it.", + "video.failed_description": "An error occurred while trying to play the video.", + "your.servers": "Your servers" } diff --git a/types/api/users.d.ts b/types/api/users.d.ts index cfa759c67f..3485b50509 100644 --- a/types/api/users.d.ts +++ b/types/api/users.d.ts @@ -98,3 +98,33 @@ type UserCustomStatus = { }; type CustomStatusDuration = '' | 'thirty_minutes' | 'one_hour' | 'four_hours' | 'today' | 'this_week' | 'date_and_time'; + +type SearchUserOptions = { + team_id?: string; + not_in_team?: string; + in_channel_id?: string; + in_group_id?: string; + group_constrained?: boolean; + allow_inactive?: boolean; + without_team?: string; + limit?: string; +}; + +type GetUsersOptions = { + page?: number; + per_page?: number; + in_team?: string; + not_in_team?: string; + in_channel?: string; + not_in_channel?: string; + in_group?: string; + group_constrained?: boolean; + without_team?: boolean; + active?: boolean; + inactive?: boolean; + role?: string; + sort?: string; + roles?: string; + channel_roles?: string; + team_roles?: string; +};