diff options
Diffstat (limited to 'webapp')
88 files changed, 1527 insertions, 406 deletions
diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index 183df2017..bb4721c22 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -37,6 +37,7 @@ "block-scoped-var": 2, "brace-style": [2, "1tbs", { "allowSingleLine": false }], "camelcase": [2, {"properties": "never"}], + "capitalized-comments": 0, "class-methods-use-this": 1, "comma-dangle": [2, "never"], "comma-spacing": [2, {"before": false, "after": true}], @@ -76,9 +77,11 @@ "newline-per-chained-call": 0, "no-alert": 2, "no-array-constructor": 2, + "no-await-in-loop": 2, "no-caller": 2, "no-case-declarations": 2, "no-class-assign": 2, + "no-compare-neg-zero": 2, "no-cond-assign": [2, "except-parens"], "no-confusing-arrow": 2, "no-console": 2, @@ -120,6 +123,7 @@ "no-magic-numbers": [1, { "ignore": [-1, 0, 1, 2], "enforceConst": true, "detectObjects": true } ], "no-mixed-operators": [2, {"allowSamePrecedence": false}], "no-mixed-spaces-and-tabs": 2, + "no-multi-assign": 2, "no-multi-spaces": [2, { "exceptions": { "Property": false } }], "no-multi-str": 0, "no-multiple-empty-lines": [2, {"max": 1}], @@ -181,11 +185,14 @@ "object-shorthand": [2, "always"], "one-var": [2, "never"], "one-var-declaration-per-line": 0, + "operator-assignment": [2, "always"], "operator-linebreak": [2, "after"], "padded-blocks": [2, "never"], "prefer-arrow-callback": 2, "prefer-const": 2, + "prefer-destructuring": 0, "prefer-numeric-literals": 2, + "prefer-promise-reject-errors": 2, "prefer-rest-params": 2, "prefer-spread": 2, "prefer-template": 0, @@ -194,6 +201,7 @@ "radix": 2, "react/display-name": [2, { "ignoreTranspilerName": false }], "react/forbid-component-props": 0, + "react/forbid-elements": [2, { "forbid": ["embed"] }], "react/jsx-boolean-value": [2, "always"], "react/jsx-closing-bracket-location": [2, { "location": "tag-aligned" }], "react/jsx-curly-spacing": [2, "never"], @@ -217,6 +225,7 @@ "react/jsx-uses-react": 2, "react/jsx-uses-vars": 2, "react/jsx-wrap-multilines": 2, + "react/no-array-index-key": 1, "react/no-children-prop": 2, "react/no-danger": 0, "react/no-danger-with-children": 2, @@ -236,11 +245,13 @@ "react/prefer-es6-class": 2, "react/prefer-stateless-function": 0, "react/prop-types": 2, + "react/require-default-props": 0, "react/require-optimization": 1, "react/require-render-return": 2, "react/self-closing-comp": 2, "react/sort-comp": 0, "react/style-prop-object": 2, + "require-await": 2, "require-yield": 2, "rest-spread-spacing": [2, "never"], "semi": [2, "always"], diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx index e3563d79c..acbc943cf 100644 --- a/webapp/actions/channel_actions.jsx +++ b/webapp/actions/channel_actions.jsx @@ -313,6 +313,7 @@ export function joinChannel(channel, success, error) { channel.id, () => { ChannelStore.removeMoreChannel(channel.id); + ChannelStore.storeChannel(channel); if (success) { success(); diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx index cbcddfc7c..0c837621f 100644 --- a/webapp/actions/post_actions.jsx +++ b/webapp/actions/post_actions.jsx @@ -68,6 +68,14 @@ export function handleNewPost(post, msg) { }); } +export function pinPost(channelId, postId) { + AsyncClient.pinPost(channelId, postId); +} + +export function unpinPost(channelId, postId) { + AsyncClient.unpinPost(channelId, postId); +} + export function flagPost(postId) { trackEvent('api', 'api_posts_flagged'); AsyncClient.savePreference(Preferences.CATEGORY_FLAGGED_POST, postId, 'true'); @@ -96,7 +104,8 @@ export function getFlaggedPosts() { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_SEARCH, results: data, - is_flagged_posts: true + is_flagged_posts: true, + is_pinned_posts: false }); loadProfilesForPosts(data.posts); @@ -107,6 +116,31 @@ export function getFlaggedPosts() { ); } +export function getPinnedPosts(channelId) { + Client.getPinnedPosts(channelId, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH_TERM, + term: null, + do_search: false, + is_mention_search: false + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH, + results: data, + is_flagged_posts: false, + is_pinned_posts: true + }); + + loadProfilesForPosts(data.posts); + }, + (err) => { + AsyncClient.dispatchError(err, 'getPinnedPosts'); + } + ); +} + export function loadPosts(channelId = ChannelStore.getCurrentId(), isPost = false) { const postList = PostStore.getAllPosts(channelId); const latestPostTime = PostStore.getLatestPostFromPageTime(channelId); diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index eaffd9ff4..a95049f93 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1802,6 +1802,15 @@ export default class Client { this.trackEvent('api', 'api_posts_get_flagged', {team_id: this.getTeamId()}); } + getPinnedPosts(channelId, success, error) { + request. + get(`${this.getChannelNeededRoute(channelId)}/pinned`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getPinnedPosts', success, error)); + } + getFileInfosForPost(channelId, postId, success, error) { request. get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get_file_infos`). @@ -2187,6 +2196,24 @@ export default class Client { }); } + pinPost(channelId, postId, success, error) { + request. + post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/pin`). + set(this.defaultHeaders). + accept('application/json'). + send(). + end(this.handleResponse.bind(this, 'pinPost', success, error)); + } + + unpinPost(channelId, postId, success, error) { + request. + post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/unpin`). + set(this.defaultHeaders). + accept('application/json'). + send(). + end(this.handleResponse.bind(this, 'unpinPost', success, error)); + } + saveReaction(channelId, reaction, success, error) { request. post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/save`). diff --git a/webapp/components/admin_console/password_settings.jsx b/webapp/components/admin_console/password_settings.jsx index 3707977b8..43ec40904 100644 --- a/webapp/components/admin_console/password_settings.jsx +++ b/webapp/components/admin_console/password_settings.jsx @@ -39,16 +39,16 @@ export default class PasswordSettings extends AdminSettings { if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.PasswordRequirements === 'true') { let sampleErrorMsgId = 'user.settings.security.passwordError'; if (props.config.PasswordSettings.Lowercase) { - sampleErrorMsgId = sampleErrorMsgId + 'Lowercase'; + sampleErrorMsgId += 'Lowercase'; } if (props.config.PasswordSettings.Uppercase) { - sampleErrorMsgId = sampleErrorMsgId + 'Uppercase'; + sampleErrorMsgId += 'Uppercase'; } if (props.config.PasswordSettings.Number) { - sampleErrorMsgId = sampleErrorMsgId + 'Number'; + sampleErrorMsgId += 'Number'; } if (props.config.PasswordSettings.Symbol) { - sampleErrorMsgId = sampleErrorMsgId + 'Symbol'; + sampleErrorMsgId += 'Symbol'; } this.sampleErrorMsg = ( <FormattedMessage @@ -101,16 +101,16 @@ export default class PasswordSettings extends AdminSettings { } let sampleErrorMsgId = 'user.settings.security.passwordError'; if (this.refs.lowercase.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Lowercase'; + sampleErrorMsgId += 'Lowercase'; } if (this.refs.uppercase.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Uppercase'; + sampleErrorMsgId += 'Uppercase'; } if (this.refs.number.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Number'; + sampleErrorMsgId += 'Number'; } if (this.refs.symbol.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Symbol'; + sampleErrorMsgId += 'Symbol'; } return ( <FormattedMessage diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx index 757f85517..1b9e5b37a 100644 --- a/webapp/components/admin_console/reset_password_modal.jsx +++ b/webapp/components/admin_console/reset_password_modal.jsx @@ -61,7 +61,7 @@ class ResetPasswordModal extends React.Component { if (this.state.serverError) { urlClass += ' has-error'; - serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>; + serverError = <div className='has-error'><p className='input__help error'>{this.state.serverError}</p></div>; } let title; diff --git a/webapp/components/audio_video_preview.jsx b/webapp/components/audio_video_preview.jsx index 4956900a9..9a55e4835 100644 --- a/webapp/components/audio_video_preview.jsx +++ b/webapp/components/audio_video_preview.jsx @@ -94,7 +94,6 @@ export default class AudioVideoPreview extends React.Component { <video key={this.props.fileInfo.id} ref='video' - style={{maxHeight: this.props.maxHeight}} data-setup='{}' controls='controls' width={width} @@ -111,6 +110,5 @@ export default class AudioVideoPreview extends React.Component { AudioVideoPreview.propTypes = { fileInfo: React.PropTypes.object.isRequired, - fileUrl: React.PropTypes.string.isRequired, - maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired + fileUrl: React.PropTypes.string.isRequired }; diff --git a/webapp/components/audit_table.jsx b/webapp/components/audit_table.jsx index c70c659d4..34e61dbac 100644 --- a/webapp/components/audit_table.jsx +++ b/webapp/components/audit_table.jsx @@ -229,12 +229,12 @@ class AuditTable extends React.Component { let iContent; if (this.props.showIp) { - iContent = <td className='word-break--all'>{auditInfo.ip}</td>; + iContent = <td className='whitespace--nowrap word-break--all'>{auditInfo.ip}</td>; } let sContent; if (this.props.showSession) { - sContent = <td className='word-break--all'>{auditInfo.sessionId}</td>; + sContent = <td className='whitespace--nowrap word-break--all'>{auditInfo.sessionId}</td>; } const descStyle = {}; @@ -244,7 +244,7 @@ class AuditTable extends React.Component { accessList[i] = ( <tr key={audit.id}> - <td className='word-break--all'>{auditInfo.timestamp}</td> + <td className='whitespace--nowrap word-break--all'>{auditInfo.timestamp}</td> {uContent} <td className='word-break--all' @@ -615,18 +615,21 @@ export function formatAuditInfo(audit, formatMessage) { const auditInfo = {}; auditInfo.timestamp = ( <div> - <FormattedDate - value={date} - day='2-digit' - month='short' - year='numeric' - /> - {' - '} - <FormattedTime - value={date} - hour='2-digit' - minute='2-digit' - /> + <div> + <FormattedDate + value={date} + day='2-digit' + month='short' + year='numeric' + /> + </div> + <div> + <FormattedTime + value={date} + hour='2-digit' + minute='2-digit' + /> + </div> </div> ); auditInfo.userId = audit.user_id; diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 01e1e98cf..c05033b51 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -30,7 +30,7 @@ import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; import {getSiteURL} from 'utils/url.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; -import {getFlaggedPosts} from 'actions/post_actions.jsx'; +import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; @@ -53,6 +53,7 @@ export default class ChannelHeader extends React.Component { this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); this.handleShortcut = this.handleShortcut.bind(this); this.getFlagged = this.getFlagged.bind(this); + this.getPinnedPosts = this.getPinnedPosts.bind(this); this.initWebrtc = this.initWebrtc.bind(this); this.onBusy = this.onBusy.bind(this); this.openDirectMessageModal = this.openDirectMessageModal.bind(this); @@ -158,6 +159,15 @@ export default class ChannelHeader extends React.Component { } } + getPinnedPosts(e) { + e.preventDefault(); + if (SearchStore.isPinnedPosts) { + GlobalActions.toggleSideBarAction(false); + } else { + getPinnedPosts(this.props.channelId); + } + } + getFlagged(e) { e.preventDefault(); if (SearchStore.isFlaggedPosts) { @@ -211,6 +221,7 @@ export default class ChannelHeader extends React.Component { render() { const flagIcon = Constants.FLAG_ICON_SVG; + const pinIcon = Constants.PIN_ICON; if (!this.validState()) { // Use an empty div to make sure the header's height stays constant @@ -230,7 +241,10 @@ export default class ChannelHeader extends React.Component { ); const flaggedTooltip = ( - <Tooltip id='flaggedTooltip'> + <Tooltip + id='flaggedTooltip' + className='text-nowrap' + > <FormattedMessage id='channel_header.flagged' defaultMessage='Flagged Posts' @@ -365,6 +379,7 @@ export default class ChannelHeader extends React.Component { if (isDirect) { dropdownContents.push( <li + id='channelEditHeaderDirect' key='edit_header_direct' role='presentation' > @@ -383,6 +398,7 @@ export default class ChannelHeader extends React.Component { } else if (isGroup) { dropdownContents.push( <li + id='channelEditHeaderGroup' key='edit_header_direct' role='presentation' > @@ -401,6 +417,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li + id='channelnotificationPreferencesGroup' key='notification_preferences' role='presentation' > @@ -423,6 +440,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li + id='channelAddMembersGroup' key='add_members' role='presentation' > @@ -441,6 +459,7 @@ export default class ChannelHeader extends React.Component { } else { dropdownContents.push( <li + id='channelViewInfo' key='view_info' role='presentation' > @@ -459,6 +478,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li + id='channelNotificationPreferences' key='notification_preferences' role='presentation' > @@ -489,6 +509,7 @@ export default class ChannelHeader extends React.Component { if (!ChannelStore.isDefault(channel)) { dropdownContents.push( <li + id='channelAddMembers' key='add_members' role='presentation' > @@ -508,6 +529,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li + id='channelManageMembers' key='manage_members' role='presentation' > @@ -534,6 +556,7 @@ export default class ChannelHeader extends React.Component { const deleteOption = ( <li + id='channelDelete' key='delete_channel' role='presentation' > @@ -556,6 +579,7 @@ export default class ChannelHeader extends React.Component { if (ChannelUtils.showManagementOptions(channel, isAdmin, isSystemAdmin, isChannelAdmin)) { dropdownContents.push( <li + id='channelEditHeader' key='set_channel_header' role='presentation' > @@ -577,6 +601,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li + id='channelEditPurpose' key='set_channel_purpose' role='presentation' > @@ -598,6 +623,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li + id='channelRename' key='rename_channel' role='presentation' > @@ -637,6 +663,7 @@ export default class ChannelHeader extends React.Component { if (!ChannelStore.isDefault(channel) && canLeave) { dropdownContents.push( <li + id='channelLeave' key='leave_channel' role='presentation' > @@ -665,19 +692,27 @@ export default class ChannelHeader extends React.Component { headerText = channel.header; } - const toggleFavoriteTooltip = ( - <Tooltip id='favoriteTooltip'> - {this.state.isFavorite ? + let toggleFavoriteTooltip; + if (this.state.isFavorite) { + toggleFavoriteTooltip = ( + <Tooltip id='favoriteTooltip'> <FormattedMessage id='channelHeader.removeFromFavorites' defaultMessage='Remove from Favorites' - /> : - <FormattedMessage - id='channelHeader.addToFavorites' - defaultMessage='Add to Favorites' - />} - </Tooltip> - ); + /> + </Tooltip> + ); + } else { + toggleFavoriteTooltip = ( + <Tooltip id='favoriteTooltip'> + <FormattedMessage + id='channelHeader.addToFavorites' + defaultMessage='Add to Favorites' + /> + </Tooltip> + ); + } + const toggleFavorite = ( <OverlayTrigger delayShow={Constants.OVERLAY_TIME_DELAY} @@ -685,6 +720,7 @@ export default class ChannelHeader extends React.Component { overlay={toggleFavoriteTooltip} > <a + id='toggleFavorite' href='#' onClick={this.toggleFavorite} className='channel-header__favorites' @@ -729,10 +765,10 @@ export default class ChannelHeader extends React.Component { {toggleFavorite} <div className='dropdown'> <a + id='channelHeaderDropdown' href='#' className='dropdown-toggle theme' type='button' - id='channel_header_dropdown' data-toggle='dropdown' aria-expanded='true' > @@ -762,8 +798,20 @@ export default class ChannelHeader extends React.Component { </OverlayTrigger> </div> </th> - <th className='header-list__members'> + <th className='header-list__right'> {popoverListMembers} + <a + href='#' + type='button' + id='pinnedPostsButton' + className='pinned-posts-button' + onClick={this.getPinnedPosts} + > + <span + dangerouslySetInnerHTML={{__html: pinIcon}} + aria-hidden='true' + /> + </a> </th> <th className='search-bar__container'> <NavbarSearchBox @@ -779,6 +827,7 @@ export default class ChannelHeader extends React.Component { overlay={recentMentionsTooltip} > <a + id='searchMentions' href='#' type='button' onClick={this.searchMentions} @@ -796,6 +845,7 @@ export default class ChannelHeader extends React.Component { overlay={flaggedTooltip} > <a + id='flaggedPostsButton' href='#' type='button' onClick={this.getFlagged} diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx index 7c2718cb9..2ef8fb4af 100644 --- a/webapp/components/channel_invite_button.jsx +++ b/webapp/components/channel_invite_button.jsx @@ -55,6 +55,7 @@ export default class ChannelInviteButton extends React.Component { render() { return ( <SpinnerButton + id='addMembers' className='btn btn-sm btn-primary' onClick={this.handleClick} spinning={this.state.addingUser} diff --git a/webapp/components/channel_members_dropdown.jsx b/webapp/components/channel_members_dropdown.jsx index a7b3259af..5ccdcd4c1 100644 --- a/webapp/components/channel_members_dropdown.jsx +++ b/webapp/components/channel_members_dropdown.jsx @@ -131,6 +131,7 @@ export default class ChannelMembersDropdown extends React.Component { removeFromChannel = ( <li role='presentation'> <a + id='removeFromChannel' role='menuitem' href='#' onClick={this.handleRemoveFromChannel} @@ -149,6 +150,7 @@ export default class ChannelMembersDropdown extends React.Component { makeChannelMember = ( <li role='presentation'> <a + id='makeChannelMember' role='menuitem' href='#' onClick={this.handleMakeChannelMember} @@ -167,6 +169,7 @@ export default class ChannelMembersDropdown extends React.Component { makeChannelAdmin = ( <li role='presentation'> <a + id='makeChannelAdmin' role='menuitem' href='#' onClick={this.handleMakeChannelAdmin} @@ -183,6 +186,7 @@ export default class ChannelMembersDropdown extends React.Component { return ( <div className='dropdown member-drop'> <a + id='channelMemberDropdown' href='#' className='dropdown-toggle theme' type='button' @@ -206,6 +210,7 @@ export default class ChannelMembersDropdown extends React.Component { } else if (this.canRemoveMember()) { return ( <button + id='removeMember' type='button' className='btn btn-danger btn-message' onClick={this.handleRemoveFromChannel} diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx index 96d90e5cc..ec5423fe2 100644 --- a/webapp/components/channel_members_modal.jsx +++ b/webapp/components/channel_members_modal.jsx @@ -41,6 +41,7 @@ export default class ChannelMembersModal extends React.Component { /> </Modal.Title> <a + id='showInviteModal' className='btn btn-md btn-primary' href='#' onClick={() => { @@ -71,4 +72,4 @@ ChannelMembersModal.propTypes = { onModalDismissed: React.PropTypes.func.isRequired, showInviteModal: React.PropTypes.func.isRequired, channel: React.PropTypes.object.isRequired -};
\ No newline at end of file +}; diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx index e78755fe3..2f76f19e1 100644 --- a/webapp/components/channel_notifications_modal.jsx +++ b/webapp/components/channel_notifications_modal.jsx @@ -135,6 +135,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelNotificationGlobalDefault' type='radio' name='desktopNotificationLevel' checked={notifyActive[0]} @@ -153,6 +154,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelNotificationAllActivity' type='radio' name='desktopNotificationLevel' checked={notifyActive[1]} @@ -165,6 +167,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelNotificationMentions' type='radio' name='desktopNotificationLevel' checked={notifyActive[2]} @@ -177,6 +180,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelNotificationNever' type='radio' name='desktopNotificationLevel' checked={notifyActive[3]} @@ -287,6 +291,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelUnreadAll' type='radio' name='markUnreadLevel' checked={this.state.unreadLevel === 'all'} @@ -302,6 +307,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelUnreadMentions' type='radio' name='markUnreadLevel' checked={this.state.unreadLevel === 'mention'} @@ -459,6 +465,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelPushNotificationGlobalDefault' type='radio' name='pushNotificationLevel' checked={notifyActive[0]} @@ -477,6 +484,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelPushNotificationAllActivity' type='radio' name='pushNotificationLevel' checked={notifyActive[1]} @@ -489,6 +497,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelPushNotificationMentions' type='radio' name='pushNotificationLevel' checked={notifyActive[2]} @@ -501,6 +510,7 @@ export default class ChannelNotificationsModal extends React.Component { <div className='radio'> <label> <input + id='channelPushNotificationNever' type='radio' name='pushNotificationLevel' checked={notifyActive[3]} diff --git a/webapp/components/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx index aeb8afbb9..865c0e6db 100644 --- a/webapp/components/create_team/components/display_name.jsx +++ b/webapp/components/create_team/components/display_name.jsx @@ -10,7 +10,6 @@ import logoImage from 'images/logo.png'; import React from 'react'; import ReactDOM from 'react-dom'; -import {Link} from 'react-router/es6'; import {FormattedMessage} from 'react-intl'; export default class TeamSignupDisplayNamePage extends React.Component { @@ -118,14 +117,6 @@ export default class TeamSignupDisplayNamePage extends React.Component { defaultMessage='Next' /><i className='fa fa-chevron-right'/> </button> - <div className='margin--extra'> - <Link to='/select_team'> - <FormattedMessage - id='create_team.display_name.back' - defaultMessage='Back to previous step' - /> - </Link> - </div> </form> </div> ); diff --git a/webapp/components/delete_modal_trigger.jsx b/webapp/components/delete_modal_trigger.jsx new file mode 100644 index 000000000..9ccbf33a2 --- /dev/null +++ b/webapp/components/delete_modal_trigger.jsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import ConfirmModal from './confirm_modal.jsx'; + +export default class DeleteModalTrigger extends React.Component { + constructor(props) { + super(props); + if (this.constructor === DeleteModalTrigger) { + throw new TypeError('Can not construct abstract class.'); + } + this.handleConfirm = this.handleConfirm.bind(this); + this.handleCancel = this.handleCancel.bind(this); + this.handleOpenModal = this.handleOpenModal.bind(this); + + this.state = { + showDeleteModal: false + }; + } + + handleOpenModal(e) { + e.preventDefault(); + + this.setState({ + showDeleteModal: true + }); + } + + handleConfirm(e) { + this.props.onDelete(e); + } + + handleCancel() { + this.setState({ + showDeleteModal: false + }); + } + + render() { + return ( + <span> + <a + href='#' + onClick={this.handleOpenModal} + > + { this.triggerTitle } + </a> + <ConfirmModal + show={this.state.showDeleteModal} + title={this.modalTitle} + message={this.modalMessage} + confirmButton={this.modalConfirmButton} + onConfirm={this.handleConfirm} + onCancel={this.handleCancel} + /> + </span> + ); + } +} + +DeleteModalTrigger.propTypes = { + onDelete: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/emoji/components/delete_emoji_modal.jsx b/webapp/components/emoji/components/delete_emoji_modal.jsx new file mode 100644 index 000000000..604e3a27b --- /dev/null +++ b/webapp/components/emoji/components/delete_emoji_modal.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import DeleteModalTrigger from '../../delete_modal_trigger.jsx'; + +export default class DeleteEmoji extends DeleteModalTrigger { + get triggerTitle() { + return ( + <FormattedMessage + id='emoji_list.delete' + defaultMessage='Delete' + /> + ); + } + + get modalTitle() { + return ( + <FormattedMessage + id='emoji_list.delete.confirm.title' + defaultMessage='Delete Custom Emoji' + /> + ); + } + + get modalMessage() { + return ( + <div className='alert alert-warning'> + <i className='fa fa-warning'/> + <FormattedMessage + id='emoji_list.delete.confirm.msg' + defaultMessage='This action permanently deletes the custom emoji. Are you sure you want to delete it?' + /> + </div> + ); + } + + get modalConfirmButton() { + return ( + <FormattedMessage + id='emoji_list.delete.confirm.button' + defaultMessage='Delete' + /> + ); + } +} + +DeleteEmoji.propTypes = { + onDelete: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/emoji/components/emoji_list_item.jsx b/webapp/components/emoji/components/emoji_list_item.jsx index f2ed82ba7..019b0ca93 100644 --- a/webapp/components/emoji/components/emoji_list_item.jsx +++ b/webapp/components/emoji/components/emoji_list_item.jsx @@ -4,6 +4,7 @@ import React from 'react'; import EmojiStore from 'stores/emoji_store.jsx'; +import DeleteEmoji from './delete_emoji_modal.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -80,15 +81,7 @@ export default class EmojiListItem extends React.Component { let deleteButton = null; if (this.props.onDelete) { deleteButton = ( - <a - href='#' - onClick={this.handleDelete} - > - <FormattedMessage - id='emoji_list.delete' - defaultMessage='Delete' - /> - </a> + <DeleteEmoji onDelete={this.handleDelete}/> ); } diff --git a/webapp/components/integrations/components/confirm_integration.jsx b/webapp/components/integrations/components/confirm_integration.jsx index 6d778f241..b4f299d1c 100644 --- a/webapp/components/integrations/components/confirm_integration.jsx +++ b/webapp/components/integrations/components/confirm_integration.jsx @@ -5,7 +5,7 @@ import React from 'react'; import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; -import {Link} from 'react-router/es6'; +import {Link, browserHistory} from 'react-router/es6'; import UserStore from 'stores/user_store.jsx'; import IntegrationStore from 'stores/integration_store.jsx'; @@ -25,6 +25,7 @@ export default class ConfirmIntegration extends React.Component { super(props); this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); const userId = UserStore.getCurrentId(); @@ -38,10 +39,12 @@ export default class ConfirmIntegration extends React.Component { componentDidMount() { IntegrationStore.addChangeListener(this.handleIntegrationChange); + window.addEventListener('keypress', this.handleKeyPress); } componentWillUnmount() { IntegrationStore.removeChangeListener(this.handleIntegrationChange); + window.removeEventListener('keypress', this.handleKeyPress); } handleIntegrationChange() { @@ -53,6 +56,12 @@ export default class ConfirmIntegration extends React.Component { }); } + handleKeyPress(e) { + if (e.key === 'Enter') { + browserHistory.push('/' + this.props.team.name + '/integrations/' + this.state.type); + } + } + render() { let headerText = null; let helpText = null; diff --git a/webapp/components/integrations/components/delete_integration.jsx b/webapp/components/integrations/components/delete_integration.jsx index 442ac57f7..8e2e27596 100644 --- a/webapp/components/integrations/components/delete_integration.jsx +++ b/webapp/components/integrations/components/delete_integration.jsx @@ -1,48 +1,29 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; -import ConfirmModal from '../../confirm_modal.jsx'; +import DeleteModalTrigger from '../../delete_modal_trigger.jsx'; -export default class DeleteIntegration extends React.Component { - constructor(props) { - super(props); - - this.handleConfirm = this.handleConfirm.bind(this); - this.handleCancel = this.handleCancel.bind(this); - this.handleOpenModal = this.handleOpenModal.bind(this); - - this.state = { - showDeleteModal: false - }; - } - - handleOpenModal(e) { - e.preventDefault(); - - this.setState({ - showDeleteModal: true - }); - } - - handleConfirm() { - this.props.onDelete(); - } - - handleCancel() { - this.setState({ - showDeleteModal: false - }); +export default class DeleteIntegration extends DeleteModalTrigger { + get triggerTitle() { + return ( + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + ); } - render() { - const title = ( + get modalTitle() { + return ( <FormattedMessage id='integrations.delete.confirm.title' defaultMessage='Delete Integration' /> ); + } - const message = ( + get modalMessage() { + return ( <div className='alert alert-warning'> <i className='fa fa-warning'/> <FormattedMessage @@ -51,35 +32,15 @@ export default class DeleteIntegration extends React.Component { /> </div> ); + } - const confirmButton = ( + get modalConfirmButton() { + return ( <FormattedMessage id='integrations.delete.confirm.button' defaultMessage='Delete' /> ); - - return ( - <span> - <a - href='#' - onClick={this.handleOpenModal} - > - <FormattedMessage - id='installed_integrations.delete' - defaultMessage='Delete' - /> - </a> - <ConfirmModal - show={this.state.showDeleteModal} - title={title} - message={message} - confirmButton={confirmButton} - onConfirm={this.handleConfirm} - onCancel={this.handleCancel} - /> - </span> - ); } } diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx index 38c594cf7..1d812147a 100644 --- a/webapp/components/login/login_controller.jsx +++ b/webapp/components/login/login_controller.jsx @@ -89,7 +89,7 @@ export default class LoginController extends React.Component { } // don't trim the password since we support spaces in passwords - loginId = loginId.trim(); + loginId = loginId.trim().toLowerCase(); if (!loginId) { // it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements @@ -413,6 +413,7 @@ export default class LoginController extends React.Component { </div> <div className='form-group'> <button + id='loginButton' type='submit' className='btn btn-primary' > diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index c945a0b9c..28d8fae05 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -19,12 +19,15 @@ import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import SearchStore from 'stores/search_store.jsx'; import ChannelSwitchModal from './channel_switch_modal.jsx'; import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; import * as ChannelActions from 'actions/channel_actions.jsx'; +import * as GlobalActions from 'actions/global_actions.jsx'; +import {getPinnedPosts} from 'actions/post_actions.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -62,6 +65,7 @@ export default class Navbar extends React.Component { this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this); this.openDirectMessageModal = this.openDirectMessageModal.bind(this); + this.getPinnedPosts = this.getPinnedPosts.bind(this); const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; @@ -216,6 +220,15 @@ export default class Navbar extends React.Component { }); } + getPinnedPosts(e) { + e.preventDefault(); + if (SearchStore.isPinnedPosts) { + GlobalActions.toggleSideBarAction(false); + } else { + getPinnedPosts(this.state.channel.id); + } + } + toggleFavorite = (e) => { e.preventDefault(); @@ -244,6 +257,7 @@ export default class Navbar extends React.Component { } let viewInfoOption; + let viewPinnedPostsOption; let addMembersOption; let manageMembersOption; let setChannelHeaderOption; @@ -335,6 +349,21 @@ export default class Navbar extends React.Component { </li> ); + viewPinnedPostsOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.getPinnedPosts} + > + <FormattedMessage + id='navbar.viewPinnedPosts' + defaultMessage='View Pinned Posts' + /> + </a> + </li> + ); + if (!ChannelStore.isDefault(channel)) { addMembersOption = ( <li role='presentation'> @@ -525,10 +554,10 @@ export default class Navbar extends React.Component { id='channelHeader.removeFromFavorites' defaultMessage='Remove from Favorites' /> : - <FormattedMessage - id='channelHeader.addToFavorites' - defaultMessage='Add to Favorites' - />} + <FormattedMessage + id='channelHeader.addToFavorites' + defaultMessage='Add to Favorites' + />} </a> </li> ); @@ -561,6 +590,7 @@ export default class Navbar extends React.Component { role='menu' > {viewInfoOption} + {viewPinnedPostsOption} {notificationPreferenceOption} {addMembersOption} {manageMembersOption} diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index 5cb714ebc..11e75bfb7 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import PostStore from 'stores/post_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx'; import {startPeriodicSync, stopPeriodicSync} from 'actions/websocket_actions.jsx'; @@ -177,12 +178,25 @@ export default class NeedsTeam extends React.Component { </div> ); } + + let channel = ChannelStore.getByName(this.props.params.channel); + if (channel == null) { + // the permalink view is not really tied to a particular channel but still needs it + const postId = PostStore.getFocusedPostId(); + const post = PostStore.getEarliestPostFromPage(postId); + + // the post take some time before being available on page load + if (post != null) { + channel = ChannelStore.get(post.channel_id); + } + } + return ( <div className='channel-view'> <ErrorBar/> <WebrtcNotification/> <div className='container-fluid'> - <SidebarRight/> + <SidebarRight channel={channel}/> <SidebarRightMenu teamType={this.state.team.type}/> <WebrtcSidebar/> {content} diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx index 2f9533b0e..f16b4596f 100644 --- a/webapp/components/new_channel_modal.jsx +++ b/webapp/components/new_channel_modal.jsx @@ -77,7 +77,7 @@ export default class NewChannelModal extends React.Component { e.preventDefault(); const displayName = ReactDOM.findDOMNode(this.refs.display_name).value.trim(); - if (displayName.length < 1) { + if (displayName.length < Constants.MIN_CHANNELNAME_LENGTH) { this.setState({displayNameError: true}); return; } @@ -104,7 +104,7 @@ export default class NewChannelModal extends React.Component { <p className='input__help error'> <FormattedMessage id='channel_modal.displayNameError' - defaultMessage='This field is required' + defaultMessage='Channel name must be 2 or more characters' /> {this.state.displayNameError} </p> @@ -232,7 +232,7 @@ export default class NewChannelModal extends React.Component { ref='display_name' className='form-control' placeholder={Utils.localizeMessage('channel_modal.nameEx', 'E.g.: "Bugs", "Marketing", "客户支持"')} - maxLength='22' + maxLength={Constants.MAX_CHANNELNAME_LENGTH} value={this.props.channelData.displayName} autoFocus={true} tabIndex='1' diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index bd2f744c7..1518b1ebf 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -233,7 +233,7 @@ export default class PopoverListMembers extends React.Component { } return ( - <div> + <div className='member-popover__container'> <div id='member_popover' className='member-popover__trigger' @@ -243,13 +243,11 @@ export default class PopoverListMembers extends React.Component { AsyncClient.getProfilesInChannel(this.props.channel.id, 0); }} > - <div> - {countText} - <span - className='fa fa-user' - aria-hidden='true' - /> - </div> + {countText} + <span + className='fa fa-user' + aria-hidden='true' + /> </div> <Overlay rootClose={true} diff --git a/webapp/components/post_view/components/date_separator.jsx b/webapp/components/post_view/components/date_separator.jsx new file mode 100644 index 000000000..18dc0c7ff --- /dev/null +++ b/webapp/components/post_view/components/date_separator.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import {FormattedDate} from 'react-intl'; + +export default class DateSeparator extends React.Component { + render() { + return ( + <div + className='date-separator' + > + <hr className='separator__hr'/> + <div className='separator__text'> + <FormattedDate + value={this.props.date} + weekday='short' + month='short' + day='2-digit' + year='numeric' + /> + </div> + </div> + ); + } +} + +DateSeparator.propTypes = { + date: React.PropTypes.instanceOf(Date) +}; diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx index 12437e672..da85905c0 100644 --- a/webapp/components/post_view/components/post_attachment_opengraph.jsx +++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx @@ -32,7 +32,6 @@ export default class PostAttachmentOpenGraph extends React.Component { this.onImageLoad = this.onImageLoad.bind(this); this.onImageError = this.onImageError.bind(this); this.truncateText = this.truncateText.bind(this); - this.setImageWidth = this.setImageWidth.bind(this); } IMAGE_LOADED = { @@ -75,20 +74,16 @@ export default class PostAttachmentOpenGraph extends React.Component { componentDidMount() { OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange); - this.setImageWidth(); - window.addEventListener('resize', this.setImageWidth); } componentDidUpdate() { if (this.props.childComponentDidUpdateFunction) { this.props.childComponentDidUpdateFunction(); } - this.setImageWidth(); } componentWillUnmount() { OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange); - window.removeEventListener('resize', this.setImageWidth); } onOpenGraphMetadataChange(url) { @@ -163,9 +158,6 @@ export default class PostAttachmentOpenGraph extends React.Component { return ( <div className='attachment__image__container--openraph' - style={{ - width: (this.imageDimentions.height * this.imageRatio) + this.smallImageContainerLeftPadding - }} // Initially set the width accordinly to max image heigh, ie 80px. Later on it would be modified according to actul height of image. ref={(div) => { this.smallImageContainer = div; }} @@ -215,20 +207,6 @@ export default class PostAttachmentOpenGraph extends React.Component { return element; } - setImageWidth() { - if ( - this.state.imageLoaded === this.IMAGE_LOADED.YES && - this.smallImageContainer && - this.smallImageElement - ) { - this.smallImageContainer.style.width = ( - (this.smallImageElement.offsetHeight * this.imageRatio) + - this.smallImageContainerLeftPadding + - 'px' - ); - } - } - truncateText(text, maxLength = this.textMaxLenght, ellipsis = this.textEllipsis) { if (text.length > maxLength) { return text.substring(0, maxLength - ellipsis.length) + ellipsis; diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index 331fdeb00..5318ec272 100644 --- a/webapp/components/post_view/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -26,6 +26,8 @@ export default class PostInfo extends React.Component { this.removePost = this.removePost.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -148,6 +150,42 @@ export default class PostInfo extends React.Component { ); } + if (this.props.post.is_pinned) { + dropdownContents.push( + <li + key='unpinLink' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='post_info.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='pinLink' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='post_info.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -250,6 +288,16 @@ export default class PostInfo extends React.Component { ); } + pinPost(e) { + e.preventDefault(); + PostActions.pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + PostActions.unpinPost(this.props.post.channel_id, this.props.post.id); + } + flagPost(e) { e.preventDefault(); PostActions.flagPost(this.props.post.id); @@ -374,6 +422,18 @@ export default class PostInfo extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + return ( <ul className='post__header--info'> <li className='col'> @@ -384,6 +444,7 @@ export default class PostInfo extends React.Component { useMilitaryTime={this.props.useMilitaryTime} postId={post.id} /> + {pinnedBadge} {flagTrigger} </li> {options} diff --git a/webapp/components/post_view/components/post_time.jsx b/webapp/components/post_view/components/post_time.jsx index 25d533e0a..77f3f3266 100644 --- a/webapp/components/post_view/components/post_time.jsx +++ b/webapp/components/post_view/components/post_time.jsx @@ -40,12 +40,15 @@ export default class PostTime extends React.Component { } renderTimeTag() { + const date = getDateForUnixTicks(this.props.eventTime); + return ( <time className='post__time' - dateTime={getDateForUnixTicks(this.props.eventTime).toISOString()} + dateTime={date.toISOString()} + title={date} > - {getDateForUnixTicks(this.props.eventTime).toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})} + {date.toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})} </time> ); } diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index cb527d850..52e4d9851 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -10,7 +10,7 @@ import ReactionListContainer from 'components/post_view/components/reaction_list import RhsDropdown from 'components/rhs_dropdown.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -36,6 +36,8 @@ export default class RhsComment extends React.Component { this.removePost = this.removePost.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -128,6 +130,16 @@ export default class RhsComment extends React.Component { unflagPost(this.props.post.id); } + pinPost(e) { + e.preventDefault(); + pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + unpinPost(this.props.post.channel_id, this.props.post.id); + } + createDropdown() { const post = this.props.post; @@ -195,6 +207,42 @@ export default class RhsComment extends React.Component { </li> ); + if (post.is_pinned) { + dropdownContents.push( + <li + key='rhs-comment-unpin' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='rhs_root.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='rhs-comment-pin' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='rhs_root.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -503,10 +551,19 @@ export default class RhsComment extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + const timeOptions = { - day: 'numeric', - month: 'short', - year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime @@ -524,6 +581,7 @@ export default class RhsComment extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post, timeOptions)} + {pinnedBadge} {flagTrigger} </li> {options} diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 0c1037501..83d930bca 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -14,7 +14,7 @@ import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; @@ -35,6 +35,8 @@ export default class RhsRootPost extends React.Component { this.handlePermalink = this.handlePermalink.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -143,6 +145,16 @@ export default class RhsRootPost extends React.Component { ); } + pinPost(e) { + e.preventDefault(); + pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + unpinPost(this.props.post.channel_id, this.props.post.id); + } + render() { const post = this.props.post; const user = this.props.user; @@ -240,6 +252,42 @@ export default class RhsRootPost extends React.Component { </li> ); + if (post.is_pinned) { + dropdownContents.push( + <li + key='rhs-root-unpin' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='rhs_root.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='rhs-root-pin' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='rhs_root.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -450,10 +498,19 @@ export default class RhsRootPost extends React.Component { flagFunc = this.flagPost; } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + const timeOptions = { - day: 'numeric', - month: 'short', - year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime @@ -470,6 +527,7 @@ export default class RhsRootPost extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post, timeOptions)} + {pinnedBadge} <OverlayTrigger key={'rootpostflagtooltipkey' + flagVisible} delayShow={Constants.OVERLAY_TIME_DELAY} diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx index 3c0b2e114..2c1d03901 100644 --- a/webapp/components/rhs_thread.jsx +++ b/webapp/components/rhs_thread.jsx @@ -8,6 +8,7 @@ import RootPost from './rhs_root_post.jsx'; import Comment from './rhs_comment.jsx'; import FileUploadOverlay from './file_upload_overlay.jsx'; import FloatingTimestamp from './post_view/components/floating_timestamp.jsx'; +import DateSeparator from './post_view/components/date_separator.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; @@ -325,6 +326,7 @@ export default class RhsThread extends React.Component { const postsArray = this.state.postsArray; const selected = this.state.selected; const profiles = this.state.profiles || {}; + let previousPostDay = Utils.getDateForUnixTicks(selected.create_at); if (postsArray == null || selected == null) { return ( @@ -355,6 +357,55 @@ export default class RhsThread extends React.Component { rootStatus = this.state.statuses[selected.user_id] || 'offline'; } + const commentsLists = []; + for (let i = 0; i < postsArray.length; i++) { + const comPost = postsArray[i]; + let p; + if (UserStore.getCurrentId() === comPost.user_id) { + p = UserStore.getCurrentUser(); + } else { + p = profiles[comPost.user_id]; + } + + let isFlagged = false; + if (this.state.flaggedPosts) { + isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true'; + } + + let status = 'offline'; + if (this.state.statuses && p && p.id) { + status = this.state.statuses[p.id] || 'offline'; + } + + const keyPrefix = comPost.id ? comPost.id : comPost.pending_post_id; + + const currentPostDay = Utils.getDateForUnixTicks(comPost.create_at); + + if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { + previousPostDay = currentPostDay; + commentsLists.push( + <DateSeparator + date={currentPostDay} + />); + } + + commentsLists.push( + <div key={keyPrefix + 'commentKey'}> + <Comment + ref={comPost.id} + post={comPost} + user={p} + currentUser={this.props.currentUser} + compactDisplay={this.state.compactDisplay} + useMilitaryTime={this.props.useMilitaryTime} + isFlagged={isFlagged} + status={status} + isBusy={this.state.isBusy} + /> + </div> + ); + } + return ( <div className='post-right__container'> <FileUploadOverlay overlayType='right'/> @@ -384,6 +435,9 @@ export default class RhsThread extends React.Component { onScroll={this.handleScroll} > <div className='post-right__scroll'> + <DateSeparator + date={previousPostDay} + /> <RootPost ref={selected.id} post={selected} @@ -401,41 +455,7 @@ export default class RhsThread extends React.Component { ref='rhspostlist' className='post-right-comments-container' > - {postsArray.map((comPost) => { - let p; - if (UserStore.getCurrentId() === comPost.user_id) { - p = UserStore.getCurrentUser(); - } else { - p = profiles[comPost.user_id]; - } - - let isFlagged = false; - if (this.state.flaggedPosts) { - isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true'; - } - - let status = 'offline'; - if (this.state.statuses && p && p.id) { - status = this.state.statuses[p.id] || 'offline'; - } - - const keyPrefix = comPost.id ? comPost.id : comPost.pending_post_id; - - return ( - <Comment - ref={comPost.id} - key={keyPrefix + 'commentKey'} - post={comPost} - user={p} - currentUser={this.props.currentUser} - compactDisplay={this.state.compactDisplay} - useMilitaryTime={this.props.useMilitaryTime} - isFlagged={isFlagged} - status={status} - isBusy={this.state.isBusy} - /> - ); - })} + {commentsLists} </div> <div className='post-create__container'> <CreateComment diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx index 1c9f607e6..b88e67a11 100644 --- a/webapp/components/search_bar.jsx +++ b/webapp/components/search_bar.jsx @@ -216,7 +216,10 @@ export default class SearchBar extends React.Component { ); const flaggedTooltip = ( - <Tooltip id='flaggedTooltip'> + <Tooltip + id='flaggedTooltip' + className='text-nowrap' + > <FormattedMessage id='channel_header.flagged' defaultMessage='Flagged Posts' diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx index 4c0105738..ce72ec3b1 100644 --- a/webapp/components/search_results.jsx +++ b/webapp/components/search_results.jsx @@ -213,6 +213,37 @@ export default class SearchResults extends React.Component { </ul> </div> ); + } else if (this.props.isPinnedPosts && noResults) { + ctls = ( + <div className='sidebar--right__subheader'> + <ul> + <li> + <FormattedHTMLMessage + id='search_results.usagePin1' + defaultMessage='There are no pinned messages yet.' + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usagePin2' + defaultMessage='All members of this channel can pin important or useful messages.' + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usagePin3' + defaultMessage='Pinned messages are visible to all channel members.' + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usagePin4' + defaultMessage={'To pin a message: Go to the message that you want to pin and click [...] > "Pin to channel".'} + /> + </li> + </ul> + </div> + ); } else if (!searchTerm && noResults) { ctls = ( <div className='sidebar--right__subheader'> @@ -289,6 +320,8 @@ export default class SearchResults extends React.Component { toggleSize={this.props.toggleSize} shrink={this.props.shrink} isFlaggedPosts={this.props.isFlaggedPosts} + isPinnedPosts={this.props.isPinnedPosts} + channelDisplayName={this.props.channelDisplayName} /> <div id='search-items-container' @@ -307,5 +340,7 @@ SearchResults.propTypes = { useMilitaryTime: React.PropTypes.bool.isRequired, toggleSize: React.PropTypes.func, shrink: React.PropTypes.func, - isFlaggedPosts: React.PropTypes.bool + isFlaggedPosts: React.PropTypes.bool, + isPinnedPosts: React.PropTypes.bool, + channelDisplayName: React.PropTypes.string.isRequired }; diff --git a/webapp/components/search_results_header.jsx b/webapp/components/search_results_header.jsx index 1f2818e98..288d883ee 100644 --- a/webapp/components/search_results_header.jsx +++ b/webapp/components/search_results_header.jsx @@ -79,6 +79,16 @@ export default class SearchResultsHeader extends React.Component { defaultMessage='Flagged Posts' /> ); + } else if (this.props.isPinnedPosts) { + title = ( + <FormattedMessage + id='search_header.title4' + defaultMessage='Pinned posts in {channelDisplayName}' + values={{ + channelDisplayName: this.props.channelDisplayName + }} + /> + ); } return ( @@ -131,5 +141,7 @@ SearchResultsHeader.propTypes = { isMentionSearch: React.PropTypes.bool, toggleSize: React.PropTypes.func, shrink: React.PropTypes.func, - isFlaggedPosts: React.PropTypes.bool + isFlaggedPosts: React.PropTypes.bool, + isPinnedPosts: React.PropTypes.bool, + channelDisplayName: React.PropTypes.string.isRequired }; diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index b3de3492c..1c7309f51 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -289,6 +289,18 @@ export default class SearchResultsItem extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + return ( <div className='search-item__container'> <div className='date-separator'> @@ -322,6 +334,7 @@ export default class SearchResultsItem extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post)} + {pinnedBadge} {flagContent} </li> {rhsControls} diff --git a/webapp/components/searchable_user_list.jsx b/webapp/components/searchable_user_list.jsx index d25c8a506..ab3f9ee9b 100644 --- a/webapp/components/searchable_user_list.jsx +++ b/webapp/components/searchable_user_list.jsx @@ -19,6 +19,7 @@ export default class SearchableUserList extends React.Component { this.nextPage = this.nextPage.bind(this); this.previousPage = this.previousPage.bind(this); this.doSearch = this.doSearch.bind(this); + this.focusSearchBar = this.focusSearchBar.bind(this); this.nextTimeoutId = 0; @@ -30,15 +31,14 @@ export default class SearchableUserList extends React.Component { } componentDidMount() { - if (this.props.focusOnMount) { - this.refs.filter.focus(); - } + this.focusSearchBar(); } componentDidUpdate(prevProps, prevState) { if (this.state.page !== prevState.page) { $(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0); } + this.focusSearchBar(); } componentWillUnmount() { @@ -57,6 +57,12 @@ export default class SearchableUserList extends React.Component { this.setState({page: this.state.page - 1}); } + focusSearchBar() { + if (this.props.focusOnMount) { + this.refs.filter.focus(); + } + } + doSearch() { const term = this.refs.filter.value; this.props.search(term); diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx index 5b6a5d53a..9f3c4f0cf 100644 --- a/webapp/components/setting_item_max.jsx +++ b/webapp/components/setting_item_max.jsx @@ -31,12 +31,30 @@ export default class SettingItemMax extends React.Component { render() { var clientError = null; if (this.props.client_error) { - clientError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.client_error}</label></div>); + clientError = ( + <div className='form-group'> + <label + id='clientError' + className='col-sm-12 has-error' + > + {this.props.client_error} + </label> + </div> + ); } var serverError = null; if (this.props.server_error) { - serverError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.server_error}</label></div>); + serverError = ( + <div className='form-group'> + <label + id='serverError' + className='col-sm-12 has-error' + > + {this.props.server_error} + </label> + </div> + ); } var extraInfo = null; @@ -48,6 +66,7 @@ export default class SettingItemMax extends React.Component { if (this.props.submit) { submit = ( <input + id='saveSetting' type='submit' className='btn btn-sm btn-primary' href='#' @@ -88,6 +107,7 @@ export default class SettingItemMax extends React.Component { {clientError} {submit} <a + id={this.props.title + 'Cancel'} className='btn btn-sm' href='#' onClick={this.props.updateSection} diff --git a/webapp/components/setting_item_min.jsx b/webapp/components/setting_item_min.jsx index 96d8bf459..4f756c46e 100644 --- a/webapp/components/setting_item_min.jsx +++ b/webapp/components/setting_item_min.jsx @@ -12,6 +12,7 @@ export default class SettingItemMin extends React.Component { editButton = ( <li className='col-xs-12 col-sm-3 section-edit'> <a + id={this.props.title} className='theme' href='#' onClick={this.props.updateSection} @@ -33,7 +34,12 @@ export default class SettingItemMin extends React.Component { > <li className='col-xs-12 col-sm-9 section-title'>{this.props.title}</li> {editButton} - <li className='col-xs-12 section-describe'>{this.props.describe}</li> + <li + id={this.props.title + 'Desc'} + className='col-xs-12 section-describe' + > + {this.props.describe} + </li> </ul> ); } diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx index b74ee8eb7..45ac4096d 100644 --- a/webapp/components/setting_picture.jsx +++ b/webapp/components/setting_picture.jsx @@ -1,8 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; -import ReactDOM from 'react-dom'; import {FormattedMessage} from 'react-intl'; import loadingGif from 'images/load.gif'; @@ -14,17 +12,35 @@ export default class SettingPicture extends React.Component { super(props); this.setPicture = this.setPicture.bind(this); + this.confirmImage = this.confirmImage.bind(this); } setPicture(file) { if (file) { var reader = new FileReader(); - var img = ReactDOM.findDOMNode(this.refs.image); - reader.onload = function load(e) { - $(img).attr('src', e.target.result); - }; + reader.onload = (e) => { + const canvas = this.refs.profileImageCanvas; + const context = canvas.getContext('2d'); + const imageObj = new Image(); + imageObj.onload = () => { + if (imageObj.width > imageObj.height) { + const side = imageObj.height; + const rem = imageObj.width - side; + const startX = parseInt(rem / 2, 10); + context.drawImage(imageObj, startX, 0, side, side, + 0, 0, canvas.width, canvas.height); + } else { + const side = imageObj.width; + const rem = imageObj.height - side; + const startY = parseInt(rem / 2, 10); + context.drawImage(imageObj, 0, startY, side, side, + 0, 0, canvas.width, canvas.height); + } + }; + imageObj.src = e.target.result; + }; reader.readAsDataURL(file); } } @@ -48,10 +64,11 @@ export default class SettingPicture extends React.Component { var img = null; if (this.props.picture) { img = ( - <img - ref='image' - className='profile-img rounded' - src='' + <canvas + ref='profileImageCanvas' + className='profile-img' + width='256px' + height='256px' /> ); } else { @@ -83,7 +100,7 @@ export default class SettingPicture extends React.Component { confirmButton = ( <a className={confirmButtonClass} - onClick={this.props.submit} + onClick={this.confirmImage} > <FormattedMessage id='setting_picture.save' @@ -147,6 +164,16 @@ export default class SettingPicture extends React.Component { </ul> ); } + + confirmImage(e) { + e.persist(); + this.refs.profileImageCanvas.toBlob((blob) => { + blob.lastModifiedDate = new Date(); + blob.name = 'image.jpg'; + this.props.imageCropChange(blob); + this.props.submit(e); + }, 'image/jpeg', 0.95); + } } SettingPicture.propTypes = { @@ -158,5 +185,6 @@ SettingPicture.propTypes = { submitActive: React.PropTypes.bool, submit: React.PropTypes.func, title: React.PropTypes.string, - pictureChange: React.PropTypes.func + pictureChange: React.PropTypes.func, + imageCropChange: React.PropTypes.func }; diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 08d80d363..b9356c5a1 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -636,13 +636,15 @@ export default class Sidebar extends React.Component { this.lastUnreadChannel = null; // create elements for all 4 types of channels - const favoriteItems = this.state.favoriteChannels.map((channel, index, arr) => { - if (channel.type === Constants.DM_CHANNEL) { - return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); - } + const favoriteItems = this.state.favoriteChannels. + sort(Utils.sortTeamsByDisplayName). + map((channel, index, arr) => { + if (channel.type === Constants.DM_CHANNEL) { + return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); + } - return this.createChannelElement(channel); - }); + return this.createChannelElement(channel); + }); const publicChannelItems = this.state.publicChannels.map(this.createChannelElement); diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx index 484ca3298..34c228ac2 100644 --- a/webapp/components/sidebar_header_dropdown.jsx +++ b/webapp/components/sidebar_header_dropdown.jsx @@ -467,6 +467,7 @@ export default class SidebarHeaderDropdown extends React.Component { <Dropdown.Menu> <li> <a + id='accountSettings' href='#' onClick={this.toggleAccountSettingsModal} > @@ -480,6 +481,7 @@ export default class SidebarHeaderDropdown extends React.Component { {teamLink} <li> <a + id='logout' href='#' onClick={() => GlobalActions.emitUserLoggedOutEvent()} > diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx index fb120337a..42b7381f4 100644 --- a/webapp/components/sidebar_right.jsx +++ b/webapp/components/sidebar_right.jsx @@ -11,13 +11,13 @@ import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import WebrtcStore from 'stores/webrtc_store.jsx'; -import {getFlaggedPosts} from 'actions/post_actions.jsx'; +import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx'; import {trackEvent} from 'actions/diagnostics_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; -import React from 'react'; +import React, {PropTypes} from 'react'; export default class SidebarRight extends React.Component { constructor(props) { @@ -27,6 +27,7 @@ export default class SidebarRight extends React.Component { this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onSelectedChange = this.onSelectedChange.bind(this); + this.onPostPinnedChange = this.onPostPinnedChange.bind(this); this.onSearchChange = this.onSearchChange.bind(this); this.onUserChange = this.onUserChange.bind(this); this.onShowSearch = this.onShowSearch.bind(this); @@ -39,6 +40,7 @@ export default class SidebarRight extends React.Component { searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch(), isFlaggedPosts: SearchStore.getIsFlaggedPosts(), + isPinnedPosts: SearchStore.getIsPinnedPosts(), postRightVisible: Boolean(PostStore.getSelectedPost()), expanded: false, fromSearch: false, @@ -50,6 +52,7 @@ export default class SidebarRight extends React.Component { componentDidMount() { SearchStore.addSearchChangeListener(this.onSearchChange); PostStore.addSelectedPostChangeListener(this.onSelectedChange); + PostStore.addPostPinnedChangeListener(this.onPostPinnedChange); SearchStore.addShowSearchListener(this.onShowSearch); UserStore.addChangeListener(this.onUserChange); PreferenceStore.addChangeListener(this.onPreferenceChange); @@ -59,6 +62,7 @@ export default class SidebarRight extends React.Component { componentWillUnmount() { SearchStore.removeSearchChangeListener(this.onSearchChange); PostStore.removeSelectedPostChangeListener(this.onSelectedChange); + PostStore.removePostPinnedChangeListener(this.onPostPinnedChange); SearchStore.removeShowSearchListener(this.onShowSearch); UserStore.removeChangeListener(this.onUserChange); PreferenceStore.removeChangeListener(this.onPreferenceChange); @@ -137,6 +141,12 @@ export default class SidebarRight extends React.Component { }); } + onPostPinnedChange() { + if (this.props.channel && this.state.isPinnedPosts) { + getPinnedPosts(this.props.channel.id); + } + } + onShrink() { this.setState({ expanded: false @@ -147,7 +157,8 @@ export default class SidebarRight extends React.Component { this.setState({ searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch(), - isFlaggedPosts: SearchStore.getIsFlaggedPosts() + isFlaggedPosts: SearchStore.getIsFlaggedPosts(), + isPinnedPosts: SearchStore.getIsPinnedPosts() }); } @@ -182,9 +193,11 @@ export default class SidebarRight extends React.Component { <SearchResults isMentionSearch={this.state.isMentionSearch} isFlaggedPosts={this.state.isFlaggedPosts} + isPinnedPosts={this.state.isPinnedPosts} useMilitaryTime={this.state.useMilitaryTime} toggleSize={this.toggleSize} shrink={this.onShrink} + channelDisplayName={this.props.channel ? this.props.channel.display_name : ''} /> ); } else if (this.state.postRightVisible) { @@ -222,3 +235,7 @@ export default class SidebarRight extends React.Component { ); } } + +SidebarRight.propTypes = { + channel: PropTypes.object +}; diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx index 0100cad64..288faae6c 100644 --- a/webapp/components/team_general_tab.jsx +++ b/webapp/components/team_general_tab.jsx @@ -106,14 +106,11 @@ class GeneralTab extends React.Component { let valid = true; const name = this.state.name.trim(); - if (!name) { + if (name) { + state.clientError = ''; + } else { state.clientError = Utils.localizeMessage('general_tab.required', 'This field is required'); valid = false; - } else if (name === this.props.team.display_name) { - state.clientError = Utils.localizeMessage('general_tab.chooseName', 'Please choose a new name for your team'); - valid = false; - } else { - state.clientError = ''; } this.setState(state); @@ -278,6 +275,7 @@ class GeneralTab extends React.Component { <div className='radio'> <label> <input + id='teamOpenInvite' name='userOpenInviteOptions' type='radio' defaultChecked={this.state.allow_open_invite} @@ -293,6 +291,7 @@ class GeneralTab extends React.Component { <div className='radio'> <label> <input + id='teamOpenInviteNo' name='userOpenInviteOptions' type='radio' defaultChecked={!this.state.allow_open_invite} @@ -352,6 +351,7 @@ class GeneralTab extends React.Component { <label className='col-sm-5 control-label visible-xs-block'/> <div className='col-sm-12'> <input + id='teamInviteId' className='form-control' type='text' onChange={this.updateInviteId} @@ -360,6 +360,7 @@ class GeneralTab extends React.Component { /> <div className='padding-top x2'> <a + id='teamInviteIdRegenerate' href='#' onClick={this.handleGenerateInviteId} > @@ -433,6 +434,7 @@ class GeneralTab extends React.Component { <label className='col-sm-5 control-label'>{teamNameLabel}</label> <div className='col-sm-7'> <input + id='teamName' className='form-control' type='text' maxLength={Constants.MAX_TEAMNAME_LENGTH.toString()} @@ -491,6 +493,7 @@ class GeneralTab extends React.Component { <label className='col-sm-5 control-label'>{teamDescriptionLabel}</label> <div className='col-sm-7'> <input + id='teamDescription' className='form-control' type='text' maxLength={Constants.MAX_TEAMDESCRIPTION_LENGTH.toString()} @@ -540,6 +543,7 @@ class GeneralTab extends React.Component { <div> <div className='modal-header'> <button + id='closeButton' type='button' className='close' data-dismiss='modal' diff --git a/webapp/components/user_settings/desktop_notification_settings.jsx b/webapp/components/user_settings/desktop_notification_settings.jsx index 3a330b623..be403ebb6 100644 --- a/webapp/components/user_settings/desktop_notification_settings.jsx +++ b/webapp/components/user_settings/desktop_notification_settings.jsx @@ -74,6 +74,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundOn' type='radio' name='notificationSounds' checked={soundRadio[0]} @@ -89,6 +90,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundOff' type='radio' name='notificationSounds' checked={soundRadio[1]} @@ -136,6 +138,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDuration3' type='radio' name='desktopDuration' checked={durationRadio[0]} @@ -154,6 +157,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDuration5' type='radio' name='desktopDuration' checked={durationRadio[1]} @@ -172,6 +176,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDuration10' type='radio' name='desktopDuration' checked={durationRadio[2]} @@ -189,6 +194,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDurationUnlimited' type='radio' name='desktopDuration' checked={durationRadio[3]} @@ -225,6 +231,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='desktopNotificationAllActivity' type='radio' name='desktopNotificationLevel' checked={activityRadio[0]} @@ -240,6 +247,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='desktopNotificationMentions' type='radio' name='desktopNotificationLevel' checked={activityRadio[1]} @@ -255,6 +263,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='desktopNotificationNever' type='radio' name='desktopNotificationLevel' checked={activityRadio[2]} diff --git a/webapp/components/user_settings/email_notification_setting.jsx b/webapp/components/user_settings/email_notification_setting.jsx index 457512507..1e6c5d7f5 100644 --- a/webapp/components/user_settings/email_notification_setting.jsx +++ b/webapp/components/user_settings/email_notification_setting.jsx @@ -113,6 +113,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationMinutes' type='radio' name='emailNotifications' checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_FIFTEEN_MINUTES} @@ -128,6 +129,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationHour' type='radio' name='emailNotifications' checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_HOUR} @@ -164,6 +166,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationImmediately' type='radio' name='emailNotifications' checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_IMMEDIATE} @@ -179,6 +182,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationNever' type='radio' name='emailNotifications' checked={!this.props.enableEmail} diff --git a/webapp/components/user_settings/import_theme_modal.jsx b/webapp/components/user_settings/import_theme_modal.jsx index 7ae057cc4..9d3cff53d 100644 --- a/webapp/components/user_settings/import_theme_modal.jsx +++ b/webapp/components/user_settings/import_theme_modal.jsx @@ -173,6 +173,7 @@ export default class ImportThemeModal extends React.Component { <div className='form-group less'> <div className='col-sm-12'> <input + id='themeVector' type='text' className='form-control' value={this.state.value} @@ -186,6 +187,7 @@ export default class ImportThemeModal extends React.Component { </Modal.Body> <Modal.Footer> <button + id='cancelButton' type='button' className='btn btn-default' onClick={() => this.setState({show: false})} @@ -196,6 +198,7 @@ export default class ImportThemeModal extends React.Component { /> </button> <button + id='submitButton' onClick={this.handleSubmit} type='submit' className='btn btn-primary' diff --git a/webapp/components/user_settings/manage_languages.jsx b/webapp/components/user_settings/manage_languages.jsx index 4f5eb223d..2955c8030 100644 --- a/webapp/components/user_settings/manage_languages.jsx +++ b/webapp/components/user_settings/manage_languages.jsx @@ -96,6 +96,7 @@ export default class ManageLanguage extends React.Component { </label> <div className='padding-top'> <select + id='displayLanguage' ref='language' className='form-control' value={this.state.locale} diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx index 3459af8b3..970856acc 100644 --- a/webapp/components/user_settings/user_settings_advanced.jsx +++ b/webapp/components/user_settings/user_settings_advanced.jsx @@ -185,6 +185,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='postFormattingOn' type='radio' name='formatting' checked={this.state.settings.formatting !== 'false'} @@ -200,6 +201,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='postFormattingOff' type='radio' name='formatting' checked={this.state.settings.formatting === 'false'} @@ -261,6 +263,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='joinLeaveOn' type='radio' name='join_leave' checked={this.state.settings.join_leave !== 'false'} @@ -276,6 +279,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='joinLeaveOff' type='radio' name='join_leave' checked={this.state.settings.join_leave === 'false'} @@ -367,6 +371,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='ctrlSendOn' type='radio' name='sendOnCtrlEnter' checked={ctrlSendActive[0]} @@ -382,6 +387,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='ctrlSendOff' type='radio' name='sendOnCtrlEnter' checked={ctrlSendActive[1]} @@ -464,6 +470,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div className='checkbox'> <label> <input + id={'advancedPreviewFeatures' + feature.label} type='checkbox' checked={this.state.settings[Constants.FeatureTogglePrefix + feature.label] === 'true'} onChange={(e) => { @@ -524,6 +531,7 @@ export default class AdvancedSettingsDisplay extends React.Component { <div> <div className='modal-header'> <button + id='closeButton' type='button' className='close' data-dismiss='modal' diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx index f51128b6f..60f322467 100644 --- a/webapp/components/user_settings/user_settings_display.jsx +++ b/webapp/components/user_settings/user_settings_display.jsx @@ -160,6 +160,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='collapseFormat' type='radio' name='collapseFormat' checked={collapseFormat[0]} @@ -175,6 +176,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='collapseFormatOff' type='radio' name='collapseFormat' checked={collapseFormat[1]} @@ -277,6 +279,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='clockFormat12h' type='radio' name='clockFormat' checked={clockFormat[0]} @@ -292,6 +295,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='clockFormat24h' type='radio' name='clockFormat' checked={clockFormat[1]} @@ -397,6 +401,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='nameFormatUsername' type='radio' name='nameFormat' checked={nameFormat[1]} @@ -409,6 +414,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='nameFormatNickname' type='radio' name='nameFormat' checked={nameFormat[0]} @@ -421,6 +427,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='nameFormatFullName' type='radio' name='nameFormat' checked={nameFormat[2]} @@ -511,6 +518,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='messageFormatStandard' type='radio' name='messageDisplay' checked={messageDisplay[0]} @@ -533,6 +541,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='messageFormatCompact' type='radio' name='messageDisplay' checked={messageDisplay[1]} @@ -626,6 +635,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='channelDisplayFormatFullScreen' type='radio' name='channelDisplayMode' checked={channelDisplayMode[0]} @@ -641,6 +651,7 @@ export default class UserSettingsDisplay extends React.Component { <div className='radio'> <label> <input + id='channelDisplayFormatCentered' type='radio' name='channelDisplayMode' checked={channelDisplayMode[1]} @@ -735,6 +746,7 @@ export default class UserSettingsDisplay extends React.Component { className='dropdown' > <select + id='displayFontSelect' className='form-control' type='text' value={this.state.selectedFont} @@ -830,6 +842,7 @@ export default class UserSettingsDisplay extends React.Component { <div> <div className='modal-header'> <button + id='closeButton' type='button' className='close' data-dismiss='modal' diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx index f9c624aa0..d558958f0 100644 --- a/webapp/components/user_settings/user_settings_general.jsx +++ b/webapp/components/user_settings/user_settings_general.jsx @@ -101,6 +101,7 @@ class UserSettingsGeneralTab extends React.Component { this.updatePicture = this.updatePicture.bind(this); this.updateSection = this.updateSection.bind(this); this.updatePosition = this.updatePosition.bind(this); + this.updatedCroppedPicture = this.updatedCroppedPicture.bind(this); this.state = this.setupInitialState(props); } @@ -311,6 +312,17 @@ class UserSettingsGeneralTab extends React.Component { this.setState({confirmEmail: e.target.value}); } + updatedCroppedPicture(file) { + if (file) { + this.setState({picture: file}); + + this.submitActive = true; + this.setState({clientError: null}); + } else { + this.setState({picture: null}); + } + } + updatePicture(e) { if (e.target.files && e.target.files[0]) { this.setState({picture: e.target.files[0]}); @@ -410,6 +422,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='primaryEmail' className='form-control' type='email' onChange={this.updateEmail} @@ -431,6 +444,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='confirmEmail' className='form-control' type='email' onChange={this.updateConfirmEmail} @@ -684,6 +698,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='firstName' className='form-control' type='text' onChange={this.updateFirstName} @@ -706,6 +721,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='lastName' className='form-control' type='text' onChange={this.updateLastName} @@ -832,6 +848,7 @@ class UserSettingsGeneralTab extends React.Component { <label className='col-sm-5 control-label'>{nicknameLabel}</label> <div className='col-sm-7'> <input + id='nickname' className='form-control' type='text' onChange={this.updateNickname} @@ -916,6 +933,7 @@ class UserSettingsGeneralTab extends React.Component { <label className='col-sm-5 control-label'>{usernameLabel}</label> <div className='col-sm-7'> <input + id='username' maxLength={Constants.MAX_USERNAME_LENGTH} className='form-control' type='text' @@ -1006,6 +1024,7 @@ class UserSettingsGeneralTab extends React.Component { <label className='col-sm-5 control-label'>{positionLabel}</label> <div className='col-sm-7'> <input + id='position' className='form-control' type='text' onChange={this.updatePosition} @@ -1086,6 +1105,7 @@ class UserSettingsGeneralTab extends React.Component { pictureChange={this.updatePicture} submitActive={this.submitActive} loadingPicture={this.state.loadingPicture} + imageCropChange={this.updatedCroppedPicture} /> ); } else { @@ -1123,6 +1143,7 @@ class UserSettingsGeneralTab extends React.Component { <div> <div className='modal-header'> <button + id='closeUserSettings' type='button' className='close' data-dismiss='modal' diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx index 7c82488f6..ebd43e5af 100644 --- a/webapp/components/user_settings/user_settings_notifications.jsx +++ b/webapp/components/user_settings/user_settings_notifications.jsx @@ -64,6 +64,9 @@ function getNotificationsStateFromStores() { } else { usernameKey = true; keys.splice(keys.indexOf(user.username), 1); + if (keys.indexOf(`@${user.username}`) !== -1) { + keys.splice(keys.indexOf(`@${user.username}`), 1); + } } customKeys = keys.join(','); @@ -281,6 +284,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationOnline' type='radio' name='pushNotificationStatus' checked={pushStatusRadio[0]} @@ -296,6 +300,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationAway' type='radio' name='pushNotificationStatus' checked={pushStatusRadio[1]} @@ -311,6 +316,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationOffline' type='radio' name='pushNotificationStatus' checked={pushStatusRadio[2]} @@ -347,6 +353,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationAllActivity' type='radio' name='pushNotificationLevel' checked={pushActivityRadio[0]} @@ -362,6 +369,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationMentions' type='radio' name='pushNotificationLevel' checked={pushActivityRadio[1]} @@ -377,6 +385,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationNever' type='radio' name='pushNotificationLevel' checked={pushActivityRadio[2]} @@ -520,6 +529,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerFirst' type='checkbox' checked={this.state.firstNameKey} onChange={handleUpdateFirstNameKey} @@ -545,6 +555,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerUsername' type='checkbox' checked={this.state.usernameKey} onChange={handleUpdateUsernameKey} @@ -569,6 +580,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerShouts' type='checkbox' checked={this.state.channelKey} onChange={handleUpdateChannelKey} @@ -587,6 +599,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerCustom' ref='customcheck' type='checkbox' checked={this.state.customKeysChecked} @@ -599,6 +612,7 @@ export default class NotificationsTab extends React.Component { </label> </div> <input + id='notificationTriggerCustomText' ref='custommentions' className='form-control mentions-input' type='text' @@ -697,6 +711,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='notificationCommentsAny' type='radio' name='commentsNotificationLevel' checked={commentsActive[0]} @@ -712,6 +727,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='notificationCommentsRoot' type='radio' name='commentsNotificationLevel' checked={commentsActive[1]} @@ -727,6 +743,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='notificationCommentsNever' type='radio' name='commentsNotificationLevel' checked={commentsActive[2]} @@ -804,6 +821,7 @@ export default class NotificationsTab extends React.Component { <div> <div className='modal-header'> <button + id='closeButton' type='button' className='close' data-dismiss='modal' diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index b6ee2d915..9ca7f4b62 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -331,6 +331,7 @@ export default class SecurityTab extends React.Component { </label> <div className='col-sm-7'> <input + id='currentPassword' className='form-control' type='password' onChange={this.updateCurrentPassword} @@ -352,6 +353,7 @@ export default class SecurityTab extends React.Component { </label> <div className='col-sm-7'> <input + id='newPassword' className='form-control' type='password' onChange={this.updateNewPassword} @@ -373,6 +375,7 @@ export default class SecurityTab extends React.Component { </label> <div className='col-sm-7'> <input + id='confirmPassword' className='form-control' type='password' onChange={this.updateConfirmPassword} diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx index 5a286a396..35df0bd13 100644 --- a/webapp/components/user_settings/user_settings_theme.jsx +++ b/webapp/components/user_settings/user_settings_theme.jsx @@ -218,6 +218,7 @@ export default class ThemeSetting extends React.Component { > <label> <input + id='standardThemes' type='radio' name='theme' checked={!displayCustom} @@ -241,6 +242,7 @@ export default class ThemeSetting extends React.Component { > <label> <input + id='customThemes' type='radio' name='theme' checked={displayCustom} @@ -260,6 +262,7 @@ export default class ThemeSetting extends React.Component { <div key='otherThemes'> <br/> <a + id='otherThemes' href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-theme-examples' target='_blank' rel='noopener noreferrer' @@ -278,6 +281,7 @@ export default class ThemeSetting extends React.Component { className='padding-top' > <a + id='slackImportTheme' className='theme' onClick={this.handleImportModal} > @@ -295,6 +299,7 @@ export default class ThemeSetting extends React.Component { <div className='checkbox user-settings__submit-checkbox'> <label> <input + id='applyThemeToAllTeams' type='checkbox' checked={this.state.applyToAllTeams} onChange={(e) => this.setState({applyToAllTeams: e.target.checked})} diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx index 385138d54..e5c3caa0a 100644 --- a/webapp/components/view_image.jsx +++ b/webapp/components/view_image.jsx @@ -185,7 +185,6 @@ export default class ViewImageModal extends React.Component { <ImagePreview fileInfo={fileInfo} fileUrl={fileUrl} - maxHeight={this.state.imgHeight} /> ); } else if (fileType === 'video' || fileType === 'audio') { @@ -193,7 +192,6 @@ export default class ViewImageModal extends React.Component { <AudioVideoPreview fileInfo={fileInfo} fileUrl={fileUrl} - maxHeight={this.state.imgHeight} /> ); } else if (PDFPreview.supports(fileInfo)) { @@ -344,7 +342,7 @@ LoadingImagePreview.propTypes = { loading: React.PropTypes.string }; -function ImagePreview({fileInfo, fileUrl, maxHeight}) { +function ImagePreview({fileInfo, fileUrl}) { let previewUrl; if (fileInfo.has_preview_image) { previewUrl = FileStore.getFilePreviewUrl(fileInfo.id); @@ -359,16 +357,12 @@ function ImagePreview({fileInfo, fileUrl, maxHeight}) { rel='noopener noreferrer' download={true} > - <img - style={{maxHeight}} - src={previewUrl} - /> + <img src={previewUrl}/> </a> ); } ImagePreview.propTypes = { fileInfo: React.PropTypes.object.isRequired, - fileUrl: React.PropTypes.string.isRequired, - maxHeight: React.PropTypes.number.isRequired + fileUrl: React.PropTypes.string.isRequired }; diff --git a/webapp/i18n/de.json b/webapp/i18n/de.json index a1ef10c11..c8d38639b 100644 --- a/webapp/i18n/de.json +++ b/webapp/i18n/de.json @@ -218,10 +218,10 @@ "admin.customization.enableCustomEmojiDesc": "Erlaube es Benutzern eigene Emojis für Nachrichten zu erstellen. Wenn aktiviert, können eigene Emojis durch wechseln zu einem Team, Klicken auf die drei Punkte über der Kanal-Seitenleiste und dem Auswählen von \"Eigene Emoji\" aufgerufen werden.", "admin.customization.enableCustomEmojiTitle": "Eigene Emoji ermöglichen:", "admin.customization.enableLinkPreviewsDesc": "Erlaube Benutzern eine Vorschau einer Webseite unterhalb der Nachricht anzuzeigen, sofern verfügbar. Wenn wahr, können Webseitenvorschauen über Kontoeinstellungrn > Erweitert > Vorschau auf Features der neuen Version aktiviert werden.", - "admin.customization.enableLinkPreviewsTitle": "Erlaube Link Vorschauen:", + "admin.customization.enableLinkPreviewsTitle": "Erlaube Link-Vorschauen:", "admin.customization.iosAppDownloadLinkDesc": "Einen Link zum Download der iOS App hinzufügen. Benutzer die die Seite über einen mobilen Browser aufrufen werden eine Seite mit der Option die App herunterzuladen angezeigt. Dieses Feld leer lassen um zu verhindern dass die Seite angezeigt wird.", "admin.customization.iosAppDownloadLinkTitle": "iOS App Download Link:", - "admin.customization.linkPreviews": "Link Vorschauen", + "admin.customization.linkPreviews": "Link-Vorschauen", "admin.customization.nativeAppLinks": "Links zu Mattermost Anwendungen", "admin.customization.restrictCustomEmojiCreationAdmin": "Erlaube System- und Teamadministratoren eigene Emojis zu erstellen", "admin.customization.restrictCustomEmojiCreationAll": "Erlaube jedem eigene Emoji zu erstellen", @@ -767,7 +767,7 @@ "admin.sidebar.ldap": "AD/LDAP", "admin.sidebar.legalAndSupport": "Rechtsabteilung und Support", "admin.sidebar.license": "Edition und Lizenz", - "admin.sidebar.linkPreviews": "Link Vorschauen", + "admin.sidebar.linkPreviews": "Link-Vorschauen", "admin.sidebar.localization": "Lokalisation", "admin.sidebar.logging": "Protokollierung", "admin.sidebar.login": "Anmelden", @@ -1118,7 +1118,7 @@ "channel_modal.channel": "Kanal", "channel_modal.createNew": "Erstelle neue(n) ", "channel_modal.descriptionHelp": "Beschreiben Sie wie diese(r) {term} genutzt werden soll.", - "channel_modal.displayNameError": "Dieses Feld wird erforderlich", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "Bearbeiten", "channel_modal.group": "Gruppe", "channel_modal.header": "Überschrift", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>Nachrichten senden</h4><p>Geben Sie hier Ihre Nachricht ein und drücken Sie <strong>Enter</strong> um sie senden.</p><p>Klicken Sie den <strong>Anhang</strong> Button um ein Bild oder eine Datei hochzuladen.</p>", "create_post.write": "Schreiben Sie eine Nachricht...", "create_team.agreement": "Wenn Sie die Erstellung Ihres Accounts fortsetzen und {siteName} nutzen, verstehen Sie sich mit unseren <a href={TermsOfServiceLink}>Nutzungsbedingungen</a> und <a href={PrivacyPolicyLink}>Datenschutzbedingungen</a> einverstanden. Wenn Sie nicht zustimmen, dürfen Sie {siteName} nicht nutzen.", - "create_team.display_name.back": "Zurück zum vorherigem Schritt", "create_team.display_name.charLength": "Name muss zwischen {min} und {max} Zeichen lang sein. Sie können eine längere Teambeschreibung später hinzufügen.", "create_team.display_name.nameHelp": "Benennen Sie Ihr Team in jeglicher Sprache. Ihr Teamname wird in Menüs und Überschriften angezeigt.", "create_team.display_name.next": "Weiter", @@ -1276,6 +1275,9 @@ "emoji_list.add": "Eigenes Emoji hinzufügen", "emoji_list.creator": "Ersteller", "emoji_list.delete": "Löschen", + "emoji_list.delete.confirm.button": "Löschen", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "Kein eigenes Emoji gefunden", "emoji_list.header": "Benutzerdefinierte Emoji", "emoji_list.help": "Benutzerdefinierte Emojis sind für jeden auf Ihrem Server verfügbar. Die Eingabe von ':' im Nachrichtenfeld öffnet das Emoji Auswahlfenster. Andere Nutzer müssen eventuell die Seite neu laden bevor neue Emojis angezeigt werden.", @@ -1290,7 +1292,7 @@ "error.not_supported.message": "Privates Browsing wird nicht unterstützt", "error.not_supported.title": "Browser nicht unterstützt", "error_bar.expired": "Enterprise Lizenz ist abgelaufen und einige Funktionen wurden evtl. deaktiviert. <a href='{link}' target='_blank'>Bitte erneuern.</a>", - "error_bar.expiring": "Enterprise Lizenz läuft ab am {Date}. <a href='{link}' target='_blank'>Bitte erneuern.</a>", + "error_bar.expiring": "Enterprise Lizenz läuft ab am {date}. <a href='{link}' target='_blank'>Bitte erneuern.</a>", "error_bar.past_grace": "Enterprise Lizenz ist abgelaufen und einige Funktionen wurden evtl. deaktiviert. Bitte wenden Sie sich an den Systemadministrator für mehr Details.", "error_bar.preview_mode": "Vorführungsmodus: E-Mail Benachrichtigungen wurden nicht konfiguriert", "file_attachment.download": "Download", @@ -1324,7 +1326,6 @@ "flag_post.flag": "zur Nachverfolgung markieren", "flag_post.unflag": "Demarkieren", "general_tab.chooseDescription": "Bitte wählen Sie eine neue Beschreibung für Ihr Team", - "general_tab.chooseName": "Bitte wählen Sie einen neuen Namen für Ihr Team", "general_tab.codeDesc": "Auf 'Bearbeiten' klicken um den Einladungscode neu zu generieren.", "general_tab.codeLongDesc": "Der Einladungscode ist Teil der URL im Team Einladungslink, welcher über {getTeamInviteLink} im Hauptmenü generiert wird. Eine Neugenerierung erstellt einen neuen Team Einladungslink und macht den vorherigen Link ungültig.", "general_tab.codeTitle": "Einladungscode", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "Seitenleiste umschalten", "navbar.toggle2": "Seitenleiste umschalten", "navbar.viewInfo": "Info anzeigen", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "Über Mattermost", "navbar_dropdown.accountSettings": "Kontoeinstellungen", "navbar_dropdown.console": "System Konsole", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "Markieren", "post_info.mobile.unflag": "Demarkieren", "post_info.permalink": "Dauerhafter Link", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "Antworten", "post_info.system": "System", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(bearbeitet)", "posts_view.loadMore": "Weitere Nachrichten laden", "posts_view.newMsg": "Neue Nachrichten", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "Markieren", "rhs_root.mobile.unflag": "Demarkieren", "rhs_root.permalink": "Dauerhafter Link", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "Suche", "search_bar.usage": "<h4>Suchoptionen</h4><ul><li><span>Verwenden Sie </span><b>\"Anführungszeichen\"</b><span> zur Suche nach Phrasen</span></li><li><span>Verwenden Sie </span><b>from:</b><span> um nach Nachrichten eines bestimmten Absenders zu suchen und </span><b>in:</b><span> für Nachrichten in einem bestimmten Kanal</span></li></ul>", "search_header.results": "Suchergebnisse", "search_header.title2": "Letzte Erwähnungen", "search_header.title3": "Markierte Nachrichten", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "Direktnachricht (mit {username})", "search_item.jump": "Anzeigen", "search_results.because": "<ul><li>Wenn Sie nach einem Textteil suchen(z.b. suche nach \"rea\", wenn Sie \"Realität\" oder \"Reaktion\" finden möchten), hängen Sie ein * an Ihren Suchbegriff</li><li>Wegen zu vieler Treffer werden Suchen mit nur zwei Buchstaben oder häufigen Wörtern wie \"this\", \"a\" und \"is\" im Suchergebnis nicht angezeigt.</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "Sie können eine Markierung zu einer Nachricht oder einem Kommentar hinzufügen indem Sie das ", "search_results.usageFlag3": " Symbol neben dem Zeitstempel klicken.", "search_results.usageFlag4": "Markierungen dienen als Möglichkeit Nachrichten für eine Wiedervorlage zu markieren. Ihre Markierungen sind persönlich und können nicht von anderen Benutzern gesehen werden.", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "Abbrechen", "setting_item_max.save": "Speichern", "setting_item_min.edit": "Bearbeiten", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "Mobile Push-Nachrichten versenden", "user.settings.push_notification.status": "Pushnachrichten auslösen wenn", "user.settings.push_notification.status_info": "Benachrichtigungen werden nur an Ihr Mobilgerät gesendet wenn Ihr Onlinestatus der oberen Auswahl entspricht.", + "user.settings.security.active": "Active", "user.settings.security.close": "Schließen", "user.settings.security.currentPassword": "Aktuelles Passwort", "user.settings.security.currentPasswordError": "Bitte geben Sie Ihr aktuelles Passwort ein.", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "E-Mail-Adresse und Passwort", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "Inaktiv", "user.settings.security.lastUpdated": "Zuletzt aktualisiert am {date} um {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Anmeldung durchgeführt durch GitLab", diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 3ef467937..68b898e7e 100644..100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "Channel", "channel_modal.createNew": "Create New ", "channel_modal.descriptionHelp": "Describe how this {term} should be used.", - "channel_modal.displayNameError": "This field is required", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "Edit", "channel_modal.group": "Group", "channel_modal.header": "Header", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>Sending Messages</h4><p>Type here to write a message and press <strong>ENTER</strong> to post it.</p><p>Click the <strong>Attachment</strong> button to upload an image or a file.</p>", "create_post.write": "Write a message...", "create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our <a href={TermsOfServiceLink}>Terms of Service</a> and <a href={PrivacyPolicyLink}>Privacy Policy</a>. If you do not agree, you cannot use {siteName}.", - "create_team.display_name.back": "Back to previous step", "create_team.display_name.charLength": "Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description later.", "create_team.display_name.nameHelp": "Name your team in any language. Your team name shows in menus and headings.", "create_team.display_name.next": "Next", @@ -1276,6 +1275,9 @@ "emoji_list.add": "Add Custom Emoji", "emoji_list.creator": "Creator", "emoji_list.delete": "Delete", + "emoji_list.delete.confirm.button": "Delete", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "No Custom Emoji Found", "emoji_list.header": "Custom Emoji", "emoji_list.help": "Custom emoji are available to everyone on your server. Type ':' in a message box to bring up the emoji selection menu. Other users may need to refresh the page before new emojis appear.", @@ -1324,7 +1326,6 @@ "flag_post.flag": "Flag for follow up", "flag_post.unflag": "Unflag", "general_tab.chooseDescription": "Please choose a new description for your team", - "general_tab.chooseName": "Please choose a new name for your team", "general_tab.codeDesc": "Click 'Edit' to regenerate Invite Code.", "general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by {getTeamInviteLink} in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.", "general_tab.codeTitle": "Invite Code", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "Toggle sidebar", "navbar.toggle2": "Toggle sidebar", "navbar.viewInfo": "View Info", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "About Mattermost", "navbar_dropdown.accountSettings": "Account Settings", "navbar_dropdown.console": "System Console", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "Flag", "post_info.mobile.unflag": "Unflag", "post_info.permalink": "Permalink", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "Reply", "post_info.system": "System", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(edited)", "posts_view.loadMore": "Load more messages", "posts_view.newMsg": "New Messages", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "Flag", "rhs_root.mobile.unflag": "Unflag", "rhs_root.permalink": "Permalink", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "Search", "search_bar.usage": "<h4>Search Options</h4><ul><li><span>Use </span><b>\"quotation marks\"</b><span> to search for phrases</span></li><li><span>Use </span><b>from:</b><span> to find posts from specific users and </span><b>in:</b><span> to find posts in specific channels</span></li></ul>", "search_header.results": "Search Results", "search_header.title2": "Recent Mentions", "search_header.title3": "Flagged Posts", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "Direct Message (with {username})", "search_item.jump": "Jump", "search_results.because": "<ul><li>If you're searching a partial phrase (ex. searching \"rea\", looking for \"reach\" or \"reaction\"), append a * to your search term.</li><li>Two letter searches and common words like \"this\", \"a\" and \"is\" won't appear in search results due to excessive results returned.</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "You can add a flag to messages and comments by clicking the ", "search_results.usageFlag3": " icon next to the timestamp.", "search_results.usageFlag4": "Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "Cancel", "setting_item_max.save": "Save", "setting_item_min.edit": "Edit", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "Send mobile push notifications", "user.settings.push_notification.status": "Trigger push notifications when", "user.settings.push_notification.status_info": "Notification alerts are only pushed to your mobile device when your online status matches the selection above.", + "user.settings.security.active": "Active", "user.settings.security.close": "Close", "user.settings.security.currentPassword": "Current Password", "user.settings.security.currentPasswordError": "Please enter your current password.", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "Email and Password", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "Inactive", "user.settings.security.lastUpdated": "Last updated {date} at {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Login done through GitLab", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index 902e75ba8..8a5566287 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "Canal", "channel_modal.createNew": "Crear Nuevo ", "channel_modal.descriptionHelp": "Describe como este {term} debería ser utilizado.", - "channel_modal.displayNameError": "Este campo es obligatorio", + "channel_modal.displayNameError": "Nombre del canal debe ser de 2 o más caracteres", "channel_modal.edit": "Editar", "channel_modal.group": "Grupo", "channel_modal.header": "Encabezado", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>Enviar Mensajes</h4><p>Escribe aquí para redactar un mensaje y presiona <strong>RETORNO</strong> para enviarlo.</p><p>Haz clic en el botón de <strong>Adjuntar</strong> para subir una imagen o archivo.</p>", "create_post.write": "Escribe un mensaje...", "create_team.agreement": "Al proceder con la creación de tu cuenta y utilizar {siteName}, estás de acuerdo con nuestros <a href={TermsOfServiceLink}>Términos de Servicio</a> y <a href={PrivacyPolicyLink}>Políticas de Privacidad</a>. Si no estás de acuerdo, no puedes utilizar {siteName}.", - "create_team.display_name.back": "Volver al paso anterior", "create_team.display_name.charLength": "El nombre debe ser {min} o más caracteres hasta un máximo de {max}. Puedes agregar una descripción mas larga al equipo más adelante.", "create_team.display_name.nameHelp": "Nombre de tu equipo en cualquier idioma. El nombre del equipo se muestra en menús y encabezados.", "create_team.display_name.next": "Siguiente", @@ -1276,6 +1275,9 @@ "emoji_list.add": "Agregar Emoticon Personalizado", "emoji_list.creator": "Creado por", "emoji_list.delete": "Eliminar", + "emoji_list.delete.confirm.button": "Eliminar", + "emoji_list.delete.confirm.msg": "Esta acción elimina de forma permanente el emoticon personalizado. ¿Estás seguro de que desea eliminar?", + "emoji_list.delete.confirm.title": "Eliminar Emoticon Personalizado", "emoji_list.empty": "No se encontraron Emoticones Personalizados", "emoji_list.header": "Emoticones Personalizados", "emoji_list.help": "Los emoticones personalizado están disponibles para todos los usuarios del servidor. Escribe ':' en el cuadro de mensaje para que aparezca el menú de selección de emoticones. Otros usuarios pueden necesitar refrescar la página antes de que los nuevos emoticones aparezcan.", @@ -1324,7 +1326,6 @@ "flag_post.flag": "Indicador de seguimiento", "flag_post.unflag": "Desmarcar", "general_tab.chooseDescription": "Por favor escoge una nueva descripción para tu equipo", - "general_tab.chooseName": "Por favor escoge otro nombre para tu equipo", "general_tab.codeDesc": "Haz clic en 'Editar' para regenerar el código de Invitación.", "general_tab.codeLongDesc": "El Código de invitación se utiliza como parte de la URL en el enlace de invitación al equipo creado por {getTeamInviteLink} en el menú principal. La regeneración se crea un nuevo enlace de invitación al equipo e invalida el enlace anterior.", "general_tab.codeTitle": "Código de Invitación", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "Mostrar Barra", "navbar.toggle2": "Esconder Barra", "navbar.viewInfo": "Ver Info", + "navbar.viewPinnedPosts": "Ver Mensajes Anclados", "navbar_dropdown.about": "Acerca de Mattermost", "navbar_dropdown.accountSettings": "Configurar Cuenta", "navbar_dropdown.console": "Consola de Sistema", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "Marcar", "post_info.mobile.unflag": "Desmarcar", "post_info.permalink": "Enlace permanente", + "post_info.pin": "Anclar al canal", + "post_info.pinned": "Anclado", "post_info.reply": "Responder", "post_info.system": "Sistema", + "post_info.unpin": "Desprender del canal", "post_message_view.edited": "(editado)", "posts_view.loadMore": "Cargar más mensajes", "posts_view.newMsg": "Nuevos Mensajes", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "Marcar", "rhs_root.mobile.unflag": "Desmarcar", "rhs_root.permalink": "Enlace permanente", + "rhs_root.pin": "Anclar al canal", + "rhs_root.unpin": "Desprender del canal", "search_bar.search": "Buscar", "search_bar.usage": "<h4>Opciones de Búsqueda</h4><ul><li><span>Utiliza </span><b>\"comillas\"</b><span> para buscar frases</span></li><li><span>Utiliza </span><b>from:</b><span> para encontrar mensajes de usuarios en específico e </span><b>in:</b><span> para encontrar mensajes de canales específicos</span></li></ul>", "search_header.results": "Resultados de la Búsqueda", "search_header.title2": "Menciones Recientes", "search_header.title3": "Mensajes Marcados", + "search_header.title4": "Mensajes anclados en {channelDisplayName}", "search_item.direct": "Mensajes Directos (con {username})", "search_item.jump": "Ir ", "search_results.because": "<ul><li>Si estás buscando un frase parcial (ej. buscando \"aud\", tratando de encontrar \"audible\" o \"audifonos\"), agrega un * a la palabra que estás buscando</li><li>Debido a la cantidad de resultados, la búsqueda con dos letras y palabras comunes como \"this\", \"a\" and \"es\" no aparecerán en los resultados debido a la cantidad excesiva de resultados retornados.</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "Puedes marcar los mensajes y comentarios haciendo clic en el icono ", "search_results.usageFlag3": " junto a la marca de hora.", "search_results.usageFlag4": "Las banderas son una forma de marcar los mensajes para hacerles seguimiento. Tus banderas son personales, y no puede ser vistas por otros usuarios.", + "search_results.usagePin1": "Todavía no hay mensajes anclados.", + "search_results.usagePin2": "Puedes anclar un mensaje al pincha en la opción \"Anclar al canal\" en el menu del mensaje.", + "search_results.usagePin3": "Los mensajes anclados son accesibles por todos los miembros del canal y es una forma de marcar un mensaje para futuras referencias.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "Cancelar", "setting_item_max.save": "Guardar", "setting_item_min.edit": "Editar", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "Enviar notificaciones push móvil", "user.settings.push_notification.status": "Activa las notificaciones push cuando", "user.settings.push_notification.status_info": "Las alertas de notificación sólo son enviadas a tu dispositivo móvil cuando tu estatus coincide con la selección anterior.", + "user.settings.security.active": "Activo", "user.settings.security.close": "Cerrar", "user.settings.security.currentPassword": "Contraseña Actual", "user.settings.security.currentPasswordError": "Por favor ingresa tu contraseña actual.", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "Correo electrónico y Contraseña", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "Inactivo", "user.settings.security.lastUpdated": "Última actualización {date} a las {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Inicio de sesión realizado a través de GitLab", diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json index acbf28516..841f5ecdd 100644 --- a/webapp/i18n/fr.json +++ b/webapp/i18n/fr.json @@ -141,7 +141,7 @@ "admin.advance.metrics": "Suivi des performances", "admin.audits.reload": "Recharger les logs de l'activité utilisateur", "admin.audits.title": "Activité de l'utilisateur", - "admin.authentication.email": "Authentification email", + "admin.authentication.email": "Authentification e-mail", "admin.authentication.gitlab": "GitLab", "admin.authentication.ldap": "AD/LDAP", "admin.authentication.oauth": "OAuth 2.0", @@ -176,7 +176,7 @@ "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>La conformité est une option disponible sur l'édition Entreprise. Votre licence ne permet pas d'utiliser cette fonction. Veuillez cliquer <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour des informations sur la licence Entreprise et connaître son prix.</p>", "admin.compliance.save": "Enregistrer", "admin.compliance.saving": "Enregistrement des paramètres...", - "admin.compliance.title": "Configuration de la conformité", + "admin.compliance.title": "Paramètres de conformité", "admin.compliance.true": "vrai", "admin.compliance_reports.desc": "Profession :", "admin.compliance_reports.desc_placeholder": "Ex : \"Audit 445 pour les RH\"", @@ -307,9 +307,9 @@ "admin.general.localization.availableLocalesNoResults": "Aucun résultat trouvé", "admin.general.localization.availableLocalesNotPresent": "La langue par défaut du client doit être inclue dans la liste des langues disponibles", "admin.general.localization.availableLocalesTitle": "Langues disponibles :", - "admin.general.localization.clientLocaleDescription": "La langue par défaut est utilisé pour les nouveaux utilisateurs et les pages créées où l'utilisateur ne s'est pas connecté.", + "admin.general.localization.clientLocaleDescription": "La langue par défaut est utilisée pour les utilisateurs nouvellement créés et les pages où l'utilisateur ne s'est pas encore connecté.", "admin.general.localization.clientLocaleTitle": "Langue par défaut :", - "admin.general.localization.serverLocaleDescription": "La langue par défaut est utilisée pour les messages et journaux systèmes. La modification de cette langue nécessitera un redémarrage du serveur avant de prendre effet.", + "admin.general.localization.serverLocaleDescription": "La langue par défaut est utilisée pour les messages et journaux systèmes. Modifier la langue nécessite un redémarrage du serveur avant de prendre effet.", "admin.general.localization.serverLocaleTitle": "Langue par défaut du serveur :", "admin.general.log": "Journalisation", "admin.general.policy": "Règles", @@ -347,7 +347,7 @@ "admin.general.privacy": "Confidentialité", "admin.general.usersAndTeams": "Utilisateur et équipes", "admin.gitab.clientSecretDescription": "Récupérez cette information depuis les instructions ci-dessus pour vous connecter à GitLab.", - "admin.gitlab.EnableHtmlDesc": "<ol><li>Connectez vous à votre compte GitLab et allez dans les réglages de votre profil, puis dans \"Applications\".</li><li>Saisissez les URLs de redirection \"<your-mattermost-url>/login/gitlab/complete\" (exemple: http://localhost:8065/login/gitlab/complete) et \"<your-mattermost-url>/signup/gitlab/complete\". </li><li>Puis utilisez les champs \"Secret\" et \"Id\" de GitLab pour compléter les options ci-dessous.</li><li>Complétez les URL de fin de parcours (Endpoint) ci-dessous. </li></ol>", + "admin.gitlab.EnableHtmlDesc": "<ol><li>Connectez vous à votre compte GitLab et allez dans les paramètres de votre profil, puis dans \"Applications\".</li><li>Saisissez les URLs de redirection \"<your-mattermost-url>/login/gitlab/complete\" (exemple: http://localhost:8065/login/gitlab/complete) et \"<your-mattermost-url>/signup/gitlab/complete\". </li><li>Puis utilisez les champs \"Clé secrète de l'application\" et \"ID d'application\" de GitLab pour compléter les options ci-dessous.</li><li>Complétez les URL de fin de parcours (Endpoint) ci-dessous. </li></ol>", "admin.gitlab.authDescription": "Saisissez https://<your-gitlab-url>/oauth/authorize (par exemple https://exemple.com:3000/oauth/authorize). Vérifiez si vous utilisez HTTP ou HTTPS que votre URL est correctement saisie.", "admin.gitlab.authExample": "Ex. : \"https://<your-gitlab-url>/oauth/authorize\"", "admin.gitlab.authTitle": "URL d'authentification (auth endpoint) :", @@ -358,7 +358,7 @@ "admin.gitlab.clientSecretTitle": "Clé secrète de l'application :", "admin.gitlab.enableDescription": "Si activé, Mattermost autorise la création d'une équipe et l'inscription avec le service OAuth de GitLab.", "admin.gitlab.enableTitle": "Activer l'authentification par GitLab : ", - "admin.gitlab.settingsTitle": "Configuration de GitLab", + "admin.gitlab.settingsTitle": "Paramètres de GitLab", "admin.gitlab.tokenDescription": "Saisissez https://<your-gitlab-url>/oauth/token. Veillez à saisir HTTP ou HTTPS dans l'URL suivant votre configuration.", "admin.gitlab.tokenExample": "Ex. : \"https://<your-gitlab-url>/oauth/token\"", "admin.gitlab.tokenTitle": "URL du jeton (token endpoint) :", @@ -524,7 +524,7 @@ "admin.log.locationDescription": "Fichier des journaux. Si vide, les journaux seront enregistrés dans \"./logs/mattermost.log\", avec une rotation toutes les 10000 lignes.", "admin.log.locationPlaceholder": "Saisir l'emplacement du fichier", "admin.log.locationTitle": "Répertoire des fichiers journaux :", - "admin.log.logSettings": "Configuration du journal (log)", + "admin.log.logSettings": "Paramètres de journalisation (logs)", "admin.logs.reload": "Recharger", "admin.logs.title": "Journaux du serveur", "admin.metrics.enableDescription": "Lorsqu'activé, Mattermost activera la collecte des rapports de performance et de profilage. Veuillez vous référer à la <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentation</a> pour en savoir plus sur la configuration du suivi des performances de Mattermost.", @@ -575,7 +575,7 @@ "admin.privacy.showFullNameTitle": "Afficher le nom complet : ", "admin.purge.button": "Purger tous les caches", "admin.purge.loading": " Chargement…", - "admin.purge.purgeDescription": "Ceci va purger tous les caches en mémoire pour des éléments tels que les sessions, les comptes, les canaux, etc. Les déploiement qui utilisent la Haute Disponibilité tenteront de purger tous les serveurs du cluster. Purger les caches peut affecter les performances.", + "admin.purge.purgeDescription": "Ceci va purger tous les caches en mémoire pour des éléments tels que les sessions, les comptes, les canaux, etc. Les déploiements qui utilisent la Haute Disponibilité tenteront de purger tous les serveurs du cluster. Purger les caches peut affecter les performances.", "admin.purge.purgeFail": "Impossible de purger : {error}", "admin.rate.enableLimiterDescription": "Si activé, les APIs sont limitées comme spécifié ci-dessous.", "admin.rate.enableLimiterTitle": "Limiter l'accès aux API : ", @@ -595,7 +595,7 @@ "admin.rate.queriesTitle": "Nombre maximum de requêtes par seconde :", "admin.rate.remoteDescription": "Si activé, l'API est limitée au moyen de l'adresse IP du client.", "admin.rate.remoteTitle": "Adapter la limite de fréquence en fonction de l'adresse distante : ", - "admin.rate.title": "Configuration des limites de débit", + "admin.rate.title": "Paramètres des limites de débit", "admin.recycle.button": "Recycler les connexions à la base de données", "admin.recycle.loading": " Recyclage...", "admin.recycle.recycleDescription": "Les déploiements utilisant plusieurs bases de données peuvent basculer d'une base de donnée principale à une autre sans redémarrer le serveur Mattermost en mettant à jour le fichier \"config.json\" avec la nouvelle configuration désirée, et en utilisant l'option <a href=\"../general/configuration\"><b>Configuration > Recharger le fichier de configuration</b></a> pour charger les nouveaux paramètres pendant que le serveur est en activité. L'administrateur devra donc ensuite utiliser l'option <b>Recycler les connexions à la base de données</b> pour recycler les connexions en se basant sur les nouveaux paramètres.", @@ -693,7 +693,7 @@ "admin.service.developerTitle": "Activer le mode développeur : ", "admin.service.enforcMfaTitle": "Imposer l'authentification multi-facteurs :", "admin.service.enforceMfaDesc": "Lorsqu'activé, l'<a href='https://docs.mattermost.com/deployment/auth.html' target='_blank'>authentification multi-facteurs (MFA)</a> est requise pour la connexion. Les nouveaux utilisateurs devront configurer MFA lors de leur inscription. Les utilisateurs connectés sans MFA configuré seront redirigés vers la page de configuration MFA jusqu'à ce que la configuration soit terminée.<br/><br/>Si votre système dispose d'utilisateurs se connectant autrement que par AD/LDAP ou une adresse e-mail, MFA devra être appliqué avec un fournisseur d'authentification extérieur à Mattermost.", - "admin.service.forward80To443": "Transférer le port 80 vers le port 443 :", + "admin.service.forward80To443": "Rediriger le port 80 sur le port 443 :", "admin.service.forward80To443Description": "Faire suivre le trafic non chiffré du port 80 vers le port sécurisé 443", "admin.service.googleDescription": "Définissez cette clé pour activer l'affichage des titres pour les aperçus de vidéos YouTube. Sans la clé, les aperçus YouTube seront créés à partir des liens apparaissant des messages ou commentaires mais ils ne montreront pas le titre de la vidéo. Regardez un <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">tutoriel Google Developers</a> pour des instructions sur comment obtenir une clé.", "admin.service.googleExample": "Ex. : \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"", @@ -718,14 +718,14 @@ "admin.service.overrideDescription": "Lorsqu'activé, les webhooks, commandes slash et autres intégrations, telles que <a href=\"https://docs.mattermost.com/integrations/zapier.html\" target=\"_blank\">Zapier</a>, seront autorisées à changer le nom d'utilisateur à partir duquel elles émettent des messages. Note : Si les intégrations sont également autorisées à redéfinir les photos de profil utilisateur, des utilisateurs pourraient effectuer des attaques de phishing en se faisant passer pour d'autres utilisateurs.", "admin.service.overrideTitle": "Permettre des intégrations pour remplacer les noms d'utilisateur:", "admin.service.readTimeout": "Délai d'attente de lecture :", - "admin.service.readTimeoutDescription": "Temps maximum autorisé à partir du moment à partir duquel la connexion est acceptée jusqu'au moment auquel le corps de la requête est entièrement lu.", + "admin.service.readTimeoutDescription": "Temps maximum autorisé à partir du moment à partir duquel la connexion est acceptée jusqu'au moment où le corps de la requête est entièrement lu.", "admin.service.securityDesc": "Si activé, les administrateurs système sont notifiés par e-mail si une alerte de sécurité a été annoncée dans les dernières 12 heures. L'envoi d'e-mails doit être activé.", "admin.service.securityTitle": "Activer les alertes de sécurité : ", "admin.service.sessionCache": "Cache de session en minutes :", "admin.service.sessionCacheDesc": "Durée pendant laquelle une session est gardée en mémoire.", "admin.service.sessionDaysEx": "Ex. : \"30\"", "admin.service.siteURL": "URL du site :", - "admin.service.siteURLDescription": "L'URL, incluant le numéro de port et le protocol, que les utilisateurs utilisent pour accéder à Mattermost. Ce champ peut être laissé vide à moins que vous ne configuriez l'envoi d'email par lot dans <b>Notifications > Email</b>. Lorsque le champ est vide, l'URL est configurée automatiquement sur base du trafic entrant.", + "admin.service.siteURLDescription": "L'URL, incluant le numéro de port et le protocol, que les utilisateurs utilisent pour accéder à Mattermost. Ce champ peut être laissé vide à moins que vous ne configuriez l'envoi d'e-mail par lot dans <b>Notifications > Email</b>. Lorsque le champ est vide, l'URL est configurée automatiquement sur base du trafic entrant.", "admin.service.siteURLExample": "Ex. : \"https://mattermost.example.com:1234\"", "admin.service.ssoSessionDays": "Durée de la session SSO (en jours) :", "admin.service.ssoSessionDaysDesc": "Le nombre de jours entre la dernière fois qu'un utilisateur a spécifié ses identifiants et l'expiration de la session de l'utilisateur. Si la méthode d'authentification est SAML ou GitLab, l'utilisateur pourra être automatiquement connecté à nouveau dans Mattermost s'ils sont déjà connectés dans SAML ou GitLab. Après le changement de ce paramètre, il prendra effet la prochaine fois que les utilisateurs saisiront leurs identifiants.", @@ -735,21 +735,21 @@ "admin.service.tlsCertFileDescription": "Le fichier de certificat à utiliser.", "admin.service.tlsKeyFile": "Fichier de la clé TLS :", "admin.service.tlsKeyFileDescription": "Le fichier contenant la clé privée à utiliser.", - "admin.service.useLetsEncrypt": "Utilise Let's Encrypt :", - "admin.service.useLetsEncryptDescription": "Activer la récupération automatique des certificats de Let's Encrypt. Le certificat sera récupéré lorsqu'un client tentera de se connecter à partir d'un nouveau domaine. Ceci fonctionne avec de domaines multiples.", - "admin.service.webSessionDays": "Longueur de la session LDAP et email (jours) :", + "admin.service.useLetsEncrypt": "Utiliser Let's Encrypt :", + "admin.service.useLetsEncryptDescription": "Activer la récupération automatique des certificats de Let's Encrypt. Le certificat sera récupéré lorsqu'un client tentera de se connecter à partir d'un nouveau domaine. Ceci fonctionne également avec des domaines multiples.", + "admin.service.webSessionDays": "Longueur de la session LDAP et e-mail (jours) :", "admin.service.webSessionDaysDesc": "Le nombre de jours entre la dernière fois qu'un utilisateur a entré ses identifiants et l'expiration de la session de l'utilisateur. Après le changement de ce paramètre, la nouvelle durée de session prendra effet la prochaine fois que les utilisateurs entreront leurs identifiants.", "admin.service.webhooksDescription": "Si activé, les webhooks entrants seront autorisés. Pour aider à combattre les attaques d'hameçonnage, tous les messages de webhooks seront étiquetés par un label BOT. Consultez la <a href='http://docs.mattermost.com/developer/webhooks-incoming.html' target='_blank'>documentation</a> pour en apprendre davantage.", "admin.service.webhooksTitle": "Activer les webhooks entrants : ", "admin.service.writeTimeout": "Délai d'attente d'écriture :", - "admin.service.writeTimeoutDescription": "Si vous utilisez HTTP (non sécurisé), il s'agit du temps maximum autorisé à partir du moment auquel les entêtes sont lus jusqu'au moment auquel la réponse est écrite. Si vous utilisez le protocole HTTPS, c'est le temps total à partir du moment où la connexion est acceptée jusqu'au moment où la réponse est écrite.", + "admin.service.writeTimeoutDescription": "Si vous utilisez HTTP (non sécurisé), il s'agit du temps maximum autorisé à partir du moment où les entêtes sont lus jusqu'au moment où la réponse est écrite. Si vous utilisez le protocole HTTPS, c'est le temps total à partir du moment où la connexion est acceptée jusqu'au moment où la réponse est écrite.", "admin.sidebar.addTeamSidebar": "Afficher l'équipe dans le menu", "admin.sidebar.advanced": "Options avancées", "admin.sidebar.audits": "Conformité et vérification", "admin.sidebar.authentication": "authentification", "admin.sidebar.cluster": "Haute disponibilité (Beta)", "admin.sidebar.compliance": "Conformité", - "admin.sidebar.configuration": "configuration", + "admin.sidebar.configuration": "Configuration", "admin.sidebar.connections": "Connexions", "admin.sidebar.customBrand": "Image de marque personnalisée", "admin.sidebar.customEmoji": "Emoji personnalisés", @@ -882,13 +882,13 @@ "admin.true": "oui", "admin.userList.title": "Utilisateurs de {team}", "admin.userList.title2": "Utilisateurs de {team} ({count})", - "admin.user_item.authServiceEmail": "<strong>Méthode de connexion :</strong> Adresse email", + "admin.user_item.authServiceEmail": "<strong>Méthode de connexion :</strong> Adresse e-mail", "admin.user_item.authServiceNotEmail": "<strong>Méthode de connexion :</strong> {service}", "admin.user_item.confirmDemoteDescription": "Si vous vous retirez le rôle d'administrateur et qu'il n'y a aucun autre administrateur désigné, vous devrez en désigner un en utilisant les outils en ligne de commande depuis un terminal sur le serveur.", "admin.user_item.confirmDemoteRoleTitle": "Confirmez le retrait de votre rôle d'administrateur", "admin.user_item.confirmDemotion": "Confirmer le retrait", "admin.user_item.confirmDemotionCmd": "Rôle au sein de la plateforme system_admin {username}", - "admin.user_item.emailTitle": "<strong>Email:</strong> {email}", + "admin.user_item.emailTitle": "<strong>E-mail:</strong> {email}", "admin.user_item.inactive": "Inactif", "admin.user_item.makeActive": "Activer", "admin.user_item.makeInactive": "Désactiver", @@ -931,15 +931,15 @@ "admin.webserverModeGzip": "gzip", "admin.webserverModeGzipDescription": "Le serveur Mattermost servira les fichiers statiques compressés avec gzip.", "admin.webserverModeHelpText": "La compression gzip s'applique aux fichiers statiques. Il est recommandé de l'activer pour améliorer les performances sauf si votre environnement dispose de restrictions spécifiques, comme un proxy web qui distribue mal des fichiers compressés en gzip.", - "admin.webserverModeTitle": "Mode webserver :", + "admin.webserverModeTitle": "Mode du serveur web :", "admin.webserverModeUncompressed": "Non compressé", "admin.webserverModeUncompressedDescription": "Le serveur Mattermost servira les fichiers statiques sans compression.", "analytics.chart.loading": "Chargement…", "analytics.chart.meaningful": "Pas assez de données pour afficher quelque chose de pertinent.", "analytics.system.activeUsers": "Utilisateurs actifs avec messages", "analytics.system.channelTypes": "Types de canaux", - "analytics.system.dailyActiveUsers": "Utilisateurs actifs quotidien", - "analytics.system.monthlyActiveUsers": "Utilisateurs actifs mensuel", + "analytics.system.dailyActiveUsers": "Utilisateurs actifs quotidiens", + "analytics.system.monthlyActiveUsers": "Utilisateurs actifs mensuels", "analytics.system.postTypes": "Messages, fichiers et hashtags", "analytics.system.privateGroups": "Groupes privés", "analytics.system.publicChannels": "Canaux publics", @@ -1118,7 +1118,7 @@ "channel_modal.channel": "Canal", "channel_modal.createNew": "Créer un nouveau ", "channel_modal.descriptionHelp": "Décrit comment ce {term} doit être utilisé.", - "channel_modal.displayNameError": "Ce champ est obligatoire", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "Modifier", "channel_modal.group": "Groupe", "channel_modal.header": "Entête", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>Envoyer des messages</h4><p>Veuillez saisir votre message ici et tapez <strong>Entrée</strong> pour l'envoyer.</p><p>Cliquez sur le bouton <strong>pièce-jointe</strong> pour télécharger une image ou un fichier.</p>", "create_post.write": "Écrire un message...", "create_team.agreement": "En créant votre compte et en utilisant {siteName}, vous acceptez nos <a href={TermsOfServiceLink}>Conditions Générales d'Utilisation</a> et notre <a href={PrivacyPolicyLink}>Politique des données personnelles</a>. Si vous le les acceptez pas, vous ne pouvez pas utiliser {siteName}.", - "create_team.display_name.back": "Retour à l’étape précedente", "create_team.display_name.charLength": "Le nom doit être de {min} caractères ou plus, jusqu'à un maximum de {max}. Vous pourrez ajouter une description d'équipe plus longue par la suite.", "create_team.display_name.nameHelp": "Nommez votre équipe dans toutes les langues. Votre nom d'équipe sera montré dans les menus et rubriques.", "create_team.display_name.next": "Suivant", @@ -1276,6 +1275,9 @@ "emoji_list.add": "Ajouter un emoji personnalisé", "emoji_list.creator": "Auteur", "emoji_list.delete": "Supprimer", + "emoji_list.delete.confirm.button": "Supprimer", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "Aucun emoji personnalisé", "emoji_list.header": "Emoticônes personnalisées", "emoji_list.help": "Les emoji personnalisés sont disponible pour tous les utilisateurs et apparaissent dans le menu d'autocomplétion des emoji. Il faut rafraîchir la page pour que les nouveaux emoji apparaissent.", @@ -1324,7 +1326,6 @@ "flag_post.flag": "Marquez le message d'un indicateur pour le suivi", "flag_post.unflag": "Supprimer l'indicateur", "general_tab.chooseDescription": "Veuillez choisir une nouvelle description pour votre équipe", - "general_tab.chooseName": "Choisissez un nom pour votre équipe", "general_tab.codeDesc": "Veuillez cliquer sur 'Modifier' pour réinitialiser le code d'invitation", "general_tab.codeLongDesc": "Le code d'invitation est utilisé dans l'URL du lien d'invitation à l'équipe. Ce lien est créé par {getTeamInviteLink} depuis le menu principal. Regénérer un nouveau lien d'invitation invalidera les invitations déjà envoyées.", "general_tab.codeTitle": "Code d'invitation", @@ -1441,7 +1442,7 @@ "help.mentioning.username": "#### @Nom d'utilisateur\nVous pouvez mentionner un coéquipier en utilisant le symbole `@` suivi de son nom d'utilisateur pour lui envoyer une notification de mention.\n\nTapez `@` pour afficher une liste des membres de l'équipe qui peuvent être mentionnés. Pour filtrer la liste, tapez les premières lettres de n'importe quel nom d'utilisateur, prénom, nom de famille, ou surnom. Les flèches du clavier **Haut** et **Bas** peuvent être utilisées pour faire défiler les différentes entrées dans la liste; la touche **Entrée** une fois appuyée sélectionnera l'utilisateur à mentionner. Une fois sélectionné, le nom d'utilisateur va automatiquement remplacer le nom et prénom ou le surnom.\nL'exemple suivant envoie une notification spéciale de mention à **alice** qui l'alerte du canal et du message dans laquelle elle a été citée. Si **alice** est absente de Mattermost et qu'elle a activé les [notifications par e-mail](http://docs.mattermost.com/help/getting-started/configuring-notifications.html#email-notifications), alors elle recevra une alerte par e-mail de sa mention accompagnée du message texte concerné par cette mention.", "help.mentioning.usernameCont": "Si l'utilisateur que vous avez mentionné n'appartient pas au canal, un message système sera envoyé pour vous le faire savoir. Il s'agit d'un message temporaire seulement visible par la personne qui l'a déclenché. Pour ajouter la personne mentionnée au canal, rendez-vous dans le menu déroulant en-dessous du nom du canal et sélectionnez **Ajouter Membres**.", "help.mentioning.usernameExample": "@alice comment s'est déroulé votre entretien avec le nouveau candidat ?", - "help.messaging.attach": "**Ajoutez des fichiers** en les glissant-déposant dans Mattermost or en cliquant l'icône de pièce jointe dans la barre de messages.", + "help.messaging.attach": "**Ajoutez des fichiers** en les glissant-déposant dans Mattermost ou en cliquant sur l'icône de pièce jointe dans la barre de composition des messages.", "help.messaging.emoji": "**Ajoutez rapidement des émoticônes** en tapant \":\", ce qui ouvrira une liste de suggestions d'émoticônes. Si l'émoticône existante ne couvre pas ce que vous souhaitez exprimer, vous pouvez également créer [votre propre émoticône](http://docs.mattermost.com/help/settings/custom-emoji.html).", "help.messaging.format": "**Formatez vos messages** en utilisant Markdown qui supporte les styles de texte, titres, émoticônes, blocs de code, blocs de citation, tableaux, listes et images intégrées au texte.", "help.messaging.notify": "**Avertissez vos coéquipiers** lorsque nécessaire en tapant `@nom d'utilisateur`.", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "Basculer la barre latérale", "navbar.toggle2": "Basculer la barre latérale", "navbar.viewInfo": "Afficher Informations", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "À propos", "navbar_dropdown.accountSettings": "Paramètres du compte", "navbar_dropdown.console": "Console système", @@ -1682,7 +1684,7 @@ "navbar_dropdown.switchTeam": "Basculer sur {team}", "navbar_dropdown.switchTo": "Basculer vers ", "navbar_dropdown.teamLink": "Créer une invitation", - "navbar_dropdown.teamSettings": "Configuration de l'équipe", + "navbar_dropdown.teamSettings": "Paramètres d'équipe", "navbar_dropdown.viewMembers": "Voir les membres", "notification.dm": "Message privé", "passwordRequirements": "Pré-requis du mot de passe :", @@ -1691,15 +1693,15 @@ "password_form.enter": "Saisissez un nouveau mot de passe pour votre compte {siteName}.", "password_form.error": "Veuillez saisir au moins {chars} caractères.", "password_form.pwd": "Mot de passe", - "password_form.title": "Réinitialisation mot de passe", + "password_form.title": "Réinitialisation du mot de passe", "password_form.update": "Votre mot de passe a été correctement mis à jour.", - "password_send.checkInbox": "Veuillez contrôler votre boite de réception.", + "password_send.checkInbox": "Veuillez vérifier votre boîte de réception.", "password_send.description": "Pour réinitialiser votre mot de passe, veuillez saisir l'adresse e-mail que vous avez utilisée pour vous inscrire", "password_send.email": "Adresse e-mail", "password_send.error": "Veuillez spécifier une adresse e-mail valide.", "password_send.link": "Si le compte existe, une demande de réinitialisation du mot de passe sera envoyée à l'adresse e-mail : <br/><b>{email}</b><br/><br/>", "password_send.reset": "Réinitialiser mon mot de passe", - "password_send.title": "Réinitialisation mot de passe", + "password_send.title": "Réinitialisation du mot de passe", "pdf_preview.max_pages": "Télécharger pour lire plus de pages", "pending_post_actions.cancel": "Annuler", "pending_post_actions.retry": "Réessayer", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "Marquer avec un indicateur", "post_info.mobile.unflag": "Supprimer l'indicateur", "post_info.permalink": "Lien permanent", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "Répondre", "post_info.system": "Système", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(édité)", "posts_view.loadMore": "Charger plus de messages", "posts_view.newMsg": "Nouveaux messages", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "Ajouter un indicateur", "rhs_root.mobile.unflag": "Supprimer l'indicateur", "rhs_root.permalink": "Lien permanent", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "Rechercher", "search_bar.usage": "<h4>Options de recherche</h4><ul><li><span>Utilisez </span><b>\"des guillemets\"</b><span> pour rechercher des phrases</span></li><li><span>Utilisez </span><b>from:</b><span> pour rechercher des messages d'utilisateur spécifiques et </span><b>in:</b><span> pour rechercher des messages sur des canaux spécifiques</span></li></ul>", "search_header.results": "Résultats de la recherche", "search_header.title2": "Mentions récentes", "search_header.title3": "Messages marqués d'un indicateur", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "Message privé (avec {username})", "search_item.jump": "Aller à", "search_results.because": "<ul><li>Pour rechercher une partie d'un mot (ex. rechercher \"réa\" en souhaitant \"réagir\" ou \"réaction\"), ajoutez un * au terme recherché</li><li>En raison du nombre de résultats, les recherches sur deux lettres ou sur les mots communs tels que \"ce\", \"un\" et \"est\" n'apparaîtront pas dans les résultats de la recherche</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "Vous pouvez marquer un message ou un commentaire en cliquant sur ", "search_results.usageFlag3": " l'icône à côté de l'horodateur.", "search_results.usageFlag4": "Marquer un message est un bon moyen d'assurer le suivi. Marquer un message est personnel et ne peut être vu par les autres utilisateurs.", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "Annuler", "setting_item_max.save": "Enregistrer", "setting_item_min.edit": "Modifier", @@ -1824,7 +1836,7 @@ "sidebar_right_menu.recentMentions": "Mentions récentes", "sidebar_right_menu.report": "Signaler un problème", "sidebar_right_menu.teamLink": "Créer un lien d'invitation d'équipe", - "sidebar_right_menu.teamSettings": "Configuration de l'équipe", + "sidebar_right_menu.teamSettings": "Paramètres d'équipe", "sidebar_right_menu.viewMembers": "Voir les membres", "signup.email": "Adresse e-mail et mot de passe", "signup.gitlab": "Authentification unifiée avec GitLab", @@ -1923,7 +1935,7 @@ "team_settings_modal.exportTab": "Exporter", "team_settings_modal.generalTab": "Général", "team_settings_modal.importTab": "Importer", - "team_settings_modal.title": "Configuration de l'équipe", + "team_settings_modal.title": "Paramètres d'équipe", "team_sidebar.join": "Les autres équipes que vous pouvez rejoindre.", "textbox.bold": "**gras**", "textbox.edit": "Modifier le message", @@ -2068,13 +2080,13 @@ "user.settings.general.newAddress": "Nouvelle adresse : {email}<br />Vérifiez vos e-mails pour valider votre adresse e-mail.", "user.settings.general.nickname": "Pseudo", "user.settings.general.nicknameExtra": "Vous pouvez utiliser un pseudo à la place de vos prénom, nom et nom d'utilisateur. Ceci est pratique lorsque deux personnes de votre équipe ont des noms similaires phonétiquement.", - "user.settings.general.notificationsExtra": "Par défaut, vous recevez une notification lorsqu'un utilisateur cite votre prénom. Rendez vous dans les réglages {notify} pour modifier ce paramètre.", + "user.settings.general.notificationsExtra": "Par défaut, vous recevez une notification lorsqu'un utilisateur cite votre prénom. Rendez vous dans les paramètres de {notify} pour modifier ce paramètre.", "user.settings.general.notificationsLink": "Notifications", "user.settings.general.position": "Rôle", "user.settings.general.positionExtra": "Veuillez utiliser ce champ pour spécifier votre rôle ou intitulé de poste. Il sera affiché dans votre infobulle de profil utilisateur.", "user.settings.general.primaryEmail": "Adresse e-mail principale", "user.settings.general.profilePicture": "Photo de profil", - "user.settings.general.title": "Configuration générale", + "user.settings.general.title": "Paramètres généraux", "user.settings.general.uploadImage": "Veuillez cliquer sur ‘Modifier’ pour télécharger une image", "user.settings.general.username": "Nom d'utilisateur", "user.settings.general.usernameInfo": "Veuillez spécifier un nom d'utilisateur facile à reconnaître et à mémoriser pour vos collègues.", @@ -2158,7 +2170,7 @@ "user.settings.notifications.sensitiveName": "Votre prénom (respectant la casse) \"{first_name}\"", "user.settings.notifications.sensitiveUsername": "Votre nom d'utilisateur (insensible à la casse) \"{username}\"", "user.settings.notifications.sensitiveWords": "Autres mots insensibles à la casse, séparés par des virgules :", - "user.settings.notifications.soundConfig": "Configurez les sons des notifications dans les préférences de votre navigateur", + "user.settings.notifications.soundConfig": "Configurez les sons des notifications dans les paramètres de votre navigateur", "user.settings.notifications.sounds_info": "Les sons de notification sont disponibles sur IE11, Edge, Safari, Chrome et les applications desktop Mattermost.", "user.settings.notifications.teamWide": "Mentions à toute l'équipe \"@all\"", "user.settings.notifications.title": "Paramètres de notification", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "Notifications push mobile", "user.settings.push_notification.status": "Déclencher des notifications push lorsque", "user.settings.push_notification.status_info": "Les notifications seront envoyées sur votre téléphone uniquement si votre statut en ligne correspond à la sélection ci-dessus.", + "user.settings.security.active": "Active", "user.settings.security.close": "Quitter", "user.settings.security.currentPassword": "Mot de passe actuel", "user.settings.security.currentPasswordError": "Veuillez saisir votre mot de passe actuel", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "Adresse e-mail et mot de passe", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "Inactif", "user.settings.security.lastUpdated": "Dernière mise à jour le {date} à {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Connexion avec GitLab", diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json index da92f954e..8cc3d7ee6 100644 --- a/webapp/i18n/ja.json +++ b/webapp/i18n/ja.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "チャンネル", "channel_modal.createNew": "新規 ", "channel_modal.descriptionHelp": "この{term}がどのように使われるべきか説明してください。", - "channel_modal.displayNameError": "この項目は必須です", + "channel_modal.displayNameError": "チャンネル名は2文字以上にしてください", "channel_modal.edit": "編集する", "channel_modal.group": "グループ", "channel_modal.header": "ヘッダー", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>メッセージを送信する</h4><p>ここにメッセージを書き、<strong>ENTER</strong>を押すことで投稿します。</p><p><strong>添付</strong>ボタンを押すことで画像やファイルをアップロードします。</p>", "create_post.write": "メッセージを書き込んでいます…", "create_team.agreement": "アカウントを作成し{siteName}を利用する前に<a href={TermsOfServiceLink}>使用条件</a>と<a href={PrivacyPolicyLink}>プライバシーポリシー</a>に同意してください。同意できない場合は{siteName}は使用できません。", - "create_team.display_name.back": "前のステップに戻る", "create_team.display_name.charLength": "名前は{min}文字以上の{max}文字以下にしてください。後でより長いチームの説明を追加することができます。", "create_team.display_name.nameHelp": "チーム名はどんな言語でも使うことができます。チーム名はメニューと画面上部に表示されます。", "create_team.display_name.next": "次へ", @@ -1276,6 +1275,9 @@ "emoji_list.add": "カスタム絵文字を追加", "emoji_list.creator": "作成者", "emoji_list.delete": "削除", + "emoji_list.delete.confirm.button": "削除", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "カスタム絵文字がありません。", "emoji_list.header": "カスタム絵文字", "emoji_list.help": "カスタム絵文字はサーバー上の全員が利用可能です。絵文字選択メニューを表示するために、メッセージボックスに':'を入力してみてください。新しい絵文字を表示するには、ページを再読み込みする必要があります。", @@ -1324,7 +1326,6 @@ "flag_post.flag": "追跡フラグ", "flag_post.unflag": "フラグを消す", "general_tab.chooseDescription": "あなたのチームの新しい説明を選択してください", - "general_tab.chooseName": "あなたのチームの新しい名称を選択してください", "general_tab.codeDesc": "招待コードを再生成するには「編集」をクリックしてください。", "general_tab.codeLongDesc": "招待コードは、メインメニューの {getTeamInviteLink} で作成されたチーム招待リンクのURLの一部として使われます。再生成することで新しいチーム招待リンクが作成され、古いリンクは無効化されます。", "general_tab.codeTitle": "招待コード", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "サイドバーの表示/非表示を切り替える", "navbar.toggle2": "サイドバーの表示/非表示を切り替える", "navbar.viewInfo": "情報を表示する", + "navbar.viewPinnedPosts": "ピン止めされた投稿を見る", "navbar_dropdown.about": "Mattermostについて", "navbar_dropdown.accountSettings": "アカウントの設定", "navbar_dropdown.console": "システムコンソール", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "フラグ", "post_info.mobile.unflag": "フラグを消す", "post_info.permalink": "パーマリンク", + "post_info.pin": "チャンネルにピン止めする", + "post_info.pinned": "ピン止め", "post_info.reply": "返信する", "post_info.system": "システム", + "post_info.unpin": "チャンネルへのピン止めをやめる", "post_message_view.edited": "(編集済)", "posts_view.loadMore": "もっとメッセージを読み込む", "posts_view.newMsg": "新しいメッセージ", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "フラグ", "rhs_root.mobile.unflag": "フラグを消す", "rhs_root.permalink": "パーマリンク", + "rhs_root.pin": "チャンネルにピン止めする", + "rhs_root.unpin": "チャンネルへのピン止めをやめる", "search_bar.search": "検索する", "search_bar.usage": "<h4>検索オプション</h4><ul><li><span>文を検索するには</span><b>「引用符」</b><span>を使ってください</span></li><li><span>特定のユーザーの投稿に限定して検索するには</span><b>From:</b><span>を、特定のチャンネルのみ検索するには</span><b>in:</b><span>を使ってください</span></li></ul>", "search_header.results": "検索結果", "search_header.title2": "最近のあなたについての投稿", "search_header.title3": "フラグの立てられた投稿", + "search_header.title4": "{channelDisplayName}チャンネルに投稿をピン止めする", "search_item.direct": "ダイレクトメッセージ({username}を参照)", "search_item.jump": "ジャンプする", "search_results.because": "<ul><li>語句の一部を検索するには*を付けてください(\"reach\"や\"reaction\"を検索するのに\"rea\"を使う場合等)</li><li>検索結果の件数によっては、2文字の検索、一般的な\"this\"や\"a\"、\"is\"は表示されません</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "アイコンをクリックすることでメッセージとコメントにフラグを立てることができます。", "search_results.usageFlag3": "アイコンはタイムスタンプの隣にあります。", "search_results.usageFlag4": "フラグはメッセージに追跡のためのマークを付ける一つの方法です。あなたのフラグは個人のもので、他のユーザーからは見えません。", + "search_results.usagePin1": "ピン止めされた投稿がまだありません。", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "キャンセル", "setting_item_max.save": "保存する", "setting_item_min.edit": "編集する", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "モバイルプッシュ通知を送信する", "user.settings.push_notification.status": "プッシュ通知をトリガーする場合", "user.settings.push_notification.status_info": "上で選択したオンラインステータスに合致する場合のみ、通知アラートをあなたのモバイルデバイスに送信します。", + "user.settings.security.active": "有効", "user.settings.security.close": "閉じる", "user.settings.security.currentPassword": "現在のパスワード", "user.settings.security.currentPasswordError": "現在のパスワードを入力してください。", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "電子メールアドレスとパスワード", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "無効", "user.settings.security.lastUpdated": "最終更新: {date} {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "GitLabでログインしました", diff --git a/webapp/i18n/ko.json b/webapp/i18n/ko.json index aa668ed14..6e36b91d3 100644 --- a/webapp/i18n/ko.json +++ b/webapp/i18n/ko.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "채널", "channel_modal.createNew": "새로운 ", "channel_modal.descriptionHelp": "{term}은 이렇게 사용되어야 합니다.", - "channel_modal.displayNameError": "필수 항목입니다.", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "편집", "channel_modal.group": "그룹", "channel_modal.header": "헤더", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>메시지 보내기</h4><p>보내고 싶은 메시지를 작성하고 <strong>Enter</strong>키를 눌러 보내세요.</p><p><strong>첨부</strong> 버튼을 눌러 이미지나 파일을 업로드하세요.</p>", "create_post.write": "메시지를 입력하세요...", "create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}.", - "create_team.display_name.back": "이전 단계로 돌아가기", "create_team.display_name.charLength": "Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description later.", "create_team.display_name.nameHelp": "팀 이름을 자유롭게 입력하세요. 설정한 팀 이름은 메뉴와 상단의 헤더에 표시됩니다.", "create_team.display_name.next": "다음", @@ -1276,6 +1275,9 @@ "emoji_list.add": "이모티콘 추가", "emoji_list.creator": "만든 사람", "emoji_list.delete": "삭제", + "emoji_list.delete.confirm.button": "삭제", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "커스텀 이모티콘이 없습니다.", "emoji_list.header": "커스텀 이모티콘", "emoji_list.help": "커스텀 이모티콘은 서버의 모두가 사용할 수 있습니다. 메시지 창에 ':'를 입력하여 이모티콘 선택 메뉴를 활성화 할 수 있습니다. 새로운 이모티콘을 등록하면 다른 사용자들은 사용하기 위해 페이지를 새로고침 해야할 수 있습니다.", @@ -1324,7 +1326,6 @@ "flag_post.flag": "중요 메시지로 지정", "flag_post.unflag": "중요 메시지 해제", "general_tab.chooseDescription": "사용자명을 선택하세요.", - "general_tab.chooseName": "사용자명을 선택하세요.", "general_tab.codeDesc": "가입 링크를 변경하려면 '편집'을 클릭하세요.", "general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.", "general_tab.codeTitle": "가입 링크", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "사이드바 토글", "navbar.toggle2": "사이드바 토글", "navbar.viewInfo": "정보 보기", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "Mattermost 정보", "navbar_dropdown.accountSettings": "계정 설정", "navbar_dropdown.console": "관리자 도구", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "중요 지정", "post_info.mobile.unflag": "중요 해제", "post_info.permalink": "링크", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "답글", "post_info.system": "System", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(edited)", "posts_view.loadMore": "메시지 더 보기", "posts_view.newMsg": "새로운 메시지", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "중요 지정", "rhs_root.mobile.unflag": "중요 해제", "rhs_root.permalink": "바로가기", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "검색", "search_bar.usage": "<h4>검색 옵션</h4><ul><li><b>\"따옴표\"</b><span>를 사용해서 문구를 검색할 수 있습니다.</span></li><li><b>from:</b><span> 를 사용해서 특정 사용자의 포스트를 검색하거나, </span><b>in:</b><span>을 사용하여 특정 채널의 포스트를 검색할 수 있습니다.</span></li></ul>", "search_header.results": "검색 결과", "search_header.title2": "최근 멘션", "search_header.title3": "중요 메시지", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "Direct Message (with {username})", "search_item.jump": "바로가기", "search_results.because": "<ul><li>문구의 일부분을 검색하려면(예: \"rea\", 를 검색하여 \"reach\" 또는 \"reaction\") 단어 앞 또는 뒤에 와일드카드 문자(*)를 붙여보세요.</li><li>두 글자 검색이나 \"this\", \"a\", \"is\"같이 일반적인 단어는 과도한 결과를 반환하기 때문에 검색결과에 표시되지 않습니다.</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "타임스탬프 옆의 ", "search_results.usageFlag3": " 아이콘을 클릭하여 중요 메시지를 추가할 수 있습니다.", "search_results.usageFlag4": "중요한 메시지를 따로 모아 확인할 수 있습니다. 중요 메시지는 개인별로 표시되며 다른 사람이 볼 수 없습니다.", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "취소", "setting_item_max.save": "저장", "setting_item_min.edit": "편집", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "알림 받기", "user.settings.push_notification.status": "지정한 상태일 때 모바일 푸시 알림", "user.settings.push_notification.status_info": "지정한 상태일때만 모바일 푸시 알림을 받습니다.", + "user.settings.security.active": "Active", "user.settings.security.close": "닫기", "user.settings.security.currentPassword": "현재 패스워드", "user.settings.security.currentPasswordError": "현재 패스워드를 입력해주세요", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "이메일과 패스워드", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "비활성화", "user.settings.security.lastUpdated": "{date} {time} 에 마지막으로 변경됨", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Gitlab을 통해 로그인 되었습니다.", diff --git a/webapp/i18n/nl.json b/webapp/i18n/nl.json index 6af442227..3bf8f0177 100644 --- a/webapp/i18n/nl.json +++ b/webapp/i18n/nl.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "Kanaal", "channel_modal.createNew": "Maak nieuw ", "channel_modal.descriptionHelp": "Beschrijf hoe deze {term} gebruikt moet worden.", - "channel_modal.displayNameError": "Dit is een verplicht veld", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "Bewerken", "channel_modal.group": "Groep", "channel_modal.header": "Kop", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>Versturen van berichten</h4><p>Tik hier om een bericht te maken en druk op <strong>Enter</strong> op het te versturen.</p><p>Klik de <strong>Bijlage</strong> knop om een beeld of bestand te uploaden.</p> ", "create_post.write": "Schrijf een bericht...", "create_team.agreement": "Door verder te gaan maak je jouw account en gebruik je {siteName}, je gaat akkoord met onze <a href={TermsOfServiceLink}>Voorwaarden</a> en <a href={PrivacyPolicyLink}>Privacy Policy</a>. Als je niet akkoord gaat, kan je geen gebruik maken van {siteName}.", - "create_team.display_name.back": "Terug naar de vorige stap", "create_team.display_name.charLength": "Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description later.", "create_team.display_name.nameHelp": "Geef uw team een naam – in eender welke taal. De naam van uw team wordt in menu's en kopteksten getoond.", "create_team.display_name.next": "Volgende", @@ -1276,6 +1275,9 @@ "emoji_list.add": "Aangepaste emoji", "emoji_list.creator": "Aangemaakt door", "emoji_list.delete": "Verwijderen", + "emoji_list.delete.confirm.button": "Verwijderen", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "Geen Custom Emoji Gevonden", "emoji_list.header": "Aangepaste emoji", "emoji_list.help": "Aangepaste emoji zijn beschikbaar voor iedereen op jouw server. Type ':' in een bericht venster om het emoji selectie menu zichtbaar te maken. Andere gebruikers zullen de pagina moeten verversen voordat de nieuwe emojis zichtbaar zijn.", @@ -1324,7 +1326,6 @@ "flag_post.flag": "Markeren voor vervolgactie", "flag_post.unflag": "Demarkeer", "general_tab.chooseDescription": "Kies een nieuwe naam voor uw team", - "general_tab.chooseName": "Kies een nieuwe naam voor uw team", "general_tab.codeDesc": "Klik op 'Bewerken' om de uitnodigings-code opnieuw te genereren.", "general_tab.codeLongDesc": "De Uitnodigings Code is onderdeel van de URL in de team uitnodigings link gemaakt door de <strong>Krijg Team Uitnodigings Link\"</strong> in het hoofdmenu. Hergenereren maakt een nieuwe team uitnodiging en zal de vorige link onbruikbaar maken.", "general_tab.codeTitle": "Uitnodigings-code", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "Schakel de navigatiekolom in / uit", "navbar.toggle2": "Schakel de navigatiekolom in / uit", "navbar.viewInfo": "Bekijk Info", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "Over Mattermost", "navbar_dropdown.accountSettings": "Account-instellingen", "navbar_dropdown.console": "Systeem-console", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "Markeer", "post_info.mobile.unflag": "Demarkeer ", "post_info.permalink": "Permanente koppeling", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "Antwoord", "post_info.system": "System", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(edited)", "posts_view.loadMore": "Laad meer berichten", "posts_view.newMsg": "Nieuwe berichten", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "Markeer", "rhs_root.mobile.unflag": "Demarkeer", "rhs_root.permalink": "Permanente koppeling", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "Zoeken", "search_bar.usage": "<h4>Zoekopties</h4><ul><li><span>Gebruik </span><b>\"aanhalingstekens\"</b><span> om naar zinnen te zoeken</span></li><li><span>Gebruik </span><b>from:</b><span> om berichten van een specifieke gebruiker te vinden en </span><b>in:</b><span> om berichten in een specifiek kanaal te vinden</span></li></ul>", "search_header.results": "Zoekresultaten", "search_header.title2": "Recente vermeldingen", "search_header.title3": "Gemarkeerde Berichten", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "Direct Message (with {username})", "search_item.jump": "Spring", "search_results.because": "<ul><li>Wanneer u op zoek bent naar een gedeeltelijk woord (bijv. zoek naar \"tele\", op zoek naar \"telefooon\" of \"televisie\"), moet u een * aan de zoekterm toevoegen</li><li>Zoekopdrachten van 2 letters en algemene woorden zoals \"dit\", \"een\", \"het\" verschijnen niet in de zoekresultaten</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "Je kan berichten en reacties markeren door te klikken op het ", "search_results.usageFlag3": " icoon naast het datum/tijd veld.", "search_results.usageFlag4": "Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "Annuleren", "setting_item_max.save": "Opslaan", "setting_item_min.edit": "Bewerken", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "Stuur mobiele push notificaties", "user.settings.push_notification.status": "Trigger push notificaties wanneer", "user.settings.push_notification.status_info": "Notificatie meldingen worden alleen gepusht naar jouw mobiele device wanneer je online status overeenkomt met de status hierboven.", + "user.settings.security.active": "Active", "user.settings.security.close": "Afsluiten", "user.settings.security.currentPassword": "Huidig wachtwoord", "user.settings.security.currentPasswordError": "Voer uw huidige wachtwoord in", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "E-mail en wachtwoord", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "Inactief", "user.settings.security.lastUpdated": "Laatst bijgewerkt op {date} {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Inloggen via Gitlab", diff --git a/webapp/i18n/pt-BR.json b/webapp/i18n/pt-BR.json index 6b8ec4809..ced02fb09 100644 --- a/webapp/i18n/pt-BR.json +++ b/webapp/i18n/pt-BR.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "Canal", "channel_modal.createNew": "Criar Novo ", "channel_modal.descriptionHelp": "Descreva como este {term} pode ser usado.", - "channel_modal.displayNameError": "Este campo é obrigatório", + "channel_modal.displayNameError": "O nome do canal deve ter com 2 ou mais caracteres", "channel_modal.edit": "Editar", "channel_modal.group": "Grupo", "channel_modal.header": "Cabeçalho", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>Enviando Mensagens</h4><p>Digite aqui para escrever uma mensagem e pressione <strong>ENTER</strong> para posta-lá.</p><p>Clique no botão <strong>Anexo</strong> para enviar uma imagem ou arquivo.</p>", "create_post.write": "Escreva uma mensagem...", "create_team.agreement": "Para prosseguir e criar sua conta e utilizar {siteName}, você deve concordar com o nosso <a href={TermsOfServiceLink}>Termos de Serviço</a> e com a <a href={PrivacyPolicyLink}>Política de Privacidade</a>. Se você não concordar, não poderá utilizar o {siteName}.", - "create_team.display_name.back": "Voltar para o passo anterior", "create_team.display_name.charLength": "O nome deve ter {min} ou mais caracteres até um máximo de {max}. Você pode adicionar uma descrição da equipe depois.", "create_team.display_name.nameHelp": "Nome da sua equipe em qualquer idioma. Seu nome de equipe é mostrado em menus e títulos.", "create_team.display_name.next": "Próximo", @@ -1276,6 +1275,9 @@ "emoji_list.add": "Adicionar Emoji Personalizado", "emoji_list.creator": "Criador", "emoji_list.delete": "Deletar", + "emoji_list.delete.confirm.button": "Deletar", + "emoji_list.delete.confirm.msg": "Esta ação exclui permanentemente o emoji personalizado. Tem certeza de que deseja excluí-lo?", + "emoji_list.delete.confirm.title": "Excluir Emoji Personalizado", "emoji_list.empty": "Emoji Personalizado Não Encontrado", "emoji_list.header": "Emoji Personalizado", "emoji_list.help": "Emojis personalizados estão disponíveis para todos no seu servidor. Digite ':' na caixa de mensagens para trazer o menu de seleção de emojis. Outros usuários podem precisar atualizar a página antes que os novos emojis apareçam.", @@ -1324,7 +1326,6 @@ "flag_post.flag": "Marcar para seguir", "flag_post.unflag": "Desmarcar", "general_tab.chooseDescription": "Por favor escolha uma nova descrição para sua equipe", - "general_tab.chooseName": "Por favor escolha um novo nome para sua equipe", "general_tab.codeDesc": "Clique 'Edit' para re-gerar o Código de Convite.", "general_tab.codeLongDesc": "O Código de convite é usado como parte da URL no link de convite da equipe criado por {getTeamInviteLink} no menu principal. Re-gerar cria um novo link de convite de equipe e invalida os link anteriores.", "general_tab.codeTitle": "Código de Convite", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "Alternar barra lateral", "navbar.toggle2": "Alternar barra lateral", "navbar.viewInfo": "Ver Informações", + "navbar.viewPinnedPosts": "Visualizar Postagens Fixadas", "navbar_dropdown.about": "Sobre o Mattermost", "navbar_dropdown.accountSettings": "Definições de Conta", "navbar_dropdown.console": "Console do Sistema", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "Marcar", "post_info.mobile.unflag": "Desmarcar", "post_info.permalink": "Permalink", + "post_info.pin": "Fixar no canal", + "post_info.pinned": "Fixado", "post_info.reply": "Responder", "post_info.system": "Sistema", + "post_info.unpin": "Desafixar do canal", "post_message_view.edited": "(editado)", "posts_view.loadMore": "Carregar mais mensagens", "posts_view.newMsg": "Novas Mensagens", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "Marcar", "rhs_root.mobile.unflag": "Desmarcar", "rhs_root.permalink": "Permalink", + "rhs_root.pin": "Fixar no canal", + "rhs_root.unpin": "Desafixar do canal", "search_bar.search": "Procurar", "search_bar.usage": "<h4>Opções de Pesquisa</h4><ul><li><span>Utilize </span><b>\"aspas\"</b><span> para pesquisar frases</span></li><li><span>Use </span><b>from:</b><span> para encontrar mensagens de usuários específicos e </span><b>in:</b><span> para encontrar postagens em canais específicos</span></li></ul>", "search_header.results": "Resultados da Pesquisa", "search_header.title2": "Menções Recentes", "search_header.title3": "Posts Marcados", + "search_header.title4": "Postagens fixas em {channelDisplayName}", "search_item.direct": "Mensagem Direta (com {username})", "search_item.jump": "Pular", "search_results.because": "<ul><li>Se você está pesquisando uma parte da frase (ex. pesquisando \"rea\", procurando por \"reagir\" ou \"reação\"), acrescente um * ao seu termo de pesquisa</li><li>Devido ao grande volume de resultados, pesquisas com duas letras e palavras comuns como \"este\", \"um\" e \"é\" não aparecerão nos resultados de pesquisa</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "Você pode marcar uma mensagem e comentários clicando no ", "search_results.usageFlag3": " ícone próximo a data.", "search_results.usageFlag4": "As bandeiras são uma forma de marcar as mensagens para segui-la. Suas bandeiras são pessoais, e não podem ser vistas por outros usuários.", + "search_results.usagePin1": "Não existem postagens fixadas.", + "search_results.usagePin2": "Você pode fixar uma mensagem clicando na opção \"Fixar no canal\" no menu da mensagem.", + "search_results.usagePin3": "Mensagens fixadas são acessíveis por todos os membros do canal e é um jeito de marcar mensagens para futura referência.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "Cancelar", "setting_item_max.save": "Salvar", "setting_item_min.edit": "Editar", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "Enviar notificações push móvel", "user.settings.push_notification.status": "Disparar notificações push quando", "user.settings.push_notification.status_info": "Alertas de notificação só são enviados para o seu dispositivo móvel quando seu status conectado corresponder à seleção acima.", + "user.settings.security.active": "Ativo", "user.settings.security.close": "Fechar", "user.settings.security.currentPassword": "Senha Atual", "user.settings.security.currentPasswordError": "Por favor entre sua senha atual.", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "Email e Senha", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "Inativo", "user.settings.security.lastUpdated": "Última atualização {date} {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Login feito através do GitLab", diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index 954adc8ba..1df6852e0 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "Канал", "channel_modal.createNew": "Создать ", "channel_modal.descriptionHelp": "Опишите, как следует использовать {term}.", - "channel_modal.displayNameError": "Обязательное поле", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "Редактировать", "channel_modal.group": "Группа", "channel_modal.header": "Заголовок", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>Отправка сообщений</h4><p>Напишите здесь сообщение и нажмите <strong>Enter</strong> для отправки.</p><p>Нажмите кнопку <strong>Вложение</strong> для загрузки изображения или файла.</p>", "create_post.write": "Ваше сообщение...", "create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our <a href={TermsOfServiceLink}>Terms of Service</a> and <a href={PrivacyPolicyLink}>Privacy Policy</a>. If you do not agree, you cannot use {siteName}.", - "create_team.display_name.back": "На предыдущий шаг", "create_team.display_name.charLength": "Имя должно быть длиннее {min} и меньше {max} символов. Вы можете добавить описание команды позже.", "create_team.display_name.nameHelp": "Название команды на любом языке. Название команды будет показано в меню и заголовках.", "create_team.display_name.next": "Далее", @@ -1276,6 +1275,9 @@ "emoji_list.add": "Добавить пользовательский смайл", "emoji_list.creator": "Автор", "emoji_list.delete": "Удалить", + "emoji_list.delete.confirm.button": "Удалить", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "Не найдено загруженного эмодзи", "emoji_list.header": "Пользовательские эмодзи", "emoji_list.help": "Пользовательские смайлы доступны для всех на вашем сервере. Введите ':' в окне сообщения, чтобы открыть меню выбора смайла. Другим пользователям, возможно, потребуется обновить страницу, прежде чем появятся новые смайлы.", @@ -1324,7 +1326,6 @@ "flag_post.flag": "Отметить для отслеживания", "flag_post.unflag": "Не помечено", "general_tab.chooseDescription": "Пожалуйста, выберите новое описание для вашей команды", - "general_tab.chooseName": "Введите новое имя для вашей команды", "general_tab.codeDesc": "Нажмите 'Редактировать' для перегенерации кода приглашения.", "general_tab.codeLongDesc": "Код приглашения используется как часть URL в ссылке приглашения в команду, созданная в разделе {getTeamInviteLink} в главном меню. Пересоздание создаст новую ссылку для приглашения и сделает предыдущую недействительной.", "general_tab.codeTitle": "Код приглашения", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "Свернуть меню", "navbar.toggle2": "Развернуть меню", "navbar.viewInfo": "Информация", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "О Mattermost", "navbar_dropdown.accountSettings": "Учетная запись", "navbar_dropdown.console": "Системная консоль", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "Отметить", "post_info.mobile.unflag": "Не помечено", "post_info.permalink": "Постоянная ссылка", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "Ответить", "post_info.system": "System", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(отредактировано)", "posts_view.loadMore": "Больше сообщений", "posts_view.newMsg": "Новые сообщения", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "Отметить", "rhs_root.mobile.unflag": "Не помечено", "rhs_root.permalink": "Постоянная ссылка", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "Поиск", "search_bar.usage": "<h4>Параметры поиска</h4><ul><li><span>Используйте </span><b>\"кавычки\"</b><span> для поиска фраз</span></li><li><span>Используйте слово </span><b>from:</b><span> для поиска сообщений нужного пользователя и слово </span><b>in:</b><span> для поиска сообщений на нужном канале</span></li></ul>", "search_header.results": "Результаты поиска", "search_header.title2": "Недавние упоминания", "search_header.title3": "Отмеченные сообщения", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "Личное сообщение ({username})", "search_item.jump": "Переход", "search_results.because": "<ul><li>Если Вы ищите часть фразы (например, \"rea\", как часть фразы \"reach\" или \"reaction\"), добавьте * в маску поиска.</li><li>Двухбуквенные слова и общие слова, такие как \"this\", \"a\" и \"is\" не появятся в результатах поиска, для исключения лишних результатов.</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "Вы можете добавить флаг в сообщения и комментарии, нажав", "search_results.usageFlag3": " иконка напротив временной метки.", "search_results.usageFlag4": "Флаги - один из способов, пометки сообщений для последующей деятельности. Ваши флаги не могут быть просмотрены другими пользователями.", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "Отмена", "setting_item_max.save": "Сохранить", "setting_item_min.edit": "Изменить", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "Отправить мобильное push-уведомление", "user.settings.push_notification.status": "Отправить push-уведомление когда", "user.settings.push_notification.status_info": "Уведомления на телефон будут присылаться только если ваш статус будет совпадать с выбранным выше.", + "user.settings.security.active": "Active", "user.settings.security.close": "Закрыть", "user.settings.security.currentPassword": "Текущий пароль", "user.settings.security.currentPasswordError": "Введите текущий пароль", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "Email и Пароль", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "Неактивен", "user.settings.security.lastUpdated": "Последние изменения: {date} в {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "Вход выполнен с помощью GitLab", diff --git a/webapp/i18n/zh-CN.json b/webapp/i18n/zh-CN.json index 9212c9e7c..d5482fc10 100644 --- a/webapp/i18n/zh-CN.json +++ b/webapp/i18n/zh-CN.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "频道", "channel_modal.createNew": "创建新", "channel_modal.descriptionHelp": "描述{term}如何被使用。", - "channel_modal.displayNameError": "此栏必须填写", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "编辑", "channel_modal.group": "群组", "channel_modal.header": "标题", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>发送信息</h4><p>在这输入信息并按<strong>回车</strong>发送。</p><p>点击<strong>附件</strong>按钮上传图片或文件。</p>", "create_post.write": "写一个消息...", "create_team.agreement": "如果继续创建您的帐户和使用{siteName},您需要同意<a href={TermsOfServiceLink}>服务条款</a>和<a href={PrivacyPolicyLink}>隐私政策</a>。如果不同意,您将不能使用{siteName}。", - "create_team.display_name.back": "返回上一步", "create_team.display_name.charLength": "名称必须在 {min} 于 {max} 个字符之间。您可以之后添加更长的团队描述。", "create_team.display_name.nameHelp": "您可以使用任何语言命名您的团队。您的团队名称将显示在菜单和标题栏上。", "create_team.display_name.next": "下一步", @@ -1276,6 +1275,9 @@ "emoji_list.add": "添加自定义表情符", "emoji_list.creator": "创建者", "emoji_list.delete": "删除", + "emoji_list.delete.confirm.button": "删除", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "未找到自定义表情符", "emoji_list.header": "自定义表情", "emoji_list.help": "自定义表情符对所有在您服务器上的用户开放。在信息框输入 ':' 会显示表情符选择菜单。其他用户可能需要刷新页面才能看见新的表情符。", @@ -1324,7 +1326,6 @@ "flag_post.flag": "标记以跟进", "flag_post.unflag": "取消标记", "general_tab.chooseDescription": "请为您的团队选个新的描述", - "general_tab.chooseName": "请选择一个新的名称为你的团队", "general_tab.codeDesc": "点击 \"编辑\" 重新生成邀请码。", "general_tab.codeLongDesc": "作为团队邀请链接中URL的一部分,邀请码在主菜单中由 {getTeamInviteLink} 创建。重新生成创建一个新的团队邀请链接将使之前的链接无效。", "general_tab.codeTitle": "邀请码", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "切换侧边栏", "navbar.toggle2": "切换侧边栏", "navbar.viewInfo": "查看信息", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "关于Mattermost", "navbar_dropdown.accountSettings": "账户设置", "navbar_dropdown.console": "系统控制台", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "标记", "post_info.mobile.unflag": "取消标记", "post_info.permalink": "永久链接", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "回复", "post_info.system": "系统", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(已编辑)", "posts_view.loadMore": "载入更多消息", "posts_view.newMsg": "新消息", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "标记", "rhs_root.mobile.unflag": "取消标记", "rhs_root.permalink": "永久链接", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "搜索", "search_bar.usage": "<h4>搜索选项</h4><ul><li><span>使用</span><b>\"双引号\"</b><span>搜索词组</span></li><li><span>使用</span><b>from:</b><span>查找来自特定用户的信息,使用</span><b>in:</b><span>查找特定频道的信息</span></li></ul>", "search_header.results": "搜索结果", "search_header.title2": "最近提及", "search_header.title3": "已标记的信息", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "私信 (与 {username})", "search_item.jump": "跳转", "search_results.because": "<ul><li>如果您需搜索一个部分词组(例如搜索 \"rea\",查找 \"reach\" 或 \"reaction\"),请在结尾附上 *。</li><li>由于搜索结果的数量限制,双字母的搜索和 \"this\",\"a\" 及 \"is\" 等常用词不会出现在搜索结果中。</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "您可以点击时间戳旁的", "search_results.usageFlag3": "图标来标记信息和评论。", "search_results.usageFlag4": "标记信息以便之后更进。您的标记是个人的,不会被他人看到。", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "取消", "setting_item_max.save": "保存", "setting_item_min.edit": "编辑", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "发送手机推送", "user.settings.push_notification.status": "触发推送通知当", "user.settings.push_notification.status_info": "通知只有在您的在线状态符合以上选择时才发送推送。", + "user.settings.security.active": "Active", "user.settings.security.close": "关闭", "user.settings.security.currentPassword": "当前密码", "user.settings.security.currentPasswordError": "请输入您当前密码。", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "邮箱和密码", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "谷歌", + "user.settings.security.inactive": "停用", "user.settings.security.lastUpdated": "上次更新时间{date}{time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "用 GitLab 登录", diff --git a/webapp/i18n/zh-TW.json b/webapp/i18n/zh-TW.json index d9c0cadae..3dc4f4941 100644 --- a/webapp/i18n/zh-TW.json +++ b/webapp/i18n/zh-TW.json @@ -1118,7 +1118,7 @@ "channel_modal.channel": "頻道", "channel_modal.createNew": "建立新的", "channel_modal.descriptionHelp": "說明{term}如何使用。", - "channel_modal.displayNameError": "此欄位是必需的", + "channel_modal.displayNameError": "Channel name must be 2 or more characters", "channel_modal.edit": "編輯", "channel_modal.group": "群組", "channel_modal.header": "標題", @@ -1208,7 +1208,6 @@ "create_post.tutorialTip": "<h4>發送訊息</h4><p>在這邊輸入訊息並按下 <strong>Enter</strong> 來發文。</p><p>按下<strong>附件</strong>按鈕來上傳圖片或是檔案。</p>", "create_post.write": "輸入訊息...", "create_team.agreement": "一旦建立帳號使用{siteName},即表示您同意<a href={TermsOfServiceLink}>服務條款</a>以及<a href={PrivacyPolicyLink}>隱私政策</a>。如果您不同意,請停止使用{siteName}。", - "create_team.display_name.back": "回到上一步", "create_team.display_name.charLength": "名字必須至少有{min}個字、最多{max}。等等有增加較長團隊敘述的方法。", "create_team.display_name.nameHelp": "團隊可以用任何語言取名。團隊名稱將會顯示在選單跟畫面上方。", "create_team.display_name.next": "下一步", @@ -1276,6 +1275,9 @@ "emoji_list.add": "新增自訂繪文字", "emoji_list.creator": "建立者", "emoji_list.delete": "刪除", + "emoji_list.delete.confirm.button": "刪除", + "emoji_list.delete.confirm.msg": "This action permanently deletes the custom emoji. Are you sure you want to delete it?", + "emoji_list.delete.confirm.title": "Delete Custom Emoji", "emoji_list.empty": "沒有自訂繪文字", "emoji_list.header": "自訂繪文字", "emoji_list.help": "自訂繪文字對此伺服器上的所有使用者開放。在訊息輸入框輸入 ':' 會叫出繪文字選單。其他使用者可能需要重新讀取頁面才會看見新的繪文字。", @@ -1324,7 +1326,6 @@ "flag_post.flag": "標記以追蹤", "flag_post.unflag": "取消標記", "general_tab.chooseDescription": "為團隊寫新的敘述", - "general_tab.chooseName": "為團隊取名", "general_tab.codeDesc": "按下'修改'來重新產生邀請碼。", "general_tab.codeLongDesc": "邀請碼用來當作主選單中{getTeamInviteLink}所產生的團隊邀請連結的一部分。重新產生會建立一個新的招待連結並且讓舊的連結失效。", "general_tab.codeTitle": "邀請碼", @@ -1665,6 +1666,7 @@ "navbar.toggle1": "切換側邊欄", "navbar.toggle2": "切換側邊欄", "navbar.viewInfo": "檢視資訊", + "navbar.viewPinnedPosts": "View Pinned Posts", "navbar_dropdown.about": "關於 Mattermost", "navbar_dropdown.accountSettings": "帳號設定", "navbar_dropdown.console": "系統控制台", @@ -1719,8 +1721,11 @@ "post_info.mobile.flag": "標記", "post_info.mobile.unflag": "取消標記", "post_info.permalink": "永久網址", + "post_info.pin": "Pin to channel", + "post_info.pinned": "Pinned", "post_info.reply": "回覆", "post_info.system": "系統", + "post_info.unpin": "Un-pin from channel", "post_message_view.edited": "(被編輯過)", "posts_view.loadMore": "載入更多訊息", "posts_view.newMsg": "新訊息", @@ -1773,11 +1778,14 @@ "rhs_root.mobile.flag": "標記", "rhs_root.mobile.unflag": "取消標記", "rhs_root.permalink": "永久網址", + "rhs_root.pin": "Pin to channel", + "rhs_root.unpin": "Un-pin from channel", "search_bar.search": "搜尋", "search_bar.usage": "<h4>搜尋選項</h4><ul><li><span>用</span><b>\"雙引號\"</b><span>來搜尋語句</span></li><li><span>用</span><b>from:</b><span>來搜尋特定使用者的訊息,用</span><b>in:</b><span>來搜尋特定頻道</span></li></ul>", "search_header.results": "搜尋結果", "search_header.title2": "最近提及", "search_header.title3": "被標記的訊息", + "search_header.title4": "Pinned posts in {channelDisplayName}", "search_item.direct": "直接訊息 (與{username})", "search_item.jump": "跳至", "search_results.because": "<ul><li>如果要搜尋部份語句(如搜尋\"rea\"以尋找\"reach\"或\"reaction\"),請在搜尋詞尾附上*。</li><li>為了減少收尋結果,兩個字母的搜尋跟常用字像\"this\"、\"a\"跟\"is\"不會顯示在結果當中。</li></ul>", @@ -1787,6 +1795,10 @@ "search_results.usageFlag2": "可以藉由按下位於時間戳記旁邊的這 ", "search_results.usageFlag3": " 圖示來標記訊息跟註解。", "search_results.usageFlag4": "標記是標注訊息以追蹤後續的功能。您的標記是屬於個人的,不會被其他使用者看到。", + "search_results.usagePin1": "There are no pinned messages yet.", + "search_results.usagePin2": "All members of this channel can pin important or useful messages.", + "search_results.usagePin3": "Pinned messages are visible to all channel members.", + "search_results.usagePin4": "To pin a message: Go to the message that you want to pin and click [...] > \"Pin to channel\".", "setting_item_max.cancel": "取消", "setting_item_max.save": "儲存", "setting_item_min.edit": "編輯", @@ -2181,6 +2193,7 @@ "user.settings.push_notification.send": "發送行動推播通知", "user.settings.push_notification.status": "何時觸發推播通知", "user.settings.push_notification.status_info": "只有在上線狀態符合上面的選項時才會發送通知到行動裝置上。", + "user.settings.security.active": "Active", "user.settings.security.close": "關閉", "user.settings.security.currentPassword": "目前的密碼", "user.settings.security.currentPasswordError": "請輸入原先的密碼。", @@ -2188,6 +2201,7 @@ "user.settings.security.emailPwd": "電子郵件跟密碼", "user.settings.security.gitlab": "GitLab", "user.settings.security.google": "Google", + "user.settings.security.inactive": "停用", "user.settings.security.lastUpdated": "最後一次更新於 {date} {time}", "user.settings.security.ldap": "AD/LDAP", "user.settings.security.loginGitlab": "已經由 GitLab 登入", diff --git a/webapp/package.json b/webapp/package.json index 216292f34..f0b1b8bb2 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -4,15 +4,15 @@ "version": "0.0.1", "private": true, "dependencies": { - "autolinker": "1.4.0", + "autolinker": "1.4.2", "bootstrap": "3.3.7", - "bootstrap-colorpicker": "2.3.6", - "chart.js": "2.4.0", + "bootstrap-colorpicker": "2.5.1", + "chart.js": "2.5.0", "compass-mixins": "0.12.10", "fastclick": "1.0.6", "flux": "3.1.2", "font-awesome": "4.7.0", - "highlight.js": "9.9.0", + "highlight.js": "9.10.0", "inobounce": "0.1.4", "intl": "1.2.5", "jasny-bootstrap": "3.1.3", @@ -20,45 +20,45 @@ "marked": "mattermost/marked#8f5902fff9bad793cd6c66e0c44002c9e79e1317", "match-at": "0.1.0", "object-assign": "4.1.1", - "pdfjs-dist": "1.7.235", + "pdfjs-dist": "1.7.363", "perfect-scrollbar": "0.6.16", "react": "15.4.2", "react-addons-pure-render-mixin": "15.4.2", - "react-bootstrap": "0.30.7", - "react-custom-scrollbars": "4.0.1", + "react-bootstrap": "0.30.8", + "react-custom-scrollbars": "4.0.2", "react-dom": "15.4.2", "react-intl": "2.2.3", "react-router": "2.8.1", - "react-select": "1.0.0-rc.2", - "superagent": "3.4.1", - "twemoji": "2.2.3", - "velocity-animate": "1.4.2", - "webrtc-adapter": "3.1.0", + "react-select": "1.0.0-rc.3", + "superagent": "3.5.0", + "twemoji": "2.2.5", + "velocity-animate": "1.4.3", + "webrtc-adapter": "3.2.0", "xregexp": "3.1.1" }, "devDependencies": { - "babel-core": "6.22.1", - "babel-eslint": "7.1.0", - "babel-loader": "6.2.10", - "babel-plugin-transform-runtime": "6.22.0", - "babel-polyfill": "6.22.0", - "babel-preset-es2015": "6.22.0", - "babel-preset-react": "6.22.0", + "babel-core": "6.24.0", + "babel-eslint": "7.1.1", + "babel-loader": "6.4.0", + "babel-plugin-transform-runtime": "6.23.0", + "babel-polyfill": "6.23.0", + "babel-preset-es2015": "6.24.0", + "babel-preset-react": "6.23.0", "babel-preset-stage-0": "6.22.0", "copy-webpack-plugin": "4.0.1", - "cross-env": "3.1.4", - "css-loader": "0.26.1", - "eslint": "3.10.2", - "eslint-plugin-react": "6.7.1", - "exports-loader": "0.6.3", - "extract-text-webpack-plugin": "1.0.1", - "file-loader": "0.10.0", - "html-loader": "0.4.4", + "cross-env": "3.2.3", + "css-loader": "0.27.3", + "eslint": "3.17.1", + "eslint-plugin-react": "6.10.0", + "exports-loader": "0.6.4", + "extract-text-webpack-plugin": "2.1.0", + "file-loader": "0.10.1", + "html-loader": "0.4.5", "html-webpack-plugin": "2.28.0", "image-webpack-loader": "3.2.0", - "imports-loader": "0.7.0", + "imports-loader": "0.7.1", "jquery-deferred": "0.3.1", - "jsdom": "9.9.1", + "jsdom": "9.12.0", "jsdom-global": "2.1.1", "json-loader": "0.5.4", "mocha": "3.2.0", @@ -67,9 +67,9 @@ "node-sass": "4.5.0", "raw-loader": "0.5.1", "react-addons-test-utils": "15.4.2", - "sass-loader": "4.1.1", - "style-loader": "0.13.1", - "url-loader": "0.5.7", + "sass-loader": "6.0.3", + "style-loader": "0.13.2", + "url-loader": "0.5.8", "webpack": "2.2.1", "webpack-node-externals": "1.5.4" }, diff --git a/webapp/sass/base/_typography.scss b/webapp/sass/base/_typography.scss index 1d3f1d052..a9bc183c4 100644 --- a/webapp/sass/base/_typography.scss +++ b/webapp/sass/base/_typography.scss @@ -26,6 +26,10 @@ body { word-break: break-all; } +.whitespace--nowrap { + white-space: nowrap; +} + .overflow--ellipsis { overflow: hidden; text-overflow: ellipsis; diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss index 93bd9fda4..bfc082ad3 100644 --- a/webapp/sass/components/_modal.scss +++ b/webapp/sass/components/_modal.scss @@ -345,7 +345,7 @@ } img { - max-height: 100%; + max-height: calc(100vh - 200px); max-width: 100%; } diff --git a/webapp/sass/components/_popover.scss b/webapp/sass/components/_popover.scss index 6b1c57725..93b567ad3 100644 --- a/webapp/sass/components/_popover.scss +++ b/webapp/sass/components/_popover.scss @@ -209,6 +209,15 @@ .more-modal__row { min-height: inherit; } + + .more-modal__details { + line-height: 32px; + } + + .more-modal__actions { + line-height: 31px; + margin: 0; + } } .popover-content { diff --git a/webapp/sass/components/_tooltip.scss b/webapp/sass/components/_tooltip.scss index 0049fe1b8..6953dad58 100644 --- a/webapp/sass/components/_tooltip.scss +++ b/webapp/sass/components/_tooltip.scss @@ -7,6 +7,12 @@ padding: 3px 10px 4px; word-break: break-word; } + + &.text-nowrap { + .tooltip-inner { + white-space: nowrap; + } + } } #webrtcTooltip { diff --git a/webapp/sass/layout/_content.scss b/webapp/sass/layout/_content.scss index 02f063573..b6fe98eb4 100644 --- a/webapp/sass/layout/_content.scss +++ b/webapp/sass/layout/_content.scss @@ -9,10 +9,20 @@ .search-btns { display: none; } - .header-list__members { + .header-list__right { + // the negative margin-right is used + // to prevent the icons in the header from + // moving to the left when the RHS is open + // + // the below z-index is used to ensure the icons + // stays on the top and don't get hidden by the + // search's input block + position: relative; + z-index: 6; + margin-right: -18px; - float: right; padding-right: 0px !important; + float: right; } } @@ -23,10 +33,20 @@ .search-btns { display: none; } - .header-list__members { + .header-list__right { + // the negative margin-right is used + // to prevent the icons in the header from + // moving to the left when the RHS is open + // + // the below z-index is used to ensure the icons + // stays on the top and don't get hidden by the + // search's input block + position: relative; + z-index: 6; + margin-right: -18px; - float: right; padding-right: 0px !important; + float: right } } } diff --git a/webapp/sass/layout/_forms.scss b/webapp/sass/layout/_forms.scss index 7552290d8..64c74b0a5 100644 --- a/webapp/sass/layout/_forms.scss +++ b/webapp/sass/layout/_forms.scss @@ -62,7 +62,6 @@ .has-error { .help-block, - .control-label, .radio, .checkbox, .radio-inline, @@ -70,6 +69,10 @@ color: $red; } + .control-label { + color: inherit; + } + &.radio, &.checkbox, &.radio-inline, diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss index 8ee6e8fdc..f8211d433 100644 --- a/webapp/sass/layout/_headers.scss +++ b/webapp/sass/layout/_headers.scss @@ -7,26 +7,43 @@ line-height: 56px; width: 100%; - .member-popover__trigger { + .member-popover__trigger, + .pinned-posts-button { cursor: pointer; - min-width: 60px; - padding-right: 10px; - text-align: right; + display: inline-block; + margin-left: 7px; + min-width: 30px; + text-align: center; white-space: nowrap; .fa { font-size: 16px; + } + } + + .member-popover__container, + .member-popover__trigger { + display: inline; + } + + .member-popover__trigger { + .fa { margin-left: 4px; } } + .pinned-posts-button svg { + position: relative; + top: 2px; + } + &.alt { margin: 0; th { font-weight: normal !important; - &.header-list__members { + &.header-list__right { padding-right: 4px; } } @@ -48,7 +65,7 @@ } &:last-child { - padding-right: 8px; + padding-right: 6px; width: 8.9%; } } diff --git a/webapp/sass/layout/_post-right.scss b/webapp/sass/layout/_post-right.scss index 455ed7fff..9a0f658a2 100644 --- a/webapp/sass/layout/_post-right.scss +++ b/webapp/sass/layout/_post-right.scss @@ -53,6 +53,12 @@ border: none; } + .date-separator { + hr { + border-top: 1px solid #eee; + } + } + .post-create__container { width: 100%; @@ -147,7 +153,8 @@ @include flex(1 1 auto); overflow: auto; position: relative; - + padding-top: 10px; + .file-preview__container { margin-top: 5px; } diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss index 5ecd50468..1e1dd4b08 100644 --- a/webapp/sass/layout/_post.scss +++ b/webapp/sass/layout/_post.scss @@ -359,6 +359,9 @@ } .post-create__container { + label { + font-weight: normal; + } .custom-textarea { overflow: hidden; } @@ -763,6 +766,7 @@ line-height: 1.6em; margin: 0; white-space: pre-wrap; + word-break: break-word; } .post__header--info { @@ -800,7 +804,7 @@ .flag-icon__container { left: 36px; - margin-left: 5px; + margin-left: 7px; position: absolute; top: 8px; } @@ -1357,15 +1361,25 @@ } } -.bot-indicator { +.bot-indicator, +.post__pinned-badge { border-radius: 2px; font-family: inherit; font-size: 10px; font-weight: 600; - margin: 2px 10px 0 -4px; padding: 1px 4px; } +.bot-indicator { + margin: 2px 10px 0 -4px; +} + +.post__pinned-badge { + margin-left: 7px; + position: relative; + top: -1px; +} + .permalink-text { overflow: hidden; } diff --git a/webapp/sass/layout/_webhooks.scss b/webapp/sass/layout/_webhooks.scss index f3a8c6fd3..79072375f 100644 --- a/webapp/sass/layout/_webhooks.scss +++ b/webapp/sass/layout/_webhooks.scss @@ -41,6 +41,7 @@ &.attachment--opengraph { max-width: 800px; } + .attachment__content { border-radius: 4px; border-style: solid; @@ -71,16 +72,21 @@ &.attachment__container--danger { border-left-color: #e40303; } + &.attachment__container--opengraph { + padding-left: 5px; + border-left-style: none; + border-left-color: transparent; display: table; - table-layout: fixed; - width: 100%; margin: 0; padding-bottom: 13px; + width: 100%; + div { margin: 0; } } + .sitename { color: #A3A3A3; } @@ -89,8 +95,8 @@ .attachment__body__wrap { &.attachment__body__wrap--opengraph { display: table-cell; - width: 100%; vertical-align: top; + width: 100%; } } @@ -104,6 +110,7 @@ &.attachment__body--no_thumb { width: 100%; } + &.attachment__body--opengraph { float: none; padding-right: 0; @@ -142,6 +149,7 @@ margin-top: 10px; max-height: 200px; max-width: 400px; + width: 100%; &.loading { height: 150px; @@ -164,16 +172,17 @@ &.has-link { color: #2f81b7; - text-overflow: ellipsis; overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; } &.attachment__title--opengraph { height: auto; word-wrap: break-word; + &.is-url { - word-break: break-all + word-break: break-all; } } } diff --git a/webapp/sass/responsive/_desktop.scss b/webapp/sass/responsive/_desktop.scss index 891431f20..f671104e1 100644 --- a/webapp/sass/responsive/_desktop.scss +++ b/webapp/sass/responsive/_desktop.scss @@ -76,6 +76,23 @@ } } } + + &.move--left { + .post { + &.post--root, + &.other--root { + .post__header { + padding-right: 70px; + } + } + + &.post--comment { + .post__header { + padding-right: 70px; + } + } + } + } } } diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index 891b0ed48..4fbec082a 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -1,6 +1,10 @@ @charset 'UTF-8'; @media screen and (max-width: 768px) { + .table-responsive { + border: none; + } + .multi-select__container { .btn { display: block; @@ -253,6 +257,7 @@ } } } + blockquote { margin-top: 0; } @@ -274,6 +279,7 @@ .post__header { margin-bottom: 0; + padding-right: 70px; .col__reply { top: -3px; @@ -1342,7 +1348,7 @@ a { border-bottom: 1px solid; - line-height: 50px; + line-height: 45px; position: relative; text-align: center; } diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss index 06a725a31..3bafc38d4 100644 --- a/webapp/sass/responsive/_tablet.scss +++ b/webapp/sass/responsive/_tablet.scss @@ -127,6 +127,15 @@ top: auto; } } + + &.move--left, + &.webrtc--show, + &.move--right { + .header-list__right { + // hide it behind the RHS + z-index: -1; + } + } } .post { .attachment { @@ -182,6 +191,14 @@ } } } + + .sidebar--right__title { + display: inline-block; + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } .inner-wrap { @@ -213,6 +230,11 @@ } } + .post__pinned-badge { + margin-left: 0; + margin-right: 5px; + } + &:not(.post--thread) { padding: 5px .5em 0 77px; @@ -359,9 +381,16 @@ } .post__header { + float: left; + padding-top: 3px; + .col__reply { top: -21px; } + + .post__pinned-badge { + margin-right: 5px; + } } &:not(.post--compact) { @@ -381,6 +410,12 @@ } } } + + &.post--comment:not(.post--compact) { + .post__pinned-badge { + margin-left: 10px; + } + } } } } diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx index 6e312f67a..6f81619c2 100644 --- a/webapp/stores/post_store.jsx +++ b/webapp/stores/post_store.jsx @@ -16,6 +16,7 @@ const FOCUSED_POST_CHANGE = 'focused_post_change'; const EDIT_POST_EVENT = 'edit_post'; const POSTS_VIEW_JUMP_EVENT = 'post_list_jump'; const SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; +const POST_PINNED_CHANGE_EVENT = 'post_pinned_change'; class PostStoreClass extends EventEmitter { constructor() { @@ -259,22 +260,42 @@ class PostStoreClass extends EventEmitter { this.postsInfo[id].postList = combinedPosts; } + focusedPostListHasPost(id) { + const focusedPostId = this.getFocusedPostId(); + if (focusedPostId == null) { + return false; + } + + const focusedPostList = makePostListNonNull(this.getAllPosts(focusedPostId)); + return focusedPostList.posts.hasOwnProperty(id); + } + storePost(post, isNewPost = false) { - const postList = makePostListNonNull(this.getAllPosts(post.channel_id)); + const ids = [ + post.channel_id + ]; - if (post.pending_post_id !== '') { - this.removePendingPost(post.channel_id, post.pending_post_id); + // update the post in the permalink view if it's there + if (!isNewPost && this.focusedPostListHasPost(post.id)) { + ids.push(this.getFocusedPostId()); } - post.pending_post_id = ''; + ids.forEach((id) => { + const postList = makePostListNonNull(this.getAllPosts(id)); + if (post.pending_post_id !== '') { + this.removePendingPost(post.channel_id, post.pending_post_id); + } - postList.posts[post.id] = post; - if (isNewPost && postList.order.indexOf(post.id) === -1) { - postList.order.unshift(post.id); - } + post.pending_post_id = ''; + + postList.posts[post.id] = post; + if (isNewPost && postList.order.indexOf(post.id) === -1) { + postList.order.unshift(post.id); + } - this.makePostsInfo(post.channel_id); - this.postsInfo[post.channel_id].postList = postList; + this.makePostsInfo(post.channel_id); + this.postsInfo[id].postList = postList; + }); } storeFocusedPost(postId, channelId, postList) { @@ -500,6 +521,18 @@ class PostStoreClass extends EventEmitter { this.removeListener(SELECTED_POST_CHANGE_EVENT, callback); } + emitPostPinnedChange() { + this.emit(POST_PINNED_CHANGE_EVENT); + } + + addPostPinnedChangeListener(callback) { + this.on(POST_PINNED_CHANGE_EVENT, callback); + } + + removePostPinnedChangeListener(callback) { + this.removeListener(POST_PINNED_CHANGE_EVENT, callback); + } + getCurrentUsersLatestPost(channelId, rootId) { const userId = UserStore.getCurrentId(); @@ -686,6 +719,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { PostStore.storeSelectedPostId(action.postId); PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts); break; + case ActionTypes.RECEIVED_POST_PINNED: + case ActionTypes.RECEIVED_POST_UNPINNED: + PostStore.emitPostPinnedChange(); + break; default: } }); diff --git a/webapp/stores/search_store.jsx b/webapp/stores/search_store.jsx index 46a086ddb..49f8b3c2f 100644 --- a/webapp/stores/search_store.jsx +++ b/webapp/stores/search_store.jsx @@ -19,6 +19,7 @@ class SearchStoreClass extends EventEmitter { this.searchResults = null; this.isMentionSearch = false; this.isFlaggedPosts = false; + this.isPinnedPosts = false; this.isVisible = false; this.searchTerm = ''; } @@ -83,6 +84,10 @@ class SearchStoreClass extends EventEmitter { return this.isFlaggedPosts; } + getIsPinnedPosts() { + return this.isPinnedPosts; + } + storeSearchTerm(term) { this.searchTerm = term; } @@ -91,10 +96,11 @@ class SearchStoreClass extends EventEmitter { return this.searchTerm; } - storeSearchResults(results, isMentionSearch, isFlaggedPosts) { + storeSearchResults(results, isMentionSearch, isFlaggedPosts, isPinnedPosts) { this.searchResults = results; this.isMentionSearch = isMentionSearch; this.isFlaggedPosts = isFlaggedPosts; + this.isPinnedPosts = isPinnedPosts; } deletePost(post) { @@ -120,7 +126,7 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { switch (action.type) { case ActionTypes.RECEIVED_SEARCH: - SearchStore.storeSearchResults(action.results, action.is_mention_search, action.is_flagged_posts); + SearchStore.storeSearchResults(action.results, action.is_mention_search, action.is_flagged_posts, action.is_pinned_posts); SearchStore.emitSearchChange(); break; case ActionTypes.RECEIVED_SEARCH_TERM: diff --git a/webapp/tests/formatting_hashtags.test.jsx b/webapp/tests/formatting_hashtags.test.jsx index 657dcf69d..cfda9aaa2 100644 --- a/webapp/tests/formatting_hashtags.test.jsx +++ b/webapp/tests/formatting_hashtags.test.jsx @@ -11,7 +11,7 @@ describe('TextFormatting.Hashtags', function() { it('Not hashtags', function(done) { assert.equal( TextFormatting.formatText('# hashtag').trim(), - '<h1 id="hashtag" class="markdown__heading">hashtag</h1>' + '<h1 class="markdown__heading">hashtag</h1>' ); assert.equal( diff --git a/webapp/tests/formatting_imgs.test.jsx b/webapp/tests/formatting_imgs.test.jsx new file mode 100644 index 000000000..604472671 --- /dev/null +++ b/webapp/tests/formatting_imgs.test.jsx @@ -0,0 +1,55 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import assert from 'assert'; + +import * as Markdown from 'utils/markdown.jsx'; + +describe('Markdown.Imgs', function() { + this.timeout(10000); + + it('Inline mage', function(done) { + assert.equal( + Markdown.format('![Mattermost](/images/icon.png)').trim(), + '<p><img src="/images/icon.png" alt="Mattermost" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"></p>' + ); + + done(); + }); + + it('Image with hover text', function(done) { + assert.equal( + Markdown.format('![Mattermost](/images/icon.png "Mattermost Icon")').trim(), + '<p><img src="/images/icon.png" alt="Mattermost" title="Mattermost Icon" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"></p>' + ); + + done(); + }); + + it('Image with link', function(done) { + assert.equal( + Markdown.format('[![Mattermost](../../images/icon-76x76.png)](https://github.com/mattermost/platform)').trim(), + '<p><a class="theme markdown__link" href="https://github.com/mattermost/platform" rel="noreferrer" target="_blank"><img src="../../images/icon-76x76.png" alt="Mattermost" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"></a></p>' + ); + + done(); + }); + + it('Image with width and height', function(done) { + assert.equal( + Markdown.format('![Mattermost](../../images/icon-76x76.png =50x76 "Mattermost Icon")').trim(), + '<p><img src="../../images/icon-76x76.png" alt="Mattermost" title="Mattermost Icon" width="50" height="76" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"></p>' + ); + + done(); + }); + + it('Image with width', function(done) { + assert.equal( + Markdown.format('![Mattermost](../../images/icon-76x76.png =50 "Mattermost Icon")').trim(), + '<p><img src="../../images/icon-76x76.png" alt="Mattermost" title="Mattermost Icon" width="50" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"></p>' + ); + + done(); + }); +}); diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 9ba853238..4afd1cc20 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -85,7 +85,7 @@ export function getChannels() { (err) => { callTracker.getChannels = 0; dispatchError(err, 'getChannels'); - reject(); + reject(new Error('Unable to getChannels')); } ); }); @@ -137,7 +137,7 @@ export function getMyChannelMembers() { (err) => { callTracker.getMyChannelMembers = 0; dispatchError(err, 'getMyChannelMembers'); - reject(); + reject(new Error('Unable to getMyChannelMembers')); } ); }); @@ -166,7 +166,7 @@ export function getMyChannelMembersForTeam(teamId) { (err) => { callTracker[`getMyChannelMembers${teamId}`] = 0; dispatchError(err, 'getMyChannelMembersForTeam'); - reject(); + reject(new Error('Unable to getMyChannelMembersForTeam')); } ); }); @@ -308,7 +308,7 @@ export function getChannelMember(channelId, userId) { (err) => { callTracker[`getChannelMember${channelId}${userId}`] = 0; dispatchError(err, 'getChannelMember'); - reject(); + reject(new Error('Unable to getChannelMeber')); } ); }); @@ -1612,6 +1612,40 @@ export function deleteEmoji(id) { ); } +export function pinPost(channelId, reaction) { + Client.pinPost( + channelId, + reaction, + () => { + // the "post_edited" websocket event take cares of updating the posts + // the action below is mostly dispatched for the RHS to update + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_PINNED + }); + }, + (err) => { + dispatchError(err, 'pinPost'); + } + ); +} + +export function unpinPost(channelId, reaction) { + Client.unpinPost( + channelId, + reaction, + () => { + // the "post_edited" websocket event take cares of updating the posts + // the action below is mostly dispatched for the RHS to update + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_UNPINNED + }); + }, + (err) => { + dispatchError(err, 'unpinPost'); + } + ); +} + export function saveReaction(channelId, reaction) { Client.saveReaction( channelId, diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 541fb48ec..d8fc169a3 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -90,6 +90,8 @@ export const ActionTypes = keyMirror({ RECEIVED_POST_SELECTED: null, RECEIVED_MENTION_DATA: null, RECEIVED_ADD_MENTION: null, + RECEIVED_POST_PINNED: null, + RECEIVED_POST_UNPINNED: null, RECEIVED_PROFILES: null, RECEIVED_PROFILES_IN_TEAM: null, @@ -419,6 +421,7 @@ export const Constants = { REPLY_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-158 242 18 18' style='enable-background:new -158 242 18 18;' xml:space='preserve'> <path d='M-142.2,252.6c-2-3-4.8-4.7-8.3-4.8v-3.3c0-0.2-0.1-0.3-0.2-0.3s-0.3,0-0.4,0.1l-6.9,6.2c-0.1,0.1-0.1,0.2-0.1,0.3 c0,0.1,0,0.2,0.1,0.3l6.9,6.4c0.1,0.1,0.3,0.1,0.4,0.1c0.1-0.1,0.2-0.2,0.2-0.4v-3.8c4.2,0,7.4,0.4,9.6,4.4c0.1,0.1,0.2,0.2,0.3,0.2 c0,0,0.1,0,0.1,0c0.2-0.1,0.3-0.3,0.2-0.4C-140.2,257.3-140.6,255-142.2,252.6z M-150.8,252.5c-0.2,0-0.4,0.2-0.4,0.4v3.3l-6-5.5 l6-5.3v2.8c0,0.2,0.2,0.4,0.4,0.4c3.3,0,6,1.5,8,4.5c0.5,0.8,0.9,1.6,1.2,2.3C-144,252.8-147.1,252.5-150.8,252.5z'/> </svg>", SCROLL_BOTTOM_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-239 239 21 23' style='enable-background:new -239 239 21 23;' xml:space='preserve'> <path d='M-239,241.4l2.4-2.4l8.1,8.2l8.1-8.2l2.4,2.4l-10.5,10.6L-239,241.4z M-228.5,257.2l8.1-8.2l2.4,2.4l-10.5,10.6l-10.5-10.6 l2.4-2.4L-228.5,257.2z'/> </svg>", VIDEO_ICON: "<svg width='55%'height='100%'viewBox='0 0 13 8'> <g transform='matrix(1,0,0,1,-507,-146)'> <g transform='matrix(0.0133892,0,0,0.014499,500.635,142.838)'> <path d='M1158,547.286L1158,644.276C1158,684.245 1125.55,716.694 1085.58,716.694L579.341,716.694C539.372,716.694 506.922,684.245 506.922,644.276L506.922,306.322C506.922,266.353 539.371,233.904 579.341,233.903L1085.58,233.903C1125.55,233.904 1158,266.353 1158,306.322L1158,402.939L1359.75,253.14C1365.83,248.362 1373.43,245.973 1382.56,245.973C1386.61,245.973 1390.83,246.602 1395.22,247.859C1408.4,252.134 1414.99,259.552 1414.99,270.113L1414.99,680.485C1414.99,691.046 1408.4,698.464 1395.22,702.739C1390.83,703.996 1386.61,704.624 1382.56,704.624C1373.43,704.624 1365.83,702.236 1359.75,697.458L1158,547.286Z'/> </g> </g> </svg>", + PIN_ICON: "<svg width='16px' height='16px' viewBox='0 0 25 25' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' clip-rule='evenodd' stroke-linejoin='round' stroke-miterlimit='1.414'><path d='M24.78 9.236L15.863.316l-1.487 4.46-4.46 4.46L8.43 7.75 3.972 9.235l4.458 4.458L.776 24.388l10.627-7.72 4.46 4.46 1.485-4.46-1.486-1.485 4.46-4.46 4.46-1.487z' fill-rule='nonzero'/></svg>", THEMES: { default: { type: 'Organization', @@ -865,6 +868,8 @@ export const Constants = { DEFAULT_MAX_NOTIFICATIONS_PER_CHANNEL: 1000, MAX_TEAMNAME_LENGTH: 15, MAX_TEAMDESCRIPTION_LENGTH: 50, + MIN_CHANNELNAME_LENGTH: 2, + MAX_CHANNELNAME_LENGTH: 22, MIN_USERNAME_LENGTH: 3, MAX_USERNAME_LENGTH: 22, MAX_NICKNAME_LENGTH: 22, diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx index c84df0fa5..db8e739e6 100644 --- a/webapp/utils/markdown.jsx +++ b/webapp/utils/markdown.jsx @@ -156,9 +156,8 @@ class MattermostMarkdownRenderer extends marked.Renderer { return out; } - heading(text, level, raw) { - const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`; - return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`; + heading(text, level) { + return `<h${level} class="markdown__heading">${text}</h${level}>`; } link(href, title, text) { diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index c860987af..b3370e88c 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -32,7 +32,7 @@ export function isMac() { } export function cmdOrCtrlPressed(e) { - return (isMac() && e.metaKey) || (!isMac() && e.ctrlKey); + return (isMac() && e.metaKey) || (!isMac() && e.ctrlKey && !e.altKey); } export function isInRole(roles, inRole) { @@ -179,7 +179,7 @@ export function displayTime(ticks, utc) { ampm = ' PM'; } - hours = hours % 12; + hours %= 12; if (!hours) { hours = '12'; } @@ -591,6 +591,7 @@ export function applyTheme(theme) { changeCss('.app__body .markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07)); changeCss('.app__body .channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8)); changeCss('.app__body .channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8)); + changeCss('.app__body .channel-header #pinned-posts-button', 'fill:' + changeOpacity(theme.centerChannelColor, 0.8)); changeCss('.app__body .custom-textarea, .app__body .custom-textarea:focus, .app__body .file-preview, .app__body .post-image__details, .app__body .sidebar--right .sidebar-right__body, .app__body .markdown__table th, .app__body .markdown__table td, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .settings-modal .settings-table .settings-content .divider-light, .app__body .webhooks__container, .app__body .dropdown-menu, .app__body .modal .modal-header, .app__body .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); changeCss('.app__body .popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25)); changeCss('.app__body .search-help-popover .search-autocomplete__divider span, .app__body .suggestion-list__divider > span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7)); @@ -659,12 +660,12 @@ export function applyTheme(theme) { } if (theme.buttonBg) { - changeCss('.app__body .btn.btn-primary, .app__body .tutorial__circles .circle.active', 'background:' + theme.buttonBg); + changeCss('.app__body .btn.btn-primary, .app__body .tutorial__circles .circle.active, .app__body .post__pinned-badge', 'background:' + theme.buttonBg); changeCss('.app__body .btn.btn-primary:hover, .app__body .btn.btn-primary:active, .app__body .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25)); } if (theme.buttonColor) { - changeCss('.app__body .btn.btn-primary', 'color:' + theme.buttonColor); + changeCss('.app__body .btn.btn-primary, .app__body .post__pinned-badge', 'color:' + theme.buttonColor); } if (theme.mentionHighlightBg) { @@ -1216,7 +1217,7 @@ export function isValidPassword(password) { error = true; } - errorId = errorId + 'Lowercase'; + errorId += 'Lowercase'; } if (global.window.mm_config.PasswordRequireUppercase === 'true') { @@ -1224,7 +1225,7 @@ export function isValidPassword(password) { error = true; } - errorId = errorId + 'Uppercase'; + errorId += 'Uppercase'; } if (global.window.mm_config.PasswordRequireNumber === 'true') { @@ -1232,7 +1233,7 @@ export function isValidPassword(password) { error = true; } - errorId = errorId + 'Number'; + errorId += 'Number'; } if (global.window.mm_config.PasswordRequireSymbol === 'true') { @@ -1240,7 +1241,7 @@ export function isValidPassword(password) { error = true; } - errorId = errorId + 'Symbol'; + errorId += 'Symbol'; } minimumLength = global.window.mm_config.PasswordMinimumLength; diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index f1742e3ae..32c5a322a 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -65,7 +65,16 @@ var config = { }, { test: /\.scss$/, - loaders: ['style-loader', 'css-loader', 'sass-loader'] + use: [{ + loader: 'style-loader' + }, { + loader: 'css-loader' + }, { + loader: 'sass-loader', + options: { + includePaths: ['node_modules/compass-mixins/lib'] + } + }] }, { test: /\.css$/, @@ -92,13 +101,6 @@ var config = { minimize: !DEV, debug: false }), - new webpack.LoaderOptionsPlugin({ - options: { - sassLoader: { - includePaths: ['node_modules/compass-mixins/lib'] - } - } - }), new webpack.optimize.CommonsChunkPlugin({ minChunks: 2, children: true |