Compare commits

...

55 Commits

Author SHA1 Message Date
Mattermost Build
41848b2634 Bump app build number to 307 (#4503)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-26 22:04:54 -04:00
Mattermost Build
8d81d946c5 Automated cherry pick of #4499 (#4501)
* Revert updated dependecies for SSO

* Fix test setup

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-26 21:58:26 -04:00
Mattermost Build
10d27ee5ba Fix SSO login with subpaths (#4495)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-26 16:47:40 -04:00
Elias Nahum
647def15be Bump Version to 1.32.2 and Build number to 306 (#4494)
* Bump app version number to 1.32.2

* Bump app build number to 306
2020-06-26 16:45:23 -04:00
Mattermost Build
da440e50fb Automated cherry pick of #4489 (#4492)
* Avoid throwing when purging

* Update app/store/index.ts

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-06-26 16:20:17 -04:00
Miguel Alatzar
793ac98d74 Bump build number to 305 (#4477) 2020-06-23 18:50:25 -07:00
Mattermost Build
fab353b494 Various fixes: (#4475)
* Fix map call on Set
* Ensure we don't destructure non-iterable values
* Include error message and stack in Alert

Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-06-23 16:50:37 -07:00
Miguel Alatzar
8853b5dd45 Bump app build number to 304 (#4468) 2020-06-22 15:38:45 -07:00
Mattermost Build
464d93df8d Various fixes: (#4466)
* Dispatch REHYDRATED if already hydrated after getStoredState call
* Empty/null prev version is valid
* Update iOS target to 11.0

Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-06-22 15:31:27 -07:00
Miguel Alatzar
47f126b71f Bump build number (#4459) 2020-06-19 16:53:27 -07:00
Miguel Alatzar
4b2a0c7aea Bump version number (#4458) 2020-06-19 16:45:07 -07:00
Mattermost Build
f6bedbb7d6 Automated cherry pick of #4452 (#4456)
* Remove withEncryption

* Remove warm up

* Downgrade react-native-keychain

Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-06-19 16:19:31 -07:00
Mattermost Build
b5ee7c8908 Bump app build number to 302 (#4426)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-15 14:21:56 -04:00
Mattermost Build
aab0814b7f Automated cherry pick of #4422 (#4424)
* Fix SSO and clear cookies

* Fix unit tests

* Removed unnecessary argument

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-06-15 14:06:53 -04:00
Miguel Alatzar
bf840785fb Bump app build number to 301 (#4420) 2020-06-12 13:22:38 -07:00
Mattermost Build
514e9cfd08 Automated cherry pick of #4418 (#4419)
* Wrap await calls in try/catch

* Fix spacing

Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-06-12 13:15:35 -07:00
Mattermost Build
b5c3e95a4b Bump app build number to 300 (#4415)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-06-11 13:45:41 -07:00
Mattermost Build
e6547d7dc1 MM-25967 await until cookies are cleared (#4413)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-11 15:13:21 -04:00
Mattermost Build
2b8bba7c24 Bump app build number to 299 (#4405)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-09 16:11:59 -04:00
Mattermost Build
9b9373e27b MM-25929 Decouple id-loaded retries from regular notification run (#4403)
Bug found in #4302 that delayed delivery/receipt of normal (non id-loaded) iOS push notifications. Response handling was happening within the guard block for id-loaded messages.

Co-authored-by: Amit Uttam <changingrainbows@gmail.com>
2020-06-09 16:15:25 -03:00
Mattermost Build
bc25a29c42 MM-25782 improve channel member reducer speed to sync memberships (#4402)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-09 14:39:48 -04:00
Elias Nahum
4c4dd8297d translations PR 20200608 (#4398) 2020-06-08 21:54:36 -04:00
Mattermost Build
c3fc53a071 Removed unnecessary trim (#4399)
Co-authored-by: marianunez <maria.nunez@mattermost.com>
2020-06-08 21:43:40 -04:00
Mattermost Build
34af598a6d clear cookies on again trying to login (#4395)
Co-authored-by: Harshit Khetan <khetanmehul@gmail.com>
2020-06-08 15:27:14 -04:00
Mattermost Build
ff89f3530e Ensure previous state is cleared when logout (#4396)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-08 15:26:53 -04:00
Mattermost Build
8a95000bd0 MM-25831 Fix timing issue with cancelPing (#4394)
With `getUrl` recently [becoming async](https://github.com/mattermost/mattermost-mobile/commit/293470ff#diff-60b06b1c4aab028b96d9b207e84000c6R316), the global definition of the cancelPing function wasn't being made available in time for use by other functions (e.g. `handleSslProblem` and `handleConnect`)
2020-06-08 14:24:16 -03:00
Mattermost Build
21e1466068 MM-25849 Apply backoff & retry logic only for ID-loaded push notifications (#4393)
Changes in #4302 piggy-backed on the existing [exception catching logic](https://github.com/mattermost/mattermost-mobile/pull/4302/files#diff-266cddcf80a6bc300b40cc922e7a659bL101-L102) to retry failed request (status != 200). However, the change applied retries for any type of push notification.
2020-06-08 12:54:14 -03:00
Mattermost Build
4ebcba6069 Bump app build number to 298 (#4383)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-06-03 10:36:53 -07:00
Elias Nahum
b34ce42016 translations PR 20200601 (#4378) 2020-06-03 08:53:10 -04:00
Mattermost Build
97d393a2ba MM-25694 Disallow profile picture update is set by LDAP (#4379)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-02 12:11:53 -04:00
Mattermost Build
fb03a88304 Automated cherry pick of #4372 (#4376)
* Handle SSO redirecting to a different URL than specified by the user

* Set Server URL based on the last redirect

* Improve last redirect condition

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-06-01 16:22:28 -04:00
Mattermost Build
6e239d5566 Automated cherry pick of #4357 (#4369) 2020-05-29 09:43:02 -04:00
Mattermost Build
72b95fa265 Bump app build number to 297 (#4367)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-28 14:03:11 -04:00
Mattermost Build
d19fc71ad4 Bump app build number to 296 (#4364)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-27 15:45:17 -04:00
Mattermost Build
526290bbdf MM-25562 Fix dismiss reaction list crash (#4363)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-27 14:17:22 -04:00
Mattermost Build
962b38d024 Bump upload timeout to 1 min (#4359)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-27 13:02:44 -04:00
Mattermost Build
51109c74d3 Invalidate versions for iOS (#4356)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-27 12:44:20 -04:00
Mattermost Build
098230e79e Automated cherry pick of #4344 (#4348)
* Fix emoji autocomplete results

* Rename selectors with "select" prefix

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-27 07:42:23 -04:00
Mattermost Build
8fa67bd5b4 Automated cherry pick of #4346 (#4351)
* MM-25510 Increase post options long press delay to 250ms

* Set delay to 200ms

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-26 20:17:52 -04:00
Elias Nahum
6d7749a098 translations PR 20200525 (#4343) 2020-05-26 10:04:51 -07:00
Mattermost Build
37479587cc Add calls to Client4.savePreferences (#4347)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-26 10:00:41 -07:00
Mattermost Build
3708b86b30 MM-25434 Fix switch team badge cut off (#4339)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-22 10:57:38 -07:00
Mattermost Build
cabce2a808 Bump app build number to 295 (#4336)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-22 10:29:29 -07:00
Mattermost Build
4abb483f2c Apply background style for Android as well (#4329)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-22 11:49:50 -04:00
Mattermost Build
7a0bf1dc77 Update control icon style (#4328)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-22 11:49:37 -04:00
Mattermost Build
6cf1140a0f Incrase redux-persist timeout to 1 min (#4324)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-21 14:24:19 -07:00
Mattermost Build
9506875683 Catch ClassCastException activity (#4323)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-21 12:28:15 -07:00
Mattermost Build
679a897848 Bump app build number to 294 (#4321)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-21 09:38:02 -07:00
Mattermost Build
b5fd0284e8 Bump app version number to 1.32.0 (#4319)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-21 09:11:11 -07:00
Mattermost Build
67eea1750d Fix overflow of searchbar for iOS in Landscape (#4316)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-21 11:01:19 -04:00
Mattermost Build
d4e405485b Do not preload images with FastImage (#4315)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-20 16:17:49 -04:00
Mattermost Build
1389e4f7f7 Handle MFA error in MFA screen (#4313)
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-20 08:37:01 -07:00
Mattermost Build
7cf4084fe5 Automated cherry pick of #4305 (#4309)
* Fixes Android Share Extension

* Revert changes to share extension navigation

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-05-20 08:05:29 -04:00
Mattermost Build
6b7cffd6af Automated cherry pick of #4295 (#4307)
* Allow interaction when the in-app notification is shown

* Wrap in gesture HOC only for Android

* Set height and remove flex for android in-app notifications gesture hoc

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2020-05-19 16:09:33 -07:00
Mattermost Build
dba3278c9f Automated cherry pick of #4304 (#4306)
* Fix infinite skeleton in different use cases

* Apply suggestions from code review

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* Update app/utils/teams.js

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-05-19 16:08:58 -07:00
128 changed files with 20881 additions and 10216 deletions

View File

@@ -113,6 +113,52 @@ SOFTWARE.
---
## @react-native-community/cookies
This product contains '@react-native-community/cookies' by React Native Community.
Cookie Manager for React Native
* HOMEPAGE:
* https://github.com/react-native-community/cookies
* LICENSE: MIT License
Copyright (c) 2020 React Native Community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @react-native-community/masked-view
This product contains '@react-native-community/masked-view' by React Native Community.
React Native Masked View Library
* HOMEPAGE:
* https://github.com/react-native-community/react-native-masked-view
* LICENSE: MIT
---
## @react-native-community/netinfo
This product contains 'netinfo' by Matt Oakes.
@@ -148,38 +194,66 @@ SOFTWARE.
---
## @sentry/react-native
## @react-navigation/native
This product contains 'react-native-sentry' by Sentry.
This product contains 'react-navigation' by Adam Miskiewicz.
Official Sentry SDK for react-native
Routing and navigation for your React Native apps
* HOMEPAGE:
* https://github.com/getsentry/react-native-sentry
* https://github.com/react-navigation/react-navigation#readme
* LICENSE: BSD-2-Clause
BSD License
For React Navigation software
Copyright (c) 2016-present, React Navigation Contributors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## @react-navigation/stack
This product contains 'react-navigation-stack' by react-navigation.
Stack navigator for React Navigation
* HOMEPAGE:
* https://github.com/react-navigation/stack
* LICENSE: MIT
The MIT License (MIT)
MIT License
Copyright (c) 2017 Sentry
Copyright (c) 2017 React Native Community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
@@ -398,6 +472,62 @@ limitations under the License.
---
## @sentry/react-native
This product contains 'react-native-sentry' by Sentry.
Official Sentry SDK for react-native
* HOMEPAGE:
* https://github.com/getsentry/react-native-sentry
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2017 Sentry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## analytics-react-native
This product contains a modified version of 'analytics-react-native' by Segment.
The hassle-free way to add analytics to your React-Native app.
* HOMEPAGE:
* https://github.com/segmentio/analytics-react-native
* LICENSE: The MIT License (MIT)
Copyright (c) 2018 Segment.io, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## commonmark
This product contains a modified version of 'commonmark' by John MacFarlane.
@@ -1430,41 +1560,6 @@ THE SOFTWARE.
---
## @react-native-community/cookies
This product contains '@react-native-community/cookies' by @joeferraro.
Cookie manager for react native
* HOMEPAGE:
* https://github.com/react-native-community/cookies
* LICENSE: MIT
MIT License
Copyright (c) 2020 React Native Community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-device-info
This product contains a modified version of 'react-native-device-info' by Rebecca Hughes.
@@ -1570,6 +1665,39 @@ SOFTWARE.
---
## react-native-elements
This product contains 'react-native-elements' by React Native Elements.
Cross Platform React Native UI Toolkit
* HOMEPAGE:
* https://github.com/react-native-elements/react-native-elements
* LICENSE: The MIT License (MIT)
Copyright (c) 2016 Nader Dabit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-fast-image
This product contains 'react-native-fast-image' by Dylan Vann.
@@ -1605,6 +1733,39 @@ SOFTWARE.
---
## react-native-file-viewer
This product contains 'react-native-file-viewer' by Vincenzo Scamporlino.
Native file viewer for React Native. Preview any type of file supported by the mobile device.
* HOMEPAGE:
* https://github.com/vinzscam/react-native-file-viewer
* LICENSE: MIT License
Copyright (c) 2017 Vincenzo Scamporlino
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-gesture-handler
This product contains 'react-native-gesture-handler' by Krzysztof Magiera.
@@ -1916,6 +2077,39 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
---
## react-native-localize
This product contains 'react-native-localize' by React Native Community.
A toolbox for your React Native app localization (formerly react-native-languages)
* HOMEPAGE:
* https://github.com/react-native-community/react-native-localize
* LICENSE: MIT License
Copyright (c) 2017-present, Mathieu Acthernoene
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-mmkv-storage
This product contains 'react-native-mmkv-storage' by Ammar Ahmed.
@@ -2090,6 +2284,39 @@ SOFTWARE.
---
## react-native-reanimated
This product contains 'react-native-reanimated' by Software Mansion.
React Native's Animated library reimplemented
* HOMEPAGE:
* https://github.com/software-mansion/react-native-reanimated
* LICENSE: The MIT License (MIT)
Copyright (c) 2016 Krzysztof Magiera
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-safe-area
This product contains 'react-native-safe-area' by Masayuki Iwai.
@@ -2125,6 +2352,72 @@ SOFTWARE.
---
## react-native-safe-area-context
This product contains 'react-native-safe-area-context' by Th3rdwave.
A flexible way to handle safe area insets in JS. Also works on Android and Web
* HOMEPAGE:
* https://github.com/th3rdwave/react-native-safe-area-context
* LICENSE: MIT License
Copyright (c) 2019 Th3rd Wave
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-screens
This product contains 'react-native-screens' by Software Mansion.
Native navigation primitives for your React Native app.
* HOMEPAGE:
* https://github.com/software-mansion/react-native-screens
* LICENSE: The MIT License (MIT)
Copyright (c) 2018 Krzysztof Magiera
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-section-list-get-item-layout
This product contains 'react-native-section-list-get-item-layout' by Jan Soendermann.
@@ -2420,69 +2713,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-navigation
This product contains 'react-navigation' by Adam Miskiewicz.
Routing and navigation for your React Native apps
* HOMEPAGE:
* https://github.com/react-navigation/react-navigation#readme
* LICENSE: BSD-2-Clause
BSD License
For React Navigation software
Copyright (c) 2016-present, React Navigation Contributors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## react-navigation-stack
This product contains 'react-navigation-stack' by react-navigation.
Stack navigator for React Navigation
* HOMEPAGE:
* https://github.com/react-navigation/stack
* LICENSE: MIT
MIT License
Copyright (c) 2017 React Native Community
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
----
## react-redux
@@ -2695,41 +2925,6 @@ SOFTWARE.
---
## redux-reset
This product contains 'redux-reset' by Wang Zixiao.
Gives redux the ability to reset the state
* HOMEPAGE:
* https://github.com/wwayne/redux-reset
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2016 Wang Zixiao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## redux-thunk
This product contains 'redux-thunk' by Dan Abramov.

View File

@@ -133,8 +133,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
versionCode 293
versionName "1.31.0"
versionCode 307
versionName "1.32.2"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'

View File

@@ -36,12 +36,7 @@ public class MattermostCredentialsHelper {
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL);
final WritableMap options = Arguments.createMap();
final WritableMap authPrompt = Arguments.createMap();
authPrompt.putString("title", "Authenticate to retrieve secret");
authPrompt.putString("cancel", "Cancel");
options.putMap("authenticationPrompt", authPrompt);
keychainModule.getInternetCredentialsForServer(serverUrl, options, promise);
keychainModule.getGenericPasswordForOptions(serverUrl, promise);
}
}

View File

@@ -105,7 +105,7 @@ public class ReceiptDelivery {
try {
Response response = client.newCall(request).execute();
String responseBody = response.body().string();
if (response.code() != 200 || !isIdLoaded) {
if (response.code() != 200) {
throw new Exception(responseBody);
}
JSONObject jsonResponse = new JSONObject(responseBody);
@@ -120,14 +120,16 @@ public class ReceiptDelivery {
promise.resolve(bundle);
} catch (Exception e) {
Log.e("ReactNative", "Receipt delivery failed to send");
try {
reRequestCount++;
if (reRequestCount < FIBONACCI_BACKOFFS.length) {
Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFFS[reRequestCount] + " seconds");
Thread.sleep(FIBONACCI_BACKOFFS[reRequestCount] * 1000);
makeServerRequest(client, request, isIdLoaded, reRequestCount, promise);
}
} catch(InterruptedException ie) {}
if (isIdLoaded) {
try {
reRequestCount++;
if (reRequestCount < FIBONACCI_BACKOFFS.length) {
Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFFS[reRequestCount] + " seconds");
Thread.sleep(FIBONACCI_BACKOFFS[reRequestCount] * 1000);
makeServerRequest(client, request, isIdLoaded, reRequestCount, promise);
}
} catch(InterruptedException ie) {}
}
promise.reject("Receipt delivery failure", e.toString());
}

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-import-assign */
import {Client4} from '@mm-redux/client';
import {Preferences} from '@mm-redux/constants';
import {PreferenceTypes} from '@mm-redux/action_types';
import * as CommonSelectors from '@mm-redux/selectors/entities/common';
import * as PreferenceSelectors from '@mm-redux/selectors/entities/preferences';
import * as PreferenceUtils from '@mm-redux/utils/preference_utils';
import {
makeDirectChannelVisibleIfNecessary,
makeGroupMessageVisibleIfNecessary,
} from './channels';
describe('Actions.Helpers.Channels', () => {
describe('makeDirectChannelVisibleIfNecessary', () => {
const state = {};
const currentUserId = 'current-user-id';
const otherUserId = 'other-user-id';
CommonSelectors.getCurrentUserId = jest.fn().mockReturnValue(currentUserId);
PreferenceSelectors.getMyPreferences = jest.fn();
PreferenceUtils.getPreferenceKey = jest.fn();
Client4.savePreferences = jest.fn();
beforeEach(() => {
PreferenceSelectors.getMyPreferences.mockClear();
PreferenceUtils.getPreferenceKey.mockClear();
Client4.savePreferences.mockClear();
});
it('makes direct channel visible when visibility preference does not exist', () => {
PreferenceSelectors.getMyPreferences.mockReturnValueOnce({});
const expectedResult = {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: [{
user_id: currentUserId,
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name: otherUserId,
value: 'true',
}],
};
const result = makeDirectChannelVisibleIfNecessary(state, otherUserId);
expect(result).toStrictEqual(expectedResult);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledTimes(1);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledWith(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId);
expect(Client4.savePreferences).toHaveBeenCalledTimes(1);
expect(Client4.savePreferences).toHaveBeenCalledWith(currentUserId, expectedResult.data);
});
it('makes direct channel visible when visibilty preference is false', () => {
const preference = {value: 'false'};
const preferenceKey = 'preference-key';
PreferenceSelectors.getMyPreferences.mockReturnValueOnce({
[preferenceKey]: preference,
});
PreferenceUtils.getPreferenceKey.mockReturnValueOnce(preferenceKey);
const expectedResult = {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: [{
user_id: currentUserId,
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name: otherUserId,
value: 'true',
}],
};
const result = makeDirectChannelVisibleIfNecessary(state, otherUserId);
expect(result).toStrictEqual(expectedResult);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledTimes(1);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledWith(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId);
expect(Client4.savePreferences).toHaveBeenCalledTimes(1);
expect(Client4.savePreferences).toHaveBeenCalledWith(currentUserId, expectedResult.data);
});
it('does nothing if direct channel visibility preference is true', () => {
const preference = {value: 'true'};
const preferenceKey = 'preference-key';
PreferenceSelectors.getMyPreferences.mockReturnValueOnce({
[preferenceKey]: preference,
});
PreferenceUtils.getPreferenceKey.mockReturnValueOnce(preferenceKey);
const result = makeDirectChannelVisibleIfNecessary(state, otherUserId);
expect(result).toEqual(null);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledTimes(1);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledWith(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId);
expect(Client4.savePreferences).not.toHaveBeenCalled();
});
});
describe('makeGroupMessageVisibleIfNecessary', () => {
const state = {};
const currentUserId = 'current-user-id';
const channelId = 'channel-id';
CommonSelectors.getCurrentUserId = jest.fn().mockReturnValue(currentUserId);
PreferenceSelectors.getMyPreferences = jest.fn();
PreferenceUtils.getPreferenceKey = jest.fn();
Client4.savePreferences = jest.fn();
beforeEach(() => {
PreferenceSelectors.getMyPreferences.mockClear();
PreferenceUtils.getPreferenceKey.mockClear();
Client4.savePreferences.mockClear();
});
it('makes group channel visible when visibility preference does not exist', async () => {
PreferenceSelectors.getMyPreferences.mockReturnValueOnce({});
const expectedPreferenceResult = {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: [{
user_id: currentUserId,
category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
name: channelId,
value: 'true',
}],
};
const result = await makeGroupMessageVisibleIfNecessary(state, channelId);
expect(result.length).toEqual(2);
expect(result[1]).toStrictEqual(expectedPreferenceResult);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledTimes(1);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledWith(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId);
expect(Client4.savePreferences).toHaveBeenCalledTimes(1);
expect(Client4.savePreferences).toHaveBeenCalledWith(currentUserId, expectedPreferenceResult.data);
});
it('makes group channel visible when visibilty preference is false', async () => {
const preference = {value: 'false'};
const preferenceKey = 'preference-key';
PreferenceSelectors.getMyPreferences.mockReturnValueOnce({
[preferenceKey]: preference,
});
PreferenceUtils.getPreferenceKey.mockReturnValueOnce(preferenceKey);
const expectedPreferenceResult = {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: [{
user_id: currentUserId,
category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
name: channelId,
value: 'true',
}],
};
const result = await makeGroupMessageVisibleIfNecessary(state, channelId);
expect(result.length).toEqual(2);
expect(result[1]).toStrictEqual(expectedPreferenceResult);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledTimes(1);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledWith(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId);
expect(Client4.savePreferences).toHaveBeenCalledTimes(1);
expect(Client4.savePreferences).toHaveBeenCalledWith(currentUserId, expectedPreferenceResult.data);
});
it('does nothing if group channel visibility preference is true', async () => {
const preference = {value: 'true'};
const preferenceKey = 'preference-key';
PreferenceSelectors.getMyPreferences.mockReturnValueOnce({
[preferenceKey]: preference,
});
PreferenceUtils.getPreferenceKey.mockReturnValueOnce(preferenceKey);
const result = await makeGroupMessageVisibleIfNecessary(state, channelId);
expect(result).toEqual(null);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledTimes(1);
expect(PreferenceUtils.getPreferenceKey).toHaveBeenCalledWith(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId);
expect(Client4.savePreferences).not.toHaveBeenCalled();
});
});
});

View File

@@ -169,6 +169,7 @@ export function makeDirectChannelVisibleIfNecessary(state: GlobalState, otherUse
value: 'true',
};
Client4.savePreferences(currentUserId, [preference]);
return {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: [preference],
@@ -193,6 +194,8 @@ export async function makeGroupMessageVisibleIfNecessary(state: GlobalState, cha
value: 'true',
};
Client4.savePreferences(currentUserId, [preference]);
const profilesInChannel = await fetchUsersInChannel(state, channelId);
return [{
@@ -368,11 +371,15 @@ async function getProfilesFromPromises(promises: Array<Promise<ActionResult>>):
return null;
}
const result = await Promise.all(promises);
const data = result.filter((p: any) => !p.error);
try {
const result = await Promise.all(promises);
const data = result.filter((p: any) => !p.error);
return {
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
data,
};
return {
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
data,
};
} catch {
return null;
}
}

View File

@@ -5,6 +5,7 @@ import {Keyboard, Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import {Preferences} from '@mm-redux/constants';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import EphemeralStore from '@store/ephemeral_store';
@@ -83,7 +84,7 @@ export function resetToChannel(passProps = {}) {
}
export function resetToSelectServer(allowOtherServers) {
const theme = getThemeFromState();
const theme = Preferences.THEMES.default;
Navigation.setRoot({
root: {

View File

@@ -26,7 +26,7 @@ import {
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {getTeamByName} from '@mm-redux/selectors/entities/teams';
import {getChannelByName as selectChannelByName} from '@mm-redux/utils/channel_utils';
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
@@ -224,10 +224,15 @@ export function handleSelectChannel(channelId) {
const channel = channels[channelId];
const member = myMembers[channelId];
dispatch(loadPostsIfNecessaryWithRetry(channelId));
if (channel) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
if (channel && currentChannelId !== channelId) {
const actions = markAsViewedAndReadBatch(state, channelId, currentChannelId);
let previousChannelId = null;
if (currentChannelId !== channelId) {
previousChannelId = currentChannelId;
}
const actions = markAsViewedAndReadBatch(state, channelId, previousChannelId);
actions.push({
type: ChannelTypes.SELECT_CHANNEL,
data: channelId,
@@ -237,10 +242,11 @@ export function handleSelectChannel(channelId) {
teamId: channel.team_id || currentTeamId,
},
});
dispatch(batchActions(actions, 'BATCH_SWITCH_CHANNEL'));
}
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
dispatch(batchActions(actions, 'BATCH_SWITCH_CHANNEL'));
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
}
};
}
@@ -321,7 +327,9 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional
if (markOnServer) {
Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId);
Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId).catch(() => {
// do nothing just adding the handler to avoid the warning
});
}
if (member) {
@@ -600,7 +608,12 @@ export function loadChannelsForTeam(teamId, skipDispatch = false) {
return async (dispatch, getState) => {
const state = getState();
const currentUserId = getCurrentUserId(state);
const data = {sync: true, teamId};
const data = {
sync: true,
teamId,
teamChannels: getChannelsIdForTeam(state, teamId),
};
const actions = [];
if (currentUserId) {
@@ -639,12 +652,17 @@ export function loadChannelsForTeam(teamId, skipDispatch = false) {
}
if (rolesToLoad.size > 0) {
data.roles = await Client4.getRolesByNames(Array.from(rolesToLoad));
if (data.roles.length) {
actions.push({
type: RoleTypes.RECEIVED_ROLES,
data: data.roles,
});
try {
data.roles = await Client4.getRolesByNames(Array.from(rolesToLoad));
if (data.roles.length) {
actions.push({
type: RoleTypes.RECEIVED_ROLES,
data: data.roles,
});
}
} catch {
//eslint-disable-next-line no-console
console.log('Could not retrieve channel members roles for the user');
}
}

View File

@@ -320,7 +320,7 @@ describe('Actions.Views.Channel', () => {
teamId: currentTeamId,
},
};
if (channelId.includes('not') || channelId === currentChannelId) {
if (channelId.includes('not')) {
expect(selectChannelWithMember).toBe(undefined);
} else {
expect(selectChannelWithMember).toStrictEqual(expectedSelectChannelWithMember);

View File

@@ -55,7 +55,7 @@ export function getEmojisInPosts(posts) {
const emojisToLoad = getNeededCustomEmojis(state, posts);
if (emojisToLoad?.size > 0) {
const promises = emojisToLoad.map((name) => getCustomEmojiByName(name));
const promises = Array.from(emojisToLoad).map((name) => getCustomEmojiByName(name));
const result = await Promise.all(promises);
const actions = [];
const data = [];

View File

@@ -87,7 +87,10 @@ export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
const postForChannel = postsInChannel[channelId];
const data = await Client4.getPosts(channelId, page, perPage);
const posts = Object.values(data.posts);
const actions = [];
const actions = [{
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
failed: false,
}];
if (posts?.length) {
actions.push(receivedPosts(data));

View File

@@ -145,7 +145,7 @@ export function purgeOfflineStore() {
dispatch({
type: General.OFFLINE_STORE_PURGE,
state: getStateForReset(initialState, currentState),
data: getStateForReset(initialState, currentState),
});
EventEmitter.emit(NavigationTypes.RESTART_APP);

View File

@@ -32,10 +32,16 @@ export function selectDefaultTeam() {
const state = getState();
const {ExperimentalPrimaryTeam} = getConfig(state);
const {teams: allTeams, myMembers} = state.entities.teams;
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
const {teams, myMembers} = state.entities.teams;
const myTeams = Object.keys(teams).reduce((result, id) => {
if (myMembers[id]) {
result.push(teams[id]);
}
let defaultTeam = selectFirstAvailableTeam(teams, ExperimentalPrimaryTeam);
return result;
}, []);
let defaultTeam = selectFirstAvailableTeam(myTeams, ExperimentalPrimaryTeam);
if (defaultTeam) {
dispatch(handleTeamChange(defaultTeam.id));

View File

@@ -183,13 +183,17 @@ export function login(loginId, password, mfaToken, ldapOnly = false) {
}
export function ssoLogin(token) {
return async (dispatch) => {
return async (dispatch, getState) => {
const state = getState();
const deviceToken = state.entities?.general?.deviceToken;
Client4.setToken(token);
await setCSRFFromCookie(Client4.getUrl());
const result = await dispatch(loadMe());
if (!result.error) {
dispatch(completeLogin(result.data.user));
dispatch(completeLogin(result.data.user, deviceToken));
}
return result;

View File

@@ -9,7 +9,6 @@ import {
Text,
View,
} from 'react-native';
import Fuse from 'fuse.js';
import AutocompleteDivider from '@components/autocomplete/autocomplete_divider';
import Emoji from '@components/emoji';
@@ -21,15 +20,6 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
const EMOJI_REGEX_WITHOUT_PREFIX = /\B(:([^:\s]*))$/i;
const options = {
shouldSort: true,
threshold: 0.3,
location: 0,
distance: 100,
minMatchCharLength: 2,
maxPatternLength: 32,
};
export default class EmojiSuggestion extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@@ -39,6 +29,7 @@ export default class EmojiSuggestion extends PureComponent {
cursorPosition: PropTypes.number,
customEmojisEnabled: PropTypes.bool,
emojis: PropTypes.array.isRequired,
fuse: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
maxListHeight: PropTypes.number,
theme: PropTypes.object.isRequired,
@@ -63,23 +54,16 @@ export default class EmojiSuggestion extends PureComponent {
super(props);
this.matchTerm = '';
const list = props.emojis || [];
this.fuse = new Fuse(list, options);
}
componentDidUpdate(prevProps) {
componentDidUpdate() {
if (this.props.isSearch) {
return;
}
const {cursorPosition, emojis, value} = this.props;
const {cursorPosition, value} = this.props;
const match = value.substring(0, cursorPosition).match(EMOJI_REGEX);
if (prevProps.emojis !== emojis) {
const list = emojis || [];
this.fuse = new Fuse(list, options);
}
if (!match || this.state.emojiComplete) {
this.resetAutocomplete();
return;
@@ -91,7 +75,7 @@ export default class EmojiSuggestion extends PureComponent {
if (this.props.customEmojisEnabled) {
this.props.actions.autocompleteCustomEmojis(this.matchTerm);
}
this.searchEmoji(this.matchTerm);
this.searchEmojis(this.matchTerm);
}
}
@@ -145,17 +129,6 @@ export default class EmojiSuggestion extends PureComponent {
getItemLayout = ({index}) => ({length: 40, offset: 40 * index, index})
handleFuzzySearch = (matchTerm) => {
const {emojis} = this.props;
clearTimeout(this.searchTermTimeout);
this.searchTermTimeout = setTimeout(() => {
const results = this.fuse.search(matchTerm.toLowerCase()).map((r) => r.refIndex);
const data = results.map((index) => emojis[index]);
this.setEmojiData(data, matchTerm);
}, 100);
};
keyExtractor = (item) => item;
renderItem = ({item}) => {
@@ -188,26 +161,38 @@ export default class EmojiSuggestion extends PureComponent {
this.props.onResultCountChange(0);
}
searchEmoji = (matchTerm) => {
if (matchTerm.length) {
this.handleFuzzySearch(matchTerm);
} else {
this.setEmojiData(this.props.emojis);
}
}
searchEmojis = (searchTerm) => {
const {emojis, fuse} = this.props;
setEmojiData = (data, matchTerm = null) => {
let sorter = compareEmojis;
if (matchTerm) {
sorter = (a, b) => compareEmojis(a, b, matchTerm);
if (searchTerm.trim().length) {
const searchTermLowerCase = searchTerm.toLowerCase();
sorter = (a, b) => compareEmojis(a, b, searchTermLowerCase);
clearTimeout(this.searchTermTimeout);
this.searchTermTimeout = setTimeout(() => {
const fuzz = fuse.search(searchTerm);
const results = fuzz.reduce((values, r) => {
const v = r.matches[0]?.value;
if (v) {
values.push(v);
}
return values;
}, []);
const data = results.sort(sorter);
this.setState({
active: data.length > 0,
dataSource: data,
});
}, 100);
} else {
this.setState({
active: emojis.length > 0,
dataSource: emojis.sort(sorter),
});
}
this.setState({
active: data.length > 0,
dataSource: data.sort(sorter),
});
this.props.onResultCountChange(data.length);
};
render() {

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Fuse from 'fuse.js';
import Preferences from '@mm-redux/constants/preferences';
import {selectEmojisByName} from '@selectors/emojis';
import initialState from '@store/initial_state';
import {shallowWithIntl} from 'test/intl-test-helper';
import EmojiSuggestion from './emoji_suggestion';
jest.useFakeTimers();
describe('components/autocomplete/emoji_suggestion', () => {
const state = {
...initialState,
views: {
recentEmojis: [],
},
};
const emojis = selectEmojisByName(state);
const options = {
shouldSort: false,
threshold: 0.3,
location: 0,
distance: 10,
includeMatches: true,
findAllMatches: true,
};
const fuse = new Fuse(emojis, options);
const baseProps = {
actions: {
addReactionToLatestPost: jest.fn(),
autocompleteCustomEmojis: jest.fn(),
},
cursorPosition: 0,
customEmojisEnabled: false,
emojis,
fuse,
isSearch: false,
theme: Preferences.THEMES.default,
onChangeText: jest.fn(),
onResultCountChange: jest.fn(),
rootId: '',
value: '',
nestedScrollEnabled: false,
};
test('should match snapshot', () => {
const wrapper = shallowWithIntl(<EmojiSuggestion {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
wrapper.setProps({cursorPosition: 1, value: ':1'});
expect(wrapper.getElement()).toMatchSnapshot();
});
test('searchEmojis should return the right values on fuse', () => {
const output1 = ['100', '1234', '1st_place_medal', '+1', '-1', 'u7121'];
const output2 = ['+1'];
const output3 = ['-1'];
const wrapper = shallowWithIntl(<EmojiSuggestion {...baseProps}/>);
wrapper.instance().searchEmojis('');
expect(wrapper.state('dataSource')).toEqual(baseProps.emojis);
wrapper.instance().searchEmojis('1');
jest.runAllTimers();
setTimeout(() => {
expect(wrapper.state('dataSource')).toEqual(output1);
}, 100);
wrapper.instance().searchEmojis('+');
jest.runAllTimers();
setTimeout(() => {
expect(wrapper.state('dataSource')).toEqual(output2);
}, 100);
wrapper.instance().searchEmojis('-');
jest.runAllTimers();
setTimeout(() => {
expect(wrapper.state('dataSource')).toEqual(output3);
}, 100);
});
});

View File

@@ -3,34 +3,32 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import Fuse from 'fuse.js';
import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {addReactionToLatestPost} from '@actions/views/emoji';
import {autocompleteCustomEmojis} from '@mm-redux/actions/emojis';
import {createIdsSelector} from '@mm-redux/utils/helpers';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {EmojiIndicesByAlias} from 'app/utils/emojis';
import {selectEmojisByName} from '@selectors/emojis';
import EmojiSuggestion from './emoji_suggestion';
const getEmojisByName = createIdsSelector(
getCustomEmojisByName,
(customEmojis) => {
const emoticons = new Set();
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
emoticons.add(key);
}
return Array.from(emoticons);
},
);
function mapStateToProps(state) {
const emojis = getEmojisByName(state);
const emojis = selectEmojisByName(state);
const options = {
shouldSort: false,
threshold: 0.3,
location: 0,
distance: 10,
includeMatches: true,
findAllMatches: true,
};
const list = emojis.length ? emojis : [];
const fuse = new Fuse(list, options);
return {
fuse,
emojis,
customEmojisEnabled: getConfig(state).EnableCustomEmoji === 'true',
theme: getTheme(state),

View File

@@ -47,25 +47,27 @@ export default class EmojiPicker extends EmojiPickerBase {
keyboardVerticalOffset={keyboardOffset}
style={styles.flex}
>
<View style={[styles.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
<View style={styles.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
</View>
<View style={[styles.container]}>
{this.renderListComponent(shorten)}

View File

@@ -2,14 +2,35 @@
// See LICENSE.txt for license information.
import React from 'react';
import Fuse from 'fuse.js';
import Preferences from '@mm-redux/constants/preferences';
import {selectEmojisByName, selectEmojisBySection} from '@selectors/emojis';
import initialState from '@store/initial_state';
import {shallowWithIntl} from 'test/intl-test-helper';
import {filterEmojiSearchInput} from './emoji_picker_base';
import EmojiPicker from './emoji_picker.ios';
describe('components/emoji_picker/EmojiPicker', () => {
describe('components/emoji_picker/emoji_picker.ios', () => {
const state = {
...initialState,
views: {
recentEmojis: [],
},
};
const emojis = selectEmojisByName(state);
const emojisBySection = selectEmojisBySection(state);
const options = {
shouldSort: false,
threshold: 0.3,
location: 0,
distance: 10,
includeMatches: true,
findAllMatches: true,
};
const fuse = new Fuse(emojis, options);
const baseProps = {
actions: {
getCustomEmojis: jest.fn(),
@@ -19,9 +40,9 @@ describe('components/emoji_picker/EmojiPicker', () => {
customEmojisEnabled: false,
customEmojiPage: 200,
deviceWidth: 400,
emojis: [],
emojisBySection: [],
fuse: {},
emojis,
emojisBySection,
fuse,
isLandscape: false,
theme: Preferences.THEMES.default,
};
@@ -48,6 +69,15 @@ describe('components/emoji_picker/EmojiPicker', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('searchEmojis should return the right values on fuse', () => {
const input = '1';
const output = ['100', '1234', '1st_place_medal', '+1', '-1', 'u7121'];
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const result = wrapper.instance().searchEmojis(input);
expect(result).toEqual(output);
});
test('should set rebuildEmojis to true when deviceWidth changes', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
@@ -84,7 +114,7 @@ describe('components/emoji_picker/EmojiPicker', () => {
const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis');
setRebuiltEmojis(searchBarAnimationComplete);
expect(instance.setState).toHaveBeenCalledWith({emojis: []});
expect(instance.setState).toHaveBeenCalledTimes(1);
expect(instance.rebuildEmojis).toBe(false);
});

View File

@@ -203,16 +203,24 @@ export default class EmojiPicker extends PureComponent {
};
searchEmojis = (searchTerm) => {
const {emojis, fuse} = this.props;
const {fuse} = this.props;
const searchTermLowerCase = searchTerm.toLowerCase();
if (!searchTerm) {
return [];
}
const results = fuse.search(searchTermLowerCase).map((r) => r.refIndex);
const sorter = (a, b) => compareEmojis(a, b, searchTerm);
const data = results.map((index) => emojis[index]).sort(sorter);
const sorter = (a, b) => compareEmojis(a, b, searchTermLowerCase);
const fuzz = fuse.search(searchTermLowerCase);
const results = fuzz.reduce((values, r) => {
const v = r.matches[0]?.value;
if (v) {
values.push(v);
}
return values;
}, []);
const data = results.sort(sorter);
return data;
};
@@ -295,6 +303,7 @@ export default class EmojiPicker extends PureComponent {
<View style={[style.flatListEmoji, padding(this.props.isLandscape)]}>
<Emoji
emojiName={item}
textStyle={style.emojiText}
size={20}
/>
</View>
@@ -476,6 +485,10 @@ export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
emojiText: {
color: '#000',
fontWeight: 'bold',
},
flatList: {
flex: 1,
backgroundColor: theme.centerChannelBg,

View File

@@ -3,154 +3,28 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {createSelector} from 'reselect';
import Fuse from 'fuse.js';
import {incrementEmojiPickerPage} from '@actions/views/emoji';
import {getCustomEmojis} from '@mm-redux/actions/emojis';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {createIdsSelector} from '@mm-redux/utils/helpers';
import {getDimensions, isLandscape} from '@selectors/device';
import {BuiltInEmojis, CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from '@utils/emojis';
import {t} from '@utils/i18n';
import {selectEmojisByName, selectEmojisBySection} from '@selectors/emojis';
import EmojiPicker from './emoji_picker';
const categoryToI18n = {
activity: {
id: t('mobile.emoji_picker.activity'),
defaultMessage: 'ACTIVITY',
icon: 'futbol-o',
},
custom: {
id: t('mobile.emoji_picker.custom'),
defaultMessage: 'CUSTOM',
icon: 'at',
},
flags: {
id: t('mobile.emoji_picker.flags'),
defaultMessage: 'FLAGS',
icon: 'flag-o',
},
foods: {
id: t('mobile.emoji_picker.foods'),
defaultMessage: 'FOODS',
icon: 'cutlery',
},
nature: {
id: t('mobile.emoji_picker.nature'),
defaultMessage: 'NATURE',
icon: 'leaf',
},
objects: {
id: t('mobile.emoji_picker.objects'),
defaultMessage: 'OBJECTS',
icon: 'lightbulb-o',
},
people: {
id: t('mobile.emoji_picker.people'),
defaultMessage: 'PEOPLE',
icon: 'smile-o',
},
places: {
id: t('mobile.emoji_picker.places'),
defaultMessage: 'PLACES',
icon: 'plane',
},
recent: {
id: t('mobile.emoji_picker.recent'),
defaultMessage: 'RECENTLY USED',
icon: 'clock-o',
},
symbols: {
id: t('mobile.emoji_picker.symbols'),
defaultMessage: 'SYMBOLS',
icon: 'heart-o',
},
};
function fillEmoji(indice) {
const emoji = Emojis[indice];
return {
name: emoji.aliases[0],
aliases: emoji.aliases,
};
}
const getEmojisBySection = createSelector(
getCustomEmojisByName,
(state) => state.views.recentEmojis,
(customEmojis, recentEmojis) => {
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
const items = EmojiIndicesByCategory.get(category).map(fillEmoji);
const section = {
...categoryToI18n[category],
key: category,
data: items,
};
return section;
});
const customEmojiItems = [];
BuiltInEmojis.forEach((emoji) => {
customEmojiItems.push({
name: emoji,
});
});
for (const [key] of customEmojis) {
customEmojiItems.push({
name: key,
});
}
emoticons.push({
...categoryToI18n.custom,
key: 'custom',
data: customEmojiItems,
});
if (recentEmojis.length) {
const items = recentEmojis.map((emoji) => ({name: emoji}));
emoticons.unshift({
...categoryToI18n.recent,
key: 'recent',
data: items,
});
}
return emoticons;
},
);
const getEmojisByName = createIdsSelector(
getCustomEmojisByName,
(customEmojis) => {
const emoticons = new Set();
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
emoticons.add(key);
}
return Array.from(emoticons);
},
);
function mapStateToProps(state) {
const emojisBySection = getEmojisBySection(state);
const emojis = getEmojisByName(state);
const emojisBySection = selectEmojisBySection(state);
const emojis = selectEmojisByName(state);
const {deviceWidth} = getDimensions(state);
const options = {
shouldSort: true,
shouldSort: false,
threshold: 0.3,
location: 0,
distance: 100,
minMatchCharLength: 2,
maxPatternLength: 32,
distance: 10,
includeMatches: true,
findAllMatches: true,
};
const list = emojis.length ? emojis : [];

View File

@@ -3,39 +3,48 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import {Text, View} from 'react-native';
import {intlShape} from 'react-intl';
import Button from 'react-native-button';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import Cloud from './cloud';
export default class FailedNetworkAction extends PureComponent {
static propTypes = {
onRetry: PropTypes.func.isRequired,
actionId: PropTypes.string,
actionDefaultMessage: PropTypes.string,
errorId: PropTypes.string,
errorDefaultMessage: PropTypes.string,
actionText: PropTypes.string,
errorMessage: PropTypes.string,
errorTitle: PropTypes.string,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape.isRequired,
};
static defaultProps = {
actionId: t('mobile.failed_network_action.retry'),
actionDefaultMessage: 'try again',
errorId: t('mobile.failed_network_action.shortDescription'),
errorDefaultMessage: 'Messages will load when you have an internet connection or {tryAgainAction}.',
showAction: true,
};
render() {
const {actionId, actionDefaultMessage, errorId, errorDefaultMessage, onRetry, theme} = this.props;
const {formatMessage} = this.context.intl;
const {onRetry, theme} = this.props;
const style = getStyleFromTheme(theme);
const errorTitle = {
id: t('mobile.failed_network_action.title'),
const actionText = this.props.actionText || formatMessage({
id: 'mobile.failed_network_action.retry',
defaultMessage: 'Try again',
});
const errorTitle = this.props.errorTitle || formatMessage({
id: 'mobile.failed_network_action.title',
defaultMessage: 'No internet connection',
};
});
const errorMessage = this.props.errorMessage || formatMessage({
id: 'mobile.failed_network_action.shortDescription',
defaultMessage: 'Messages will load when you have an internet connection.',
});
return (
<View style={style.container}>
@@ -44,26 +53,14 @@ export default class FailedNetworkAction extends PureComponent {
height={76}
width={76}
/>
<FormattedText
id={errorTitle.id}
defaultMessage={errorTitle.defaultMessage}
style={style.title}
/>
<FormattedText
id={errorId}
defaultMessage={errorDefaultMessage}
style={style.description}
values={{
tryAgainAction: (
<FormattedText
id={actionId}
defaultMessage={actionDefaultMessage}
style={style.link}
onPress={onRetry}
/>
),
}}
/>
<Text style={style.title}>{errorTitle}</Text>
<Text style={style.description}>{errorMessage}</Text>
<Button
onPress={onRetry}
containerStyle={style.buttonContainer}
>
<Text style={style.link}>{actionText}</Text>
</Button>
</View>
);
}
@@ -92,7 +89,16 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
textAlign: 'center',
},
link: {
color: theme.linkColor,
color: theme.buttonColor,
fontSize: 15,
},
buttonContainer: {
backgroundColor: theme.buttonBg,
borderRadius: 5,
height: 42,
justifyContent: 'center',
marginTop: 20,
paddingHorizontal: 12,
},
};
});

View File

@@ -7,7 +7,6 @@ import {
View,
StyleSheet,
} from 'react-native';
import FastImage from 'react-native-fast-image';
import brokenImageIcon from '@assets/images/icons/brokenimage.png';
import ProgressiveImage from '@components/progressive_image';
@@ -49,29 +48,9 @@ export default class FileAttachmentImage extends PureComponent {
resizeMethod: 'resize',
};
constructor(props) {
super(props);
const {file} = props;
if (file && file.id && !file.localPath) {
const headers = Client4.getOptions({}).headers;
const preloadImages = [
{uri: Client4.getFileThumbnailUrl(file.id), headers},
{uri: Client4.getFileUrl(file.id), headers},
];
if (isGif(file)) {
preloadImages.push({uri: Client4.getFilePreviewUrl(file.id), headers});
}
FastImage.preload(preloadImages);
}
this.state = {
failed: false,
};
}
state = {
failed: false,
};
boxPlaceholder = () => {
if (this.props.isSingleImage) {

View File

@@ -77,7 +77,6 @@ export default class MarkdownImage extends ImageViewPort {
uri = EphemeralStore.currentServerUrl + uri;
}
FastImage.preload([{uri}]);
return uri;
};

View File

@@ -16,14 +16,6 @@ export default class AttachmentAuthor extends PureComponent {
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
if (props.icon) {
FastImage.preload([{uri: props.icon}]);
}
}
openLink = () => {
const {link} = this.props;
if (link && Linking.canOpenURL(link)) {

View File

@@ -17,14 +17,6 @@ export default class AttachmentFooter extends PureComponent {
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
if (props.icon) {
FastImage.preload([{uri: props.icon}]);
}
}
render() {
const {
text,

View File

@@ -312,7 +312,7 @@ export default class Post extends PureComponent {
<TouchableWithFeedback
onPress={this.handlePress}
onLongPress={this.showPostOptions}
delayLongPress={100}
delayLongPress={200}
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
cancelTouchOnPanning={true}
>

View File

@@ -110,8 +110,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
dimensions = calculateDimensions(ogImage.height, ogImage.width, this.getViewPostWidth());
}
FastImage.preload([{uri: imageUrl}]);
return {
hasImage: true,
...dimensions,

View File

@@ -141,7 +141,7 @@ export default class PostDraft extends PureComponent {
channel_id: channelId,
root_id: rootId,
parent_id: rootId,
message: value.trim(),
message: value,
};
createPost(post, postFiles);

View File

@@ -151,7 +151,7 @@ export default class UploadItem extends PureComponent {
const certificate = await mattermostBucket.getPreference('cert');
const options = {
timeout: 10000,
timeout: 60000,
certificate,
};
this.uploadPromise = RNFetchBlob.config(options).fetch('POST', Client4.getFilesRoute(), headers, data);

View File

@@ -61,7 +61,6 @@ export default class ProfilePicture extends PureComponent {
this.setImageURL(imageUri);
} else if (user) {
const uri = Client4.getProfilePictureUrl(user.id, user.last_picture_update);
FastImage.preload([{uri, headers: Client4.getOptions({}).headers}]);
this.setImageURL(uri);
this.clearProfileImageUri();
@@ -102,7 +101,6 @@ export default class ProfilePicture extends PureComponent {
if (nextUrl && url !== nextUrl) {
// empty function is so that promise unhandled is not triggered in dev mode
FastImage.preload([{uri: nextUrl, headers: Client4.getOptions({}).headers}]);
this.setImageURL(nextUrl);
this.clearProfileImageUri();
}

View File

@@ -8,10 +8,6 @@ import Preferences from '@mm-redux/constants/preferences';
import ProgressiveImage from './progressive_image';
jest.mock('react-native-fast-image', () => ({
preload: jest.fn(),
}));
jest.useFakeTimers();
describe('ProgressiveImage', () => {

View File

@@ -68,7 +68,7 @@ export default class Root extends PureComponent {
const {currentUrl, theme} = this.props;
const {intl} = this.providerRef.getChildContext();
let passProps = null;
let passProps = {theme};
const options = {topBar: {}};
if (Platform.OS === 'android') {
options.topBar.rightButtons = [{
@@ -87,6 +87,7 @@ export default class Root extends PureComponent {
if (screen === 'SelectTeam') {
passProps = {
...passProps,
currentUrl,
userWithoutTeams: true,
};

View File

@@ -38,6 +38,7 @@ export default class Search extends PureComponent {
tintColorDelete: PropTypes.string,
selectionColor: PropTypes.string,
inputStyle: CustomPropTypes.Style,
containerStyle: CustomPropTypes.Style,
cancelButtonStyle: CustomPropTypes.Style,
autoFocus: PropTypes.bool,
placeholder: PropTypes.string,
@@ -285,7 +286,7 @@ export default class Search extends PureComponent {
}
return (
<View style={searchBarStyle.container}>
<View style={[searchBarStyle.container, this.props.containerStyle]}>
{((this.props.leftComponent) ?
<Animated.View
style={{
@@ -385,7 +386,7 @@ const getSearchBarStyle = memoizeResult((
justifyContent: 'flex-start',
alignItems: 'center',
height: containerHeight,
flex: 1,
overflow: 'hidden',
},
clearIconColorIos: tintColorDelete || styles.defaultColor.color,
clearIconColorAndroid: titleCancelColor || placeholderTextColor,

View File

@@ -135,6 +135,7 @@ export default class ChannelsList extends PureComponent {
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
containerStyle={styles.searchBar}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
@@ -225,6 +226,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
right: 10,
top: 10,
},
searchBar: {
flex: 1,
overflow: 'visible',
},
searchContainer: {
flex: 1,
flexDirection: 'row',

View File

@@ -113,11 +113,15 @@ export default class MainSidebarIOS extends MainSidebarBase {
};
render() {
const {children} = this.props;
const {children, currentUserId} = this.props;
const {deviceWidth, openDrawerOffset} = this.state;
const isTablet = DeviceTypes.IS_TABLET && !this.state.isSplitView && this.state.permanentSidebar;
const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
if (!currentUserId) {
return null;
}
return (
<DrawerLayout
ref={this.drawerRef}

View File

@@ -71,7 +71,7 @@ export default class MainSidebarBase extends Component {
const condition = nextProps.currentTeamId !== currentTeamId ||
nextProps.teamsCount !== teamsCount ||
nextProps.theme !== theme;
nextProps.theme !== theme || this.props.children !== nextProps.children;
if (Platform.OS === 'ios') {
return condition ||
@@ -263,6 +263,10 @@ export default class MainSidebarBase extends Component {
selectChannel = (channel, currentChannelId, closeDrawer = true) => {
const {logChannelSwitch, handleSelectChannel} = this.props.actions;
if (channel.id === currentChannelId) {
return;
}
logChannelSwitch(channel.id, currentChannelId);
tracker.channelSwitch = Date.now();

View File

@@ -221,7 +221,7 @@ export default class SlideUpPanel extends PureComponent {
scrollToTop = () => {
if (this.scrollViewRef?.current) {
this.scrollViewRef.current._component.scrollTo({ //eslint-disable-line no-underscore-dangle
this.scrollViewRef.current.scrollTo({
x: 0,
y: 0,
animated: false,

View File

@@ -10,10 +10,10 @@ import TeamIcon from './team_icon';
function mapStateToProps(state, ownProps) {
const team = getTeam(state, ownProps.teamId);
const lastIconUpdate = team.last_team_icon_update;
const lastIconUpdate = team?.last_team_icon_update;
return {
displayName: team.display_name,
displayName: team?.display_name,
lastIconUpdate,
theme: getTheme(state),
};

View File

@@ -54,7 +54,6 @@ export default class TeamIcon extends React.PureComponent {
preloadTeamIcon = (teamId, lastIconUpdate) => {
const uri = Client4.getTeamIconUrl(teamId, lastIconUpdate);
FastImage.preload([{uri, headers: Client4.getOptions({}).headers}]);
this.setImageURL(uri);
};
@@ -87,7 +86,7 @@ export default class TeamIcon extends React.PureComponent {
} else {
teamIconContent = (
<Text style={[styles.text, styleText]}>
{displayName.substr(0, 2).toUpperCase()}
{displayName?.substr(0, 2).toUpperCase()}
</Text>
);
}

View File

@@ -106,17 +106,15 @@ export default class VideoControls extends PureComponent {
});
};
getPlayerStateIcon = (playerState) => {
getControlIconAndAspectRatio = (playerState) => {
switch (playerState) {
case PLAYER_STATE.PAUSED:
return playImage;
case PLAYER_STATE.PLAYING:
return pauseImage;
return {icon: pauseImage, aspectRatio: 0.83};
case PLAYER_STATE.ENDED:
return replayImage;
return {icon: replayImage, aspectRatio: 1.17};
}
return playImage;
return {icon: playImage, aspectRatio: 0.83};
};
handleAppStateChange = (nextAppState) => {
@@ -216,16 +214,16 @@ export default class VideoControls extends PureComponent {
};
setPlayerControls = (playerState) => {
const icon = this.getPlayerStateIcon(playerState);
const {icon, aspectRatio} = this.getControlIconAndAspectRatio(playerState);
const pressAction = playerState === PLAYER_STATE.ENDED ? this.onReplay : this.onPause;
return (
<TouchableOpacity
style={[styles.playButton, {backgroundColor: this.props.mainColor}]}
style={[styles.controlButton, {backgroundColor: this.props.mainColor}]}
onPress={pressAction}
>
<FastImage
source={icon}
style={styles.playIcon}
style={[styles.controlIcon, {aspectRatio}]}
/>
</TouchableOpacity>
);
@@ -288,7 +286,7 @@ const styles = StyleSheet.create({
timeRow: {
alignSelf: 'stretch',
},
playButton: {
controlButton: {
justifyContent: 'center',
alignItems: 'center',
width: 50,
@@ -297,15 +295,8 @@ const styles = StyleSheet.create({
borderWidth: 1.5,
borderColor: 'rgba(255,255,255,0.5)',
},
playIcon: {
width: 22,
height: 22,
resizeMode: 'contain',
},
replayIcon: {
width: 25,
controlIcon: {
height: 20,
resizeMode: 'stretch',
},
progressContainer: {
position: 'absolute',

View File

@@ -3,7 +3,7 @@
import {Alert, AppState, Dimensions, Linking, NativeModules, Platform} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import CookieManager from '@react-native-community/cookies';
import CookieManager from 'react-native-cookies';
import DeviceInfo from 'react-native-device-info';
import {getLocales} from 'react-native-localize';
import RNFetchBlob from 'rn-fetch-blob';
@@ -156,30 +156,35 @@ class GlobalEventHandler {
emmProvider.handleManagedConfig(true);
};
onLogout = async () => {
Store.redux.dispatch(closeWebSocket(false));
Store.redux.dispatch(setServerVersion(''));
if (analytics) {
await analytics.reset();
clearCookiesAndWebData = async () => {
try {
await CookieManager.clearAll(Platform.OS === 'ios');
} catch (error) {
// Nothing to clear
}
removeAppCredentials();
deleteFileCache();
await this.resetState();
resetMomentLocale();
switch (Platform.OS) {
case 'ios': {
const mainPath = RNFetchBlob.fs.dirs.DocumentDir.split('/').slice(0, -1).join('/');
const libraryDir = `${mainPath}/Library`;
const cookiesDir = `${libraryDir}/Cookies`;
const cookies = await RNFetchBlob.fs.exists(cookiesDir);
const webkitDir = `${libraryDir}/WebKit`;
const webkit = await RNFetchBlob.fs.exists(webkitDir);
// TODO: Handle when multi-server support is added
CookieManager.clearAll(Platform.OS === 'ios');
PushNotifications.clearNotifications();
const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
const mainPath = cacheDir.split('/').slice(0, -1).join('/');
if (cookies) {
RNFetchBlob.fs.unlink(cookiesDir);
}
mattermostBucket.removePreference('cert');
mattermostBucket.removePreference('emm');
if (Platform.OS === 'ios') {
mattermostBucket.removeFile('entities');
} else {
if (webkit) {
RNFetchBlob.fs.unlink(webkitDir);
}
break;
}
case 'android': {
const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
const mainPath = cacheDir.split('/').slice(0, -1).join('/');
const cookies = await RNFetchBlob.fs.exists(`${mainPath}/app_webview/Cookies`);
const cookiesJ = await RNFetchBlob.fs.exists(`${mainPath}/app_webview/Cookies-journal`);
if (cookies) {
@@ -189,7 +194,31 @@ class GlobalEventHandler {
if (cookiesJ) {
RNFetchBlob.fs.unlink(`${mainPath}/app_webview/Cookies-journal`);
}
break;
}
}
};
onLogout = async () => {
Store.redux.dispatch(closeWebSocket(false));
Store.redux.dispatch(setServerVersion(''));
if (analytics) {
await analytics.reset();
}
mattermostBucket.removePreference('cert');
mattermostBucket.removePreference('emm');
if (Platform.OS === 'ios') {
mattermostBucket.removeFile('entities');
}
removeAppCredentials();
deleteFileCache();
resetMomentLocale();
await this.clearCookiesAndWebData();
PushNotifications.clearNotifications();
if (this.launchApp) {
this.launchApp();
@@ -325,7 +354,7 @@ class GlobalEventHandler {
return Store.redux.dispatch({
type: General.OFFLINE_STORE_PURGE,
state: newState,
data: newState,
});
} catch (e) {
// clear error

View File

@@ -365,17 +365,20 @@ function myMembers(state: RelationOneToOne<Channel, ChannelMembership> = {}, act
case ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS: { // Used by the mobile app
const nextState: any = {...state};
const current = Object.values(nextState);
const {sync, channelMembers} = action.data;
const {sync, teamChannels, channelMembers} = action.data;
let hasNewValues = channelMembers && channelMembers.length > 0;
// Remove existing channel memberships when the user is no longer a member
if (sync) {
current.forEach((member: ChannelMembership) => {
const id = member.channel_id;
if (channelMembers.find((cm: ChannelMembership) => cm.channel_id !== id)) {
delete nextState[id];
hasNewValues = true;
}
const memberIds = channelMembers.map((cm: ChannelMembership) => cm.channel_id);
const removedMembers = current.filter((cm: ChannelMembership) => {
const id = cm.channel_id;
return !memberIds.includes(id) && teamChannels.includes(id);
});
removedMembers.forEach((member: ChannelMembership) => {
delete nextState[member.channel_id];
hasNewValues = true;
});
}

View File

@@ -11,7 +11,7 @@ export default ((state: Array<{error: any;displayable?: boolean;date: string}> =
return nextState;
}
case ErrorTypes.LOG_ERROR: {
const nextState = [...state];
const nextState = state.length ? [...state] : [];
const {displayable, error} = action;
nextState.push({
displayable,

View File

@@ -885,7 +885,7 @@ export const getDefaultChannelForTeams: (a: GlobalState) => RelationOneToOne<Tea
});
export const getMyFirstChannelForTeams: (a: GlobalState) => RelationOneToOne<Team, Channel> = createSelector(getAllChannels, getMyChannelMemberships, getMyTeams, getCurrentUser, (allChannels: IDMappedObjects<Channel>, myChannelMemberships: RelationOneToOne<Channel, ChannelMembership>, myTeams: Array<Team>, currentUser: UserProfile): RelationOneToOne<Team, Channel> => {
const locale = currentUser.locale || General.DEFAULT_LOCALE;
const locale = currentUser?.locale || General.DEFAULT_LOCALE;
const result: RelationOneToOne<Team, Channel> = {};
for (const team of myTeams) {

View File

@@ -2,5 +2,4 @@
// See LICENSE.txt for license information.
declare module 'gfycat-sdk';
declare module 'remote-redux-devtools';
declare module 'redux-action-buffer';
declare module 'redux-reset';
declare module 'redux-action-buffer';

View File

@@ -31,18 +31,10 @@ export default class ChannelAndroid extends ChannelBase {
render() {
const {theme} = this.props;
const channelLoadingOrFailed = this.renderLoadingOrFailedChannel();
if (channelLoadingOrFailed) {
return channelLoadingOrFailed;
}
let component = this.renderLoadingOrFailedChannel();
const drawerContent = (
<>
<ChannelNavBar
openMainSidebar={this.openMainSidebar}
openSettingsSidebar={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
if (!component) {
component = (
<KeyboardLayout>
<View style={style.flex}>
<ChannelPostList/>
@@ -52,6 +44,17 @@ export default class ChannelAndroid extends ChannelBase {
screenId={this.props.componentId}
/>
</KeyboardLayout>
);
}
const drawerContent = (
<>
<ChannelNavBar
openMainSidebar={this.openMainSidebar}
openSettingsSidebar={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
{component}
<NetworkIndicator/>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener/>}
</>

View File

@@ -54,10 +54,27 @@ export default class ChannelIOS extends ChannelBase {
render() {
const {currentChannelId, theme} = this.props;
let component = this.renderLoadingOrFailedChannel();
let renderDraftArea = false;
const channelLoadingOrFailed = this.renderLoadingOrFailedChannel();
if (channelLoadingOrFailed) {
return channelLoadingOrFailed;
if (!component) {
renderDraftArea = true;
component = (
<>
<ChannelPostList
updateNativeScrollView={this.updateNativeScrollView}
/>
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
/>
</View>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener/>}
</>
);
}
const style = getStyle(theme);
@@ -71,19 +88,9 @@ export default class ChannelIOS extends ChannelBase {
openSettingsSidebar={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
<ChannelPostList
updateNativeScrollView={this.updateNativeScrollView}
/>
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
/>
</View>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener/>}
{component}
</SafeAreaView>
{renderDraftArea &&
<KeyboardTrackingView
ref={this.keyboardTracker}
scrollViewNativeID={currentChannelId}
@@ -96,6 +103,7 @@ export default class ChannelIOS extends ChannelBase {
screenId={this.props.componentId}
/>
</KeyboardTrackingView>
}
</>
);

View File

@@ -7,14 +7,11 @@ import {intlShape} from 'react-intl';
import {
Keyboard,
StyleSheet,
View,
} from 'react-native';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {showModal, showModalOverCurrentContext} from '@actions/navigation';
import LocalConfig from '@assets/config';
import SafeAreaView from '@components/safe_area_view';
import EmptyToolbar from '@components/start/empty_toolbar';
import {NavigationTypes} from '@constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import EphemeralStore from '@store/ephemeral_store';
@@ -30,19 +27,19 @@ export let ClientUpgradeListener;
export default class ChannelBase extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getChannelStats: PropTypes.func.isRequired,
loadChannelsForTeam: PropTypes.func.isRequired,
selectDefaultTeam: PropTypes.func.isRequired,
selectInitialChannel: PropTypes.func.isRequired,
recordLoadTime: PropTypes.func.isRequired,
getChannelStats: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string,
currentTeamId: PropTypes.string,
isLandscape: PropTypes.bool,
disableTermsModal: PropTypes.bool,
teamName: PropTypes.string,
theme: PropTypes.object.isRequired,
showTermsOfService: PropTypes.bool,
disableTermsModal: PropTypes.bool,
skipMetrics: PropTypes.bool,
};
@@ -213,9 +210,10 @@ export default class ChannelBase extends PureComponent {
};
renderLoadingOrFailedChannel() {
const {formatMessage} = this.context.intl;
const {
currentChannelId,
isLandscape,
teamName,
theme,
} = this.props;
@@ -223,37 +221,28 @@ export default class ChannelBase extends PureComponent {
if (!currentChannelId) {
if (channelsRequestFailed) {
const FailedNetworkAction = require('app/components/failed_network_action').default;
const title = formatMessage({id: 'mobile.failed_network_action.teams_title', defaultMessage: 'Something went wrong'});
const message = formatMessage({
id: 'mobile.failed_network_action.teams_channel_description',
defaultMessage: 'Channels could not be loaded for {teamName}.',
}, {teamName});
return (
<SafeAreaView>
<View style={style.flex}>
<EmptyToolbar
theme={theme}
isLandscape={isLandscape}
/>
<FailedNetworkAction
onRetry={this.retryLoadChannels}
theme={theme}
/>
</View>
</SafeAreaView>
<FailedNetworkAction
errorMessage={message}
errorTitle={title}
onRetry={this.retryLoadChannels}
theme={theme}
/>
);
}
const Loading = require('app/components/channel_loader').default;
return (
<SafeAreaView>
<View style={style.flex}>
<EmptyToolbar
theme={theme}
isLandscape={isLandscape}
/>
<Loading
channelIsLoading={true}
color={theme.centerChannelColor}
/>
</View>
</SafeAreaView>
<Loading
channelIsLoading={true}
color={theme.centerChannelColor}
/>
);
}

View File

@@ -4,30 +4,24 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from '@mm-redux/actions/users';
import {loadChannelsForTeam, selectInitialChannel} from '@actions/views/channel';
import {recordLoadTime} from '@actions/views/root';
import {selectDefaultTeam} from '@actions/views/select_team';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {shouldShowTermsOfService} from '@mm-redux/selectors/entities/users';
import {getChannelStats} from '@mm-redux/actions/channels';
import {
loadChannelsForTeam,
selectInitialChannel,
} from 'app/actions/views/channel';
import {connection} from 'app/actions/device';
import {recordLoadTime} from 'app/actions/views/root';
import {logout} from 'app/actions/views/user';
import {selectDefaultTeam} from 'app/actions/views/select_team';
import {isLandscape} from 'app/selectors/device';
import Channel from './channel';
function mapStateToProps(state) {
const currentTeam = getCurrentTeam(state);
return {
currentTeamId: getCurrentTeamId(state),
currentTeamId: currentTeam?.id,
currentChannelId: getCurrentChannelId(state),
isLandscape: isLandscape(state),
teamName: currentTeam?.display_name,
theme: getTheme(state),
showTermsOfService: shouldShowTermsOfService(state),
};
@@ -37,14 +31,10 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getChannelStats,
connection,
loadChannelsForTeam,
logout,
selectDefaultTeam,
selectInitialChannel,
recordLoadTime,
startPeriodicStatusUpdates,
stopPeriodicStatusUpdates,
}, dispatch),
};
}

View File

@@ -334,25 +334,27 @@ export default class ChannelAddMembers extends PureComponent {
return (
<KeyboardLayout>
<StatusBar/>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
<View style={style.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
</View>
</View>
<CustomList
data={data}

View File

@@ -5,53 +5,54 @@ exports[`ChannelMembers should match snapshot 1`] = `
<Connect(StatusBar) />
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
</View>
<CustomList
canRefresh={true}

View File

@@ -363,25 +363,27 @@ export default class ChannelMembers extends PureComponent {
return (
<KeyboardLayout>
<StatusBar/>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
<View style={style.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
</View>
</View>
<CustomList
data={data}

View File

@@ -10,92 +10,12 @@ exports[`edit_profile should match snapshot 1`] = `
}
}
>
<ProfilePictureButton
blurTextBox={[Function]}
browseFileTypes="public.image"
canBrowseVideoLibrary={false}
canTakeVideo={false}
currentUser={
Object {
"email": "dwight@schrutefarms.com",
"first_name": "Dwight",
"last_name": "Schrute",
"nickname": "Dragon",
"position": "position",
"username": "ieatbeets",
}
}
maxFileSize={20971520}
onShowFileSizeWarning={[Function]}
onShowUnsupportedMimeTypeWarning={[Function]}
removeProfileImage={[Function]}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
uploadFiles={[Function]}
validMimeTypes={
Array [
"image/jpeg",
"image/jpeg",
"image/jpg",
"image/jp_",
"application/jpg",
"application/x-jpg",
"image/pjpeg",
"image/pipeg",
"image/vnd.swiftview-jpeg",
"image/x-xbitmap",
"image/png",
"application/png",
"application/x-png",
"image/bmp",
"image/x-bmp",
"image/x-bitmap",
"image/x-xbitmap",
"image/x-win-bitmap",
"image/x-windows-bmp",
"image/ms-bmp",
"image/x-ms-bmp",
"application/bmp",
"application/x-bmp",
"application/x-win-bitmap",
]
}
wrapper={true}
>
<Connect(ProfilePicture)
edit={true}
imageUri={null}
size={150}
statusBorderWidth={6}
statusSize={40}
/>
</ProfilePictureButton>
<Connect(ProfilePicture)
edit={false}
imageUri={null}
size={150}
statusBorderWidth={6}
statusSize={40}
/>
</View>
`;

View File

@@ -95,6 +95,7 @@ export default class EditProfile extends PureComponent {
lastNameDisabled: PropTypes.bool.isRequired,
nicknameDisabled: PropTypes.bool.isRequired,
positionDisabled: PropTypes.bool.isRequired,
profilePictureDisabled: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired,
commandType: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
@@ -504,6 +505,7 @@ export default class EditProfile extends PureComponent {
renderProfilePicture = () => {
const {
currentUser,
profilePictureDisabled,
theme,
} = this.props;
@@ -514,6 +516,25 @@ export default class EditProfile extends PureComponent {
const style = getStyleSheet(theme);
const uri = profileImage ? profileImage.uri : null;
const profilePicture = (
<ProfilePicture
userId={currentUser.id}
size={150}
statusBorderWidth={6}
statusSize={40}
edit={!profilePictureDisabled}
imageUri={uri}
profileImageRemove={profileImageRemove}
/>
);
if (profilePictureDisabled) {
return (
<View style={style.top}>
{profilePicture}
</View>
);
}
return (
<View style={style.top}>
@@ -532,15 +553,7 @@ export default class EditProfile extends PureComponent {
onShowUnsupportedMimeTypeWarning={this.onShowUnsupportedMimeTypeWarning}
validMimeTypes={VALID_MIME_TYPES}
>
<ProfilePicture
userId={currentUser.id}
size={150}
statusBorderWidth={6}
statusSize={40}
edit={true}
imageUri={uri}
profileImageRemove={profileImageRemove}
/>
{profilePicture}
</ProfilePictureButton>
</View>
);

View File

@@ -32,6 +32,7 @@ describe('edit_profile', () => {
lastNameDisabled: true,
nicknameDisabled: true,
positionDisabled: true,
profilePictureDisabled: true,
theme: Preferences.THEMES.default,
currentUser: {
first_name: 'Dwight',
@@ -58,6 +59,7 @@ describe('edit_profile', () => {
const wrapper = shallow(
<EditProfile
{...baseProps}
profilePictureDisabled={false}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);

View File

@@ -26,12 +26,12 @@ function mapStateToProps(state, ownProps) {
const nicknameDisabled = (service === 'ldap' && config.LdapNicknameAttributeSet === 'true') ||
(service === 'saml' && config.SamlNicknameAttributeSet === 'true');
let positionDisabled = false;
if (isMinimumServerVersion(serverVersion, 5, 12)) {
positionDisabled = (service === 'ldap' && config.LdapPositionAttributeSet === 'true') ||
(service === 'saml' && config.SamlPositionAttributeSet === 'true');
} else {
positionDisabled = (service === 'ldap' || service === 'saml') && config.PositionAttribute === 'true';
const positionDisabled = (service === 'ldap' && config.LdapPositionAttributeSet === 'true') ||
(service === 'saml' && config.SamlPositionAttributeSet === 'true');
let profilePictureDisabled = false;
if (isMinimumServerVersion(serverVersion, 5, 24)) {
profilePictureDisabled = (service === 'ldap' || service === 'saml') && config.LdapPictureAttributeSet === 'true';
}
return {
@@ -39,6 +39,7 @@ function mapStateToProps(state, ownProps) {
lastNameDisabled,
nicknameDisabled,
positionDisabled,
profilePictureDisabled,
theme: getTheme(state),
isLandscape: isLandscape(state),
};

View File

@@ -10,11 +10,10 @@ exports[`ErrorTeamsList should match snapshot 1`] = `
>
<Connect(StatusBar) />
<FailedNetworkAction
actionDefaultMessage="try again"
actionId="mobile.failed_network_action.retry"
errorDefaultMessage="Messages will load when you have an internet connection or {tryAgainAction}."
errorId="mobile.failed_network_action.shortDescription"
errorMessage="Teams could not be loaded."
errorTitle="Something went wrong"
onRetry={[Function]}
showAction={true}
theme={
Object {
"awayIndicator": "#ffbc42",

View File

@@ -3,6 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
InteractionManager,
StyleSheet,
@@ -26,6 +27,10 @@ export default class ErrorTeamsList extends PureComponent {
theme: PropTypes.object,
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
@@ -67,16 +72,25 @@ export default class ErrorTeamsList extends PureComponent {
}
render() {
const {formatMessage} = this.context.intl;
const {theme} = this.props;
if (this.state.loading) {
return <Loading color={theme.centerChannelColor}/>;
}
const title = formatMessage({id: 'mobile.failed_network_action.teams_title', defaultMessage: 'Something went wrong'});
const message = formatMessage({
id: 'mobile.failed_network_action.teams_description',
defaultMessage: 'Teams could not be loaded.',
});
return (
<View style={style.container}>
<StatusBar/>
<FailedNetworkAction
errorMessage={message}
errorTitle={title}
onRetry={this.getUserInfo}
theme={theme}
/>

View File

@@ -2,11 +2,11 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import FailedNetworkAction from '@components/failed_network_action';
import Preferences from '@mm-redux/constants/preferences';
import {shallowWithIntl} from 'test/intl-test-helper.js';
import FailedNetworkAction from 'app/components/failed_network_action';
import ErrorTeamsList from './error_teams_list';
describe('ErrorTeamsList', () => {
@@ -28,7 +28,7 @@ describe('ErrorTeamsList', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<ErrorTeamsList {...baseProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
@@ -51,7 +51,7 @@ describe('ErrorTeamsList', () => {
actions,
};
const wrapper = shallow(
const wrapper = shallowWithIntl(
<ErrorTeamsList {...newProps}/>,
);

View File

@@ -40,11 +40,8 @@ exports[`FlaggedPosts should match snapshot when getFlaggedPosts failed 1`] = `
>
<Connect(StatusBar) />
<FailedNetworkAction
actionDefaultMessage="try again"
actionId="mobile.failed_network_action.retry"
errorDefaultMessage="Messages will load when you have an internet connection or {tryAgainAction}."
errorId="mobile.failed_network_action.shortDescription"
onRetry={[Function]}
showAction={true}
theme={
Object {
"awayIndicator": "#ffbc42",

View File

@@ -16,10 +16,10 @@ export function registerScreens(store, Provider) {
Root = require('app/components/root').default;
}
const wrapper = (Comp) => (props) => ( // eslint-disable-line react/display-name
const wrapper = (Comp, excludeEvents = true) => (props) => ( // eslint-disable-line react/display-name
<Provider store={store}>
<ThemeProvider>
<Root>
<Root excludeEvents={excludeEvents}>
<Comp {...props}/>
</Root>
</ThemeProvider>
@@ -29,7 +29,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('About', () => wrapper(require('app/screens/about').default), () => require('app/screens/about').default);
Navigation.registerComponent('AddReaction', () => wrapper(require('app/screens/add_reaction').default), () => require('app/screens/add_reaction').default);
Navigation.registerComponent('AdvancedSettings', () => wrapper(require('app/screens/settings/advanced_settings').default), () => require('app/screens/settings/advanced_settings').default);
Navigation.registerComponent('Channel', () => wrapper(require('app/screens/channel').default), () => require('app/screens/channel').default);
Navigation.registerComponent('Channel', () => wrapper(require('app/screens/channel').default, false), () => require('app/screens/channel').default);
Navigation.registerComponent('ChannelAddMembers', () => wrapper(require('app/screens/channel_add_members').default), () => require('app/screens/channel_add_members').default);
Navigation.registerComponent('ChannelInfo', () => wrapper(require('app/screens/channel_info').default), () => require('app/screens/channel_info').default);
Navigation.registerComponent('ChannelMembers', () => wrapper(require('app/screens/channel_members').default), () => require('app/screens/channel_members').default);
@@ -52,7 +52,11 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('MFA', () => wrapper(require('app/screens/mfa').default), () => require('app/screens/mfa').default);
Navigation.registerComponent('MoreChannels', () => wrapper(require('app/screens/more_channels').default), () => require('app/screens/more_channels').default);
Navigation.registerComponent('MoreDirectMessages', () => wrapper(require('app/screens/more_dms').default), () => require('app/screens/more_dms').default);
Navigation.registerComponent('Notification', () => gestureHandlerRootHOC(wrapper(require('app/screens/notification').default)), () => require('app/screens/notification').default);
if (Platform.OS === 'android') {
Navigation.registerComponent('Notification', () => gestureHandlerRootHOC(wrapper(require('app/screens/notification').default), {flex: undefined, height: 100}), () => require('app/screens/notification').default);
} else {
Navigation.registerComponent('Notification', () => wrapper(require('app/screens/notification').default), () => require('app/screens/notification').default);
}
Navigation.registerComponent('NotificationSettings', () => wrapper(require('app/screens/settings/notification_settings').default), () => require('app/screens/settings/notification_settings').default);
Navigation.registerComponent('NotificationSettingsAutoResponder', () => wrapper(require('app/screens/settings/notification_settings_auto_responder').default), () => require('app/screens/settings/notification_settings_auto_responder').default);
Navigation.registerComponent('NotificationSettingsEmail', () => wrapper(require('app/screens/settings/notification_settings_email').default), () => require('app/screens/settings/notification_settings_email').default);
@@ -67,7 +71,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('RecentMentions', () => wrapper(require('app/screens/recent_mentions').default), () => require('app/screens/recent_mentions').default);
Navigation.registerComponent('Search', () => wrapper(require('app/screens/search').default), () => require('app/screens/search').default);
Navigation.registerComponent('SelectorScreen', () => wrapper(require('app/screens/selector_screen').default), () => require('app/screens/selector_screen').default);
Navigation.registerComponent('SelectServer', () => wrapper(require('app/screens/select_server').default), () => require('app/screens/select_server').default);
Navigation.registerComponent('SelectServer', () => wrapper(require('app/screens/select_server').default, false), () => require('app/screens/select_server').default);
Navigation.registerComponent('SelectTeam', () => wrapper(require('app/screens/select_team').default), () => require('app/screens/select_team').default);
Navigation.registerComponent('SelectTimezone', () => wrapper(require('app/screens/settings/timezone/select_timezone').default), () => require('app/screens/settings/timezone/select_timezone').default);
Navigation.registerComponent('Settings', () => wrapper(require('app/screens/settings/general').default), () => require('app/screens/settings/general').default);

View File

@@ -25,7 +25,6 @@ import FormattedText from '@components/formatted_text';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import StatusBar from '@components/status_bar';
import {t} from '@utils/i18n';
import {setMfaPreflightDone, getMfaPreflightDone} from '@utils/security';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
import tracker from '@utils/time_tracker';
@@ -68,7 +67,6 @@ export default class Login extends PureComponent {
componentDidMount() {
Dimensions.addEventListener('change', this.orientationDidChange);
setMfaPreflightDone(false);
this.setEmmUsernameIfAvailable();
}
@@ -92,7 +90,7 @@ export default class Login extends PureComponent {
const loginId = this.loginId;
const password = this.password;
goToScreen(screen, title, {onMfaComplete: this.checkLoginResponse, goToChannel: this.goToChannel, loginId, password});
goToScreen(screen, title, {goToChannel: this.goToChannel, loginId, password});
};
blur = () => {
@@ -182,9 +180,6 @@ export default class Login extends PureComponent {
if (!errorId) {
return error.message;
}
if (mfaExpectedErrors.includes(errorId) && !getMfaPreflightDone()) {
return null;
}
if (
errorId === 'store.sql_user.get_for_login.app_error' ||
errorId === 'ent.ldap.do_login.user_not_registered.app_error'

View File

@@ -94,7 +94,6 @@ describe('Login', () => {
'Multi-factor Authentication',
{
goToChannel: wrapper.instance().goToChannel,
onMfaComplete: wrapper.instance().checkLoginResponse,
loginId,
password,
},

View File

@@ -21,6 +21,7 @@ import FormattedText from '@components/formatted_text';
import StatusBar from '@components/status_bar';
import {paddingHorizontal as padding} from '@components/safe_area_view/iphone_x_spacing';
import {ViewTypes} from '@constants';
import globalEventHandler from '@init/global_event_handler';
import {preventDoubleTap} from '@utils/tap';
import {GlobalStyles} from 'app/styles';
@@ -44,19 +45,21 @@ export default class LoginOptions extends PureComponent {
Dimensions.removeEventListener('change', this.orientationDidChange);
}
goToLogin = preventDoubleTap(() => {
goToLogin = preventDoubleTap(async () => {
const {intl} = this.context;
const screen = 'Login';
const title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'});
globalEventHandler.clearCookiesAndWebData();
goToScreen(screen, title);
});
goToSSO = (ssoType) => {
goToSSO = async (ssoType) => {
const {intl} = this.context;
const screen = 'SSO';
const title = intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'});
globalEventHandler.clearCookiesAndWebData();
goToScreen(screen, title, {ssoType});
};

View File

@@ -15,14 +15,12 @@ import {
} from 'react-native';
import Button from 'react-native-button';
import {popTopScreen} from '@actions/navigation';
import ErrorText from '@components/error_text';
import FormattedText from '@components/formatted_text';
import StatusBar from '@components/status_bar';
import TextInputWithLocalizedPlaceholder from '@components/text_input_with_localized_placeholder';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
import {setMfaPreflightDone} from '@utils/security';
import {GlobalStyles} from 'app/styles';
@@ -34,7 +32,6 @@ export default class Mfa extends PureComponent {
goToChannel: PropTypes.func.isRequired,
loginId: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
onMfaComplete: PropTypes.func.isRequired,
};
constructor(props) {
@@ -79,7 +76,7 @@ export default class Mfa extends PureComponent {
};
submit = preventDoubleTap(() => {
const {actions, goToChannel, loginId, password, onMfaComplete} = this.props;
const {actions, goToChannel, loginId, password} = this.props;
const {token} = this.state;
Keyboard.dismiss();
@@ -94,16 +91,16 @@ export default class Mfa extends PureComponent {
});
return;
}
setMfaPreflightDone(true);
this.setState({isLoading: true});
actions.login(loginId, password, token).then((result) => {
this.setState({isLoading: false});
if (onMfaComplete(result)) {
goToChannel();
if (result.error) {
this.setState({error: result.error});
return;
}
popTopScreen();
goToChannel();
});
});

View File

@@ -6,51 +6,52 @@ exports[`MoreChannels should match snapshot 1`] = `
<React.Fragment>
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
</View>
<View
style={

View File

@@ -452,25 +452,27 @@ export default class MoreChannels extends PureComponent {
content = (
<React.Fragment>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.searchChannels}
onSearchButtonPress={this.searchChannels}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
<View style={style.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.searchChannels}
onSearchButtonPress={this.searchChannels}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
</View>
</View>
{channelDropdown}
<CustomList

View File

@@ -52,7 +52,7 @@ export default class MoreDirectMessages extends PureComponent {
currentDisplayName: PropTypes.string,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
isGuest: PropTypes.object.isRequired,
isGuest: PropTypes.bool,
restrictDirectMessage: PropTypes.bool.isRequired,
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
@@ -472,25 +472,27 @@ export default class MoreDirectMessages extends PureComponent {
return (
<KeyboardLayout>
<StatusBar/>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
<View style={style.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
</View>
</View>
<SelectedUsers
selectedIds={this.state.selectedIds}
@@ -501,6 +503,7 @@ export default class MoreDirectMessages extends PureComponent {
<CustomList
data={data}
extraData={selectedIds}
isLandscape={isLandscape}
key='custom_list'
listType={listType}
loading={loading}

View File

@@ -110,7 +110,7 @@ export default class OptionsModal extends PureComponent {
const style = StyleSheet.create({
wrapper: {
backgroundColor: Platform.select({ios: 'rgba(0, 0, 0, 0.5)'}),
backgroundColor: 'rgba(0, 0, 0, 0.5)',
flex: 1,
},
});

View File

@@ -40,11 +40,8 @@ exports[`PinnedPosts should match snapshot when getPinnedPosts failed 1`] = `
>
<Connect(StatusBar) />
<FailedNetworkAction
actionDefaultMessage="try again"
actionId="mobile.failed_network_action.retry"
errorDefaultMessage="Messages will load when you have an internet connection or {tryAgainAction}."
errorId="mobile.failed_network_action.shortDescription"
onRetry={[Function]}
showAction={true}
theme={
Object {
"awayIndicator": "#ffbc42",

View File

@@ -19,7 +19,7 @@ function makeMapStateToProps() {
const getProfilesByIdsAndUsernames = makeGetProfilesByIdsAndUsernames();
return function mapStateToProps(state, ownProps) {
const reactions = getReactionsForPostSelector(state, ownProps.postId);
const reactions = getReactionsForPostSelector(state, ownProps.postId) || undefined;
const allUserIds = getUniqueUserIds(reactions);
return {

View File

@@ -40,11 +40,8 @@ exports[`RecentMentions should match snapshot when getRecentMentions failed 1`]
>
<Connect(StatusBar) />
<FailedNetworkAction
actionDefaultMessage="try again"
actionId="mobile.failed_network_action.retry"
errorDefaultMessage="Messages will load when you have an internet connection or {tryAgainAction}."
errorId="mobile.failed_network_action.shortDescription"
onRetry={[Function]}
showAction={true}
theme={
Object {
"awayIndicator": "#ffbc42",

View File

@@ -713,7 +713,7 @@ export default class Search extends PureComponent {
paddingRes.paddingLeft = null;
if (isLandscape) {
paddingRes.paddingTop = 10;
paddingRes.paddingTop = 5;
}
}

View File

@@ -147,19 +147,19 @@ export default class SelectServer extends PureComponent {
}
};
getUrl = () => {
const urlParse = require('url-parse');
let preUrl = urlParse(this.state.url, true);
getUrl = async (serverUrl, useHttp = false) => {
let url = this.sanitizeUrl(serverUrl, useHttp);
if (!preUrl.host || preUrl.protocol === 'file:') {
preUrl = urlParse('https://' + stripTrailingSlashes(this.state.url), true);
try {
const resp = await fetch(url, {method: 'HEAD'});
if (resp?.rnfbRespInfo?.redirects?.length) {
url = resp.rnfbRespInfo.redirects[resp.rnfbRespInfo.redirects.length - 1];
}
} catch {
// do nothing
}
if (preUrl.protocol === 'http:') {
preUrl.protocol = 'https:';
}
return stripTrailingSlashes(preUrl.protocol + '//' + preUrl.host + preUrl.pathname);
return this.sanitizeUrl(url, useHttp);
};
goToNextScreen = (screen, title, passProps = {}, navOptions = {}) => {
@@ -187,8 +187,6 @@ export default class SelectServer extends PureComponent {
};
handleConnect = preventDoubleTap(async () => {
const url = this.getUrl();
Keyboard.dismiss();
if (this.state.connecting || this.state.connected) {
@@ -197,7 +195,7 @@ export default class SelectServer extends PureComponent {
return;
}
if (!isValidUrl(url)) {
if (!isValidUrl(this.sanitizeUrl(this.state.url))) {
this.setState({
error: {
intl: {
@@ -219,11 +217,11 @@ export default class SelectServer extends PureComponent {
auto: true,
certificate,
}).build();
this.pingServer(url);
this.pingServer(this.state.url);
}
});
} else {
this.pingServer(url);
this.pingServer(this.state.url);
}
});
@@ -301,7 +299,7 @@ export default class SelectServer extends PureComponent {
resetToChannel();
};
pingServer = (url, retryWithHttp = true) => {
pingServer = async (url, retryWithHttp = true) => {
const {
getPing,
handleServerUrlChanged,
@@ -315,9 +313,6 @@ export default class SelectServer extends PureComponent {
error: null,
});
Client4.setUrl(url);
handleServerUrlChanged(url);
let cancel = false;
this.cancelPing = () => {
cancel = true;
@@ -330,13 +325,20 @@ export default class SelectServer extends PureComponent {
this.cancelPing = null;
};
getPing().then((result) => {
const serverUrl = await this.getUrl(url, !retryWithHttp);
Client4.setUrl(serverUrl);
handleServerUrlChanged(serverUrl);
try {
const result = await getPing();
if (cancel) {
return;
}
if (result.error && retryWithHttp) {
this.pingServer(url.replace('https:', 'http:'), false);
const nurl = serverUrl.replace('https:', 'http:');
this.pingServer(nurl, false);
return;
}
@@ -350,7 +352,7 @@ export default class SelectServer extends PureComponent {
connecting: false,
error: result.error,
});
}).catch(() => {
} catch {
if (cancel) {
return;
}
@@ -358,9 +360,23 @@ export default class SelectServer extends PureComponent {
this.setState({
connecting: false,
});
});
}
};
sanitizeUrl = (url, useHttp = false) => {
const urlParse = require('url-parse');
let preUrl = urlParse(url, true);
if (!preUrl.host || preUrl.protocol === 'file:') {
preUrl = urlParse('https://' + stripTrailingSlashes(url), true);
}
if (preUrl.protocol === 'http:' && !useHttp) {
preUrl.protocol = 'https:';
}
return stripTrailingSlashes(preUrl.protocol + '//' + preUrl.host + preUrl.pathname);
}
scheduleSessionExpiredNotification = () => {
const {intl} = this.context;
const {actions} = this.props;

View File

@@ -2,11 +2,8 @@
exports[`SelectTeam should match snapshot for fail of teams 1`] = `
<FailedNetworkAction
actionDefaultMessage="try again"
actionId="mobile.failed_network_action.retry"
errorDefaultMessage="Messages will load when you have an internet connection or {tryAgainAction}."
errorId="mobile.failed_network_action.shortDescription"
onRetry={[Function]}
showAction={true}
theme={
Object {
"awayIndicator": "#ffbc42",

View File

@@ -11,53 +11,54 @@ exports[`SelectorScreen should match snapshot for channels 1`] = `
<Connect(StatusBar) />
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
</View>
<CustomList
canRefresh={true}
@@ -116,53 +117,54 @@ exports[`SelectorScreen should match snapshot for channels 2`] = `
<Connect(StatusBar) />
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
</View>
<CustomList
canRefresh={true}
@@ -221,53 +223,54 @@ exports[`SelectorScreen should match snapshot for explicit options 1`] = `
<Connect(StatusBar) />
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
</View>
<CustomList
canRefresh={true}
@@ -333,53 +336,54 @@ exports[`SelectorScreen should match snapshot for searching 1`] = `
<Connect(StatusBar) />
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value="name2"
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value="name2"
/>
</View>
</View>
<CustomList
canRefresh={true}
@@ -438,53 +442,54 @@ exports[`SelectorScreen should match snapshot for users 1`] = `
<Connect(StatusBar) />
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
</View>
<CustomList
canRefresh={true}
@@ -543,53 +548,54 @@ exports[`SelectorScreen should match snapshot for users 2`] = `
<Connect(StatusBar) />
<View
style={
Array [
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
},
null,
]
Object {
"height": 38,
"marginVertical": 5,
"paddingLeft": 8,
}
}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
<View
style={null}
>
<Search
autoCapitalize="none"
backArrowSize={24}
backgroundColor="transparent"
blurOnSubmit={false}
cancelTitle="Cancel"
containerHeight={40}
deleteIconSize={20}
editable={true}
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"color": "#3d3c40",
"fontSize": 15,
}
}
}
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
keyboardAppearance="light"
keyboardShouldPersist={false}
keyboardType="default"
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
returnKeyType="search"
searchBarRightMargin={0}
searchIconSize={24}
showArrow={false}
showCancel={true}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.5)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
</View>
<CustomList
canRefresh={true}

View File

@@ -316,25 +316,27 @@ export default class SelectorScreen extends PureComponent {
return (
<View style={style.container}>
<StatusBar/>
<View style={[style.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
<View style={style.searchBar}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
</View>
</View>
<CustomList
data={data}

View File

@@ -32,7 +32,7 @@ class Settings extends PureComponent {
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
errors: PropTypes.array.isRequired,
errors: PropTypes.object.isRequired,
intl: intlShape.isRequired,
joinableTeams: PropTypes.array.isRequired,
theme: PropTypes.object,

View File

@@ -112,26 +112,28 @@ export default class Timezone extends PureComponent {
return (
<View style={style.container}>
<StatusBar/>
<View style={[style.header, padding(isLandscape)]}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={Platform.OS === 'ios' ? 33 : 46}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
onChangeText={this.handleTextChanged}
autoCapitalize='none'
value={value}
containerStyle={style.searchBarContainer}
showArrow={false}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
<View style={style.header}>
<View style={padding(isLandscape)}>
<SearchBar
ref={this.setSearchBarRef}
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={Platform.OS === 'ios' ? 33 : 46}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
onChangeText={this.handleTextChanged}
autoCapitalize='none'
value={value}
containerStyle={style.searchBarContainer}
showArrow={false}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
/>
</View>
</View>
<FlatList
data={this.filteredTimezones(value)}

View File

@@ -10,7 +10,7 @@ import {
Platform,
} from 'react-native';
import {WebView} from 'react-native-webview';
import CookieManager from '@react-native-community/cookies';
import CookieManager from 'react-native-cookies';
import urlParse from 'url-parse';
import {Client4} from '@mm-redux/client';
@@ -27,7 +27,7 @@ const HEADERS = {
'X-Mobile-App': 'mattermost',
};
const postMessageJS = "window.ReactNativeWebView.postMessage(document.body.innerText, '*');";
const postMessageJS = "window.postMessage(document.body.innerText, '*');";
// Used to make sure that OneLogin forms scale appropriately on both platforms.
const oneLoginFormScalingJS = `
@@ -87,15 +87,15 @@ class SSO extends PureComponent {
switch (props.ssoType) {
case ViewTypes.GITLAB:
this.loginUrl = `${props.serverUrl}/oauth/gitlab/mobile_login`;
this.completedUrl = '/signup/gitlab/complete';
this.completeUrlPath = '/signup/gitlab/complete';
break;
case ViewTypes.SAML:
this.loginUrl = `${props.serverUrl}/login/sso/saml?action=mobile`;
this.completedUrl = '/login/sso/saml';
this.completeUrlPath = '/login/sso/saml';
break;
case ViewTypes.OFFICE365:
this.loginUrl = `${props.serverUrl}/oauth/office365/mobile_login`;
this.completedUrl = '/signup/office365/complete';
this.completeUrlPath = '/signup/office365/complete';
break;
}
@@ -104,6 +104,45 @@ class SSO extends PureComponent {
}
}
componentWillUnmount() {
clearTimeout(this.cookiesTimeout);
}
extractCookie = (parsedUrl) => {
const original = urlParse(this.props.serverUrl);
// Check whether we need to set a sub-path
parsedUrl.set('pathname', original.pathname || '');
parsedUrl.set('query', '');
Client4.setUrl(parsedUrl.href);
CookieManager.get(parsedUrl.href, true).then((res) => {
const mmtoken = res.MMAUTHTOKEN;
const token = typeof mmtoken === 'object' ? mmtoken.value : mmtoken;
if (token) {
clearTimeout(this.cookiesTimeout);
this.setState({renderWebView: false});
const {
ssoLogin,
} = this.props.actions;
Client4.setToken(token);
ssoLogin(token).then((result) => {
if (result.error) {
this.onLoadEndError(result.error);
return;
}
this.goToChannel();
});
} else if (this.webView && !this.state.error) {
this.webView.injectJavaScript(postMessageJS);
this.cookiesTimeout = setTimeout(this.extractCookie.bind(null, parsedUrl), 250);
}
});
}
goToChannel = () => {
tracker.initialLoad = Date.now();
@@ -122,6 +161,7 @@ class SSO extends PureComponent {
status_code: statusCode,
} = response;
if (id && message && statusCode !== 200) {
clearTimeout(this.cookiesTimeout);
this.setState({error: message});
}
}
@@ -139,7 +179,7 @@ class SSO extends PureComponent {
if (parsed.host.includes('.onelogin.com')) {
nextState.jsCode = oneLoginFormScalingJS;
} else if (parsed.pathname === this.completedUrl) {
} else if (parsed.pathname === this.completeUrlPath) {
// To avoid `window.postMessage` conflicts in any of the SSO flows
// we enable the onMessage handler only When the webView navigates to the final SSO URL.
nextState.messagingEnabled = true;
@@ -150,29 +190,15 @@ class SSO extends PureComponent {
onLoadEnd = (event) => {
const url = event.nativeEvent.url;
if (url.includes(this.completedUrl)) {
CookieManager.get(this.props.serverUrl, this.useWebkit).then((res) => {
const mmtoken = res.MMAUTHTOKEN;
const token = typeof mmtoken === 'object' ? mmtoken.value : mmtoken;
const parsed = urlParse(url);
if (token) {
this.setState({renderWebView: false});
const {
ssoLogin,
} = this.props.actions;
let isLastRedirect = url.includes(this.completeUrlPath);
if (this.props.ssoType === ViewTypes.SAML) {
isLastRedirect = isLastRedirect && !parsed.query;
}
Client4.setToken(token);
ssoLogin(token).then((result) => {
if (result.error) {
this.onLoadEndError(result.error);
return;
}
this.goToChannel();
});
} else if (this.webView && !this.state.error) {
this.webView.injectJavaScript(postMessageJS);
}
});
if (isLastRedirect) {
this.extractCookie(parsed);
}
};
@@ -214,7 +240,7 @@ class SSO extends PureComponent {
<WebView
ref={this.webViewRef}
source={{uri: this.loginUrl, headers: HEADERS}}
javaScriptEnabled={true}
javaScriptEnabledAndroid={true}
automaticallyAdjustContentInsets={false}
startInLoadingState={true}
onNavigationStateChange={this.onNavigationStateChange}
@@ -222,7 +248,7 @@ class SSO extends PureComponent {
injectedJavaScript={jsCode}
onLoadEnd={this.onLoadEnd}
onMessage={messagingEnabled ? this.onMessage : null}
sharedCookiesEnabled={Platform.OS === 'android'}
useSharedProcessPool={true}
cacheEnabled={false}
/>
);

View File

@@ -288,11 +288,8 @@ exports[`TermsOfService should match snapshot for fail of get terms 1`] = `
>
<Connect(StatusBar) />
<FailedNetworkAction
actionDefaultMessage="try again"
actionId="mobile.failed_network_action.retry"
errorDefaultMessage="Messages will load when you have an internet connection or {tryAgainAction}."
errorId="mobile.failed_network_action.shortDescription"
onRetry={[Function]}
showAction={true}
theme={
Object {
"awayIndicator": "#ffbc42",

131
app/selectors/emojis.js Normal file
View File

@@ -0,0 +1,131 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createSelector} from 'reselect';
import {t} from '@utils/i18n';
import {getCustomEmojisByName as selectCustomEmojisByName} from '@mm-redux/selectors/entities/emojis';
import {createIdsSelector} from '@mm-redux/utils/helpers';
import {BuiltInEmojis, CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from '@utils/emojis';
const categoryToI18n = {
activity: {
id: t('mobile.emoji_picker.activity'),
defaultMessage: 'ACTIVITY',
icon: 'futbol-o',
},
custom: {
id: t('mobile.emoji_picker.custom'),
defaultMessage: 'CUSTOM',
icon: 'at',
},
flags: {
id: t('mobile.emoji_picker.flags'),
defaultMessage: 'FLAGS',
icon: 'flag-o',
},
foods: {
id: t('mobile.emoji_picker.foods'),
defaultMessage: 'FOODS',
icon: 'cutlery',
},
nature: {
id: t('mobile.emoji_picker.nature'),
defaultMessage: 'NATURE',
icon: 'leaf',
},
objects: {
id: t('mobile.emoji_picker.objects'),
defaultMessage: 'OBJECTS',
icon: 'lightbulb-o',
},
people: {
id: t('mobile.emoji_picker.people'),
defaultMessage: 'PEOPLE',
icon: 'smile-o',
},
places: {
id: t('mobile.emoji_picker.places'),
defaultMessage: 'PLACES',
icon: 'plane',
},
recent: {
id: t('mobile.emoji_picker.recent'),
defaultMessage: 'RECENTLY USED',
icon: 'clock-o',
},
symbols: {
id: t('mobile.emoji_picker.symbols'),
defaultMessage: 'SYMBOLS',
icon: 'heart-o',
},
};
function fillEmoji(indice) {
const emoji = Emojis[indice];
return {
name: emoji.aliases[0],
aliases: emoji.aliases,
};
}
export const selectEmojisByName = createIdsSelector(
selectCustomEmojisByName,
(customEmojis) => {
const emoticons = new Set();
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
emoticons.add(key);
}
return Array.from(emoticons);
},
);
export const selectEmojisBySection = createSelector(
selectCustomEmojisByName,
(state) => state.views.recentEmojis,
(customEmojis, recentEmojis) => {
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
const items = EmojiIndicesByCategory.get(category).map(fillEmoji);
const section = {
...categoryToI18n[category],
key: category,
data: items,
};
return section;
});
const customEmojiItems = [];
BuiltInEmojis.forEach((emoji) => {
customEmojiItems.push({
name: emoji,
});
});
for (const [key] of customEmojis) {
customEmojiItems.push({
name: key,
});
}
emoticons.push({
...categoryToI18n.custom,
key: 'custom',
data: customEmojiItems,
});
if (recentEmojis.length) {
const items = recentEmojis.map((emoji) => ({name: emoji}));
emoticons.unshift({
...categoryToI18n.recent,
key: 'recent',
data: items,
});
}
return emoticons;
},
);

View File

@@ -5,11 +5,11 @@ import AsyncStorage from '@react-native-community/async-storage';
import * as redux from 'redux';
import {createPersistoid, createTransform, persistReducer, persistStore, Persistor, PersistConfig} from 'redux-persist';
import {createBlacklistFilter} from 'redux-persist-transform-filter';
import reduxReset from 'redux-reset';
import DeviceInfo from 'react-native-device-info';
import {General} from '@mm-redux/constants';
import serviceReducer from '@mm-redux/reducers';
import {GenericAction} from '@mm-redux/types/actions';
import {GlobalState} from '@mm-redux/types/store';
import initialState from '@store/initial_state';
@@ -178,11 +178,22 @@ export default function configureStore(storage: any, preloadedState: any = {}, o
emojiBlackListFilter,
],
throttle: 100,
timeout: 60000,
};
const persistConfig: PersistConfig<GlobalState> = Object.assign({}, defaultConfig, optionalConfig);
const baseState: any = Object.assign({}, initialState, preloadedState);
const rootReducer: any = createReducer(serviceReducer as any, appReducer as any);
const baseReducer: any = createReducer(serviceReducer as any, appReducer as any);
const rootReducer: any = (state: GlobalState, action: GenericAction) => {
if (action.type === General.OFFLINE_STORE_PURGE) {
// eslint-disable-next-line no-underscore-dangle
if (action.data?._persist) {
delete action?.data?._persist;
}
return baseReducer(action.data, action as any);
}
return baseReducer(state as any, action as any);
};
const persistedReducer = persistReducer({...persistConfig}, rootReducer);
const options: ClientOptions = Object.assign({}, defaultOptions, optionalOptions);
@@ -193,7 +204,6 @@ export default function configureStore(storage: any, preloadedState: any = {}, o
redux.applyMiddleware(
...createMiddlewares(options),
),
reduxReset(General.OFFLINE_STORE_PURGE),
),
);
@@ -222,7 +232,7 @@ export default function configureStore(storage: any, preloadedState: any = {}, o
store.dispatch({
type: General.OFFLINE_STORE_PURGE,
state,
data: state,
});
console.log('HYDRATED FROM v4', storeKeys); // eslint-disable-line no-console
@@ -232,6 +242,8 @@ export default function configureStore(storage: any, preloadedState: any = {}, o
});
store.dispatch({type: General.REHYDRATED});
AsyncStorage.multiRemove(storeKeys);
} else if (store.getState()._persist?.rehydrated) { // eslint-disable-line no-underscore-dangle
store.dispatch({type: General.REHYDRATED});
} else {
let executed = false;
const unsubscribe = store.subscribe(() => {

View File

@@ -31,9 +31,9 @@ export function cleanUpState(payload, keepCurrent = false) {
postsInChannel: {},
postsInThread: {},
reactions: {},
openGraph: payload.entities.posts.openGraph,
selectedPostId: payload.entities.posts.selectedPostId,
currentFocusedPostId: payload.entities.posts.currentFocusedPostId,
openGraph: payload.entities?.posts?.openGraph,
selectedPostId: payload.entities?.posts?.selectedPostId,
currentFocusedPostId: payload.entities?.posts?.currentFocusedPostId,
},
files: {
files: {},
@@ -42,21 +42,20 @@ export function cleanUpState(payload, keepCurrent = false) {
};
let retentionPeriod = 0;
if (payload.entities.general && payload.entities.general.dataRetentionPolicy &&
payload.entities.general.dataRetentionPolicy.message_deletion_enabled) {
if (payload.entities?.general?.dataRetentionPolicy?.message_deletion_enabled) {
retentionPeriod = payload.entities.general.dataRetentionPolicy.message_retention_cutoff;
}
const postIdsToKeep = [];
// Keep the last 60 posts in each recently viewed channel
nextEntities.posts.postsInChannel = cleanUpPostsInChannel(payload.entities.posts.postsInChannel, lastChannelForTeam, keepCurrent ? currentChannelId : '');
nextEntities.posts.postsInChannel = cleanUpPostsInChannel(payload.entities.posts?.postsInChannel, lastChannelForTeam, keepCurrent ? currentChannelId : '');
postIdsToKeep.push(...getAllFromPostsInChannel(nextEntities.posts.postsInChannel));
// Keep any posts that appear in search results
let searchResults = [];
let flaggedPosts = [];
if (payload.entities.search) {
if (payload.entities?.search) {
if (payload.entities.search.results?.length) {
const {results} = payload.entities.search;
searchResults = results;
@@ -71,50 +70,57 @@ export function cleanUpState(payload, keepCurrent = false) {
}
const nextSearch = {
...payload.entities.search,
...(payload.entities.search || {}),
results: searchResults,
flagged: flaggedPosts,
};
postIdsToKeep.forEach((postId) => {
const post = payload.entities.posts.posts[postId];
if (payload.entities.posts?.posts) {
const reactions = payload.entities.posts.reactions || {};
const fileIdsByPostId = payload.entities.files?.fileIdsByPostId || {};
const files = payload.entities.files?.files || {};
const postsInThread = payload.entities.posts.postsInThread || {};
if (post) {
if (retentionPeriod && post.create_at < retentionPeriod) {
// This post has been removed by data retention, so don't keep it
removeFromPostsInChannel(nextEntities.posts.postsInChannel, post.channel_id, postId);
postIdsToKeep.forEach((postId) => {
const post = payload.entities.posts.posts[postId];
return;
if (post) {
if (retentionPeriod && post.create_at < retentionPeriod) {
// This post has been removed by data retention, so don't keep it
removeFromPostsInChannel(nextEntities.posts.postsInChannel, post.channel_id, postId);
return;
}
// Keep the post
nextEntities.posts.posts[postId] = post;
// And its reactions
const reaction = reactions[postId];
if (reaction) {
nextEntities.posts.reactions[postId] = reaction;
}
// And its files
const fileIds = fileIdsByPostId[postId];
if (fileIds) {
nextEntities.files.fileIdsByPostId[postId] = fileIds;
fileIds.forEach((fileId) => {
nextEntities.files.files[fileId] = files[fileId];
});
}
// And its comments
const threadPosts = postsInThread[postId];
if (threadPosts) {
nextEntities.posts.postsInThread[postId] = threadPosts;
}
}
// Keep the post
nextEntities.posts.posts[postId] = post;
// And its reactions
const reaction = payload.entities.posts.reactions[postId];
if (reaction) {
nextEntities.posts.reactions[postId] = reaction;
}
// And its files
const fileIds = payload.entities.files.fileIdsByPostId[postId];
if (fileIds) {
nextEntities.files.fileIdsByPostId[postId] = fileIds;
fileIds.forEach((fileId) => {
nextEntities.files.files[fileId] = payload.entities.files.files[fileId];
});
}
// And its comments
const postsInThread = payload.entities.posts.postsInThread[postId];
if (postsInThread) {
nextEntities.posts.postsInThread[postId] = postsInThread;
}
}
});
});
}
// Remove any pending posts that haven't failed
if (payload.entities.posts && payload.entities.posts.pendingPostIds && payload.entities.posts.pendingPostIds.length) {
if (payload.entities.posts?.pendingPostIds?.length) {
const nextPendingPostIds = [...payload.entities.posts.pendingPostIds];
payload.entities.posts.pendingPostIds.forEach((id) => {
const posts = nextEntities.posts.posts;
@@ -135,9 +141,9 @@ export function cleanUpState(payload, keepCurrent = false) {
}
nextState.views = {
...nextState.views,
...(nextState.views || {}),
root: {
...nextState.views?.root,
...(nextState.views?.root || {}),
// eslint-disable-next-line no-underscore-dangle
hydrationComplete: nextState.views?.root?.hydrationComplete || !nextState._persist,
},
@@ -161,41 +167,43 @@ export function cleanUpState(payload, keepCurrent = false) {
export function cleanUpPostsInChannel(postsInChannel, lastChannelForTeam, currentChannelId, recentPostCount = 60) {
const nextPostsInChannel = {};
for (const channelIds of Object.values(lastChannelForTeam)) {
for (const channelId of channelIds) {
if (nextPostsInChannel[channelId]) {
// This is a DM or GM channel that we've already seen on another team
continue;
}
const postsForChannel = postsInChannel[channelId];
if (!postsForChannel) {
// We don't have anything to keep for this channel
continue;
}
let nextPostsForChannel;
if (channelId === currentChannelId) {
// Keep all of the posts for this channel
nextPostsForChannel = postsForChannel;
} else {
// Only keep the most recent posts for this channel
const recentBlock = postsForChannel.find((block) => block.recent);
if (!recentBlock) {
// We don't have recent posts for this channel
if (postsInChannel && lastChannelForTeam) {
for (const channelIds of Object.values(lastChannelForTeam)) {
for (const channelId of channelIds) {
if (nextPostsInChannel[channelId]) {
// This is a DM or GM channel that we've already seen on another team
continue;
}
nextPostsForChannel = [{
...recentBlock,
order: recentBlock.order.slice(0, recentPostCount),
}];
}
const postsForChannel = postsInChannel[channelId];
nextPostsInChannel[channelId] = nextPostsForChannel;
if (!postsForChannel) {
// We don't have anything to keep for this channel
continue;
}
let nextPostsForChannel;
if (channelId === currentChannelId) {
// Keep all of the posts for this channel
nextPostsForChannel = postsForChannel;
} else {
// Only keep the most recent posts for this channel
const recentBlock = postsForChannel.find((block) => block.recent);
if (!recentBlock) {
// We don't have recent posts for this channel
continue;
}
nextPostsForChannel = [{
...recentBlock,
order: recentBlock.order.slice(0, recentPostCount),
}];
}
nextPostsInChannel[channelId] = nextPostsForChannel;
}
}
}
@@ -206,9 +214,11 @@ export function cleanUpPostsInChannel(postsInChannel, lastChannelForTeam, curren
export function getAllFromPostsInChannel(postsInChannel) {
const postIds = [];
for (const postsForChannel of Object.values(postsInChannel)) {
for (const block of postsForChannel) {
postIds.push(...block.order);
if (postsInChannel) {
for (const postsForChannel of Object.values(postsInChannel)) {
for (const block of postsForChannel) {
postIds.push(...block.order);
}
}
}

View File

@@ -31,7 +31,6 @@ export default async function getStorage(identifier = 'default') {
const MMKV = await new MMKVStorage.Loader().
withInstanceID(identifier).
setProcessingMode(MMKVStorage.MODES.MULTI_PROCESS).
withEncryption().
initialize();
return {

View File

@@ -93,6 +93,12 @@ export function getStateForReset(initialState, currentState) {
},
teams: {
currentTeamId,
teams: {
[currentTeamId]: currentState.entities.teams.teams[currentTeamId],
},
myMembers: {
[currentTeamId]: currentState.entities.teams.myMembers[currentTeamId],
},
},
preferences,
},

View File

@@ -37,6 +37,16 @@ describe('getStateForReset', () => {
},
teams: {
currentTeamId,
teams: {
[currentTeamId]: {
id: 'currentTeamId',
name: 'test',
display_name: 'Test',
},
},
myMembers: {
[currentTeamId]: {},
},
},
preferences: {
myPreferences: {
@@ -74,10 +84,12 @@ describe('getStateForReset', () => {
expect(users.profiles[currentUserId]).toBeDefined();
});
it('should keep the current team ID', () => {
it('should keep the current team', () => {
const resetState = getStateForReset(initialState, currentState);
const {teams} = resetState.entities;
expect(teams.currentTeamId).toEqual(currentTeamId);
expect(teams.teams[currentTeamId]).toEqual(currentState.entities.teams.teams[currentTeamId]);
expect(teams.myMembers[currentTeamId]).toEqual(currentState.entities.teams.myMembers[currentTeamId]);
});
it('should keep theme preferences', () => {

View File

@@ -1,27 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
Alert,
} from 'react-native';
import {
setJSExceptionHandler,
setNativeExceptionHandler,
} from 'react-native-exception-handler';
import {Alert} from 'react-native';
import {setJSExceptionHandler, setNativeExceptionHandler} from 'react-native-exception-handler';
import {dismissAllModals} from '@actions/navigation';
import {purgeOfflineStore} from '@actions/views/root';
import {close as closeWebSocket} from '@actions/websocket';
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
import {Client4} from '@mm-redux/client';
import {logError} from '@mm-redux/actions/errors';
import {close as closeWebSocket} from '@actions/websocket';
import {purgeOfflineStore} from 'app/actions/views/root';
import {DEFAULT_LOCALE, getTranslations} from 'app/i18n';
import {t} from 'app/utils/i18n';
import {t} from '@utils/i18n';
import {
captureException,
captureJSException,
initializeSentry,
LOGGER_NATIVE,
} from 'app/utils/sentry';
} from '@utils/sentry';
class JavascriptAndNativeErrorHandler {
initializeErrorHandling = (store) => {
@@ -60,10 +55,12 @@ class JavascriptAndNativeErrorHandler {
Alert.alert(
translations[t('mobile.error_handler.title')],
translations[t('mobile.error_handler.description')],
translations[t('mobile.error_handler.description')] + `\n\n${e.message}\n\n${e.stack}`,
[{
text: translations[t('mobile.error_handler.button')],
onPress: () => {
onPress: async () => {
await dismissAllModals();
// purge the store
dispatch(purgeOfflineStore());
},

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from 'react-native';
import {Alert, Platform} from 'react-native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import {Posts} from '@mm-redux/constants';
@@ -86,7 +86,11 @@ export function isPendingPost(postId, userId) {
}
export function validatePreviousVersion(previousVersion) {
if (!previousVersion || INVALID_VERSIONS.includes(previousVersion)) {
if (Platform.OS === 'ios') {
INVALID_VERSIONS.push('1.31.0', '1.31.1');
}
if (INVALID_VERSIONS.includes(previousVersion)) {
return false;
}

View File

@@ -2,17 +2,7 @@
// See LICENSE.txt for license information.
import {Client4} from '@mm-redux/client';
import CookieManager from '@react-native-community/cookies';
let mfaPreflightDone = false;
export function setMfaPreflightDone(state) {
mfaPreflightDone = state;
}
export function getMfaPreflightDone() {
return mfaPreflightDone;
}
import CookieManager from 'react-native-cookies';
export function setCSRFFromCookie(url) {
return new Promise((resolve) => {

View File

@@ -2,10 +2,10 @@
// See LICENSE.txt for license information.
// @flow
export function selectFirstAvailableTeam(teams, primaryTeam) {
export function selectFirstAvailableTeam(teams, primaryTeamName) {
let defaultTeam;
if (primaryTeam) {
defaultTeam = teams.find((t) => t.name === primaryTeam.toLowerCase());
if (primaryTeamName) {
defaultTeam = teams.find((t) => t?.name === primaryTeamName.toLowerCase());
}
if (!defaultTeam) {

View File

@@ -9,7 +9,7 @@
"about.teamEditionSt": "Ihre gesamte Team-Kommunikation an einem Ort, sofort durchsuchbar und überall verfügbar.",
"about.teamEditiont0": "Team Edition",
"about.teamEditiont1": "Enterprise Edition",
"about.title": "Über Mattermost",
"about.title": "Über {appTitle}",
"announcment_banner.dont_show_again": "Nicht erneut anzeigen",
"api.channel.add_member.added": "{addedUsername} durch {username} zum Kanal hinzugefügt.",
"archivedChannelMessage": "Sie sehen einen **archivierten Kanal**. Neue Nachrichten können nicht geschickt werden.",
@@ -27,7 +27,7 @@
"channel_modal.descriptionHelp": "Beschreiben Sie,wie dieser Kanal genutzt werden soll.",
"channel_modal.header": "Überschrift",
"channel_modal.headerEx": "Z.B.: \"[Link Titel](http://beispiel.de)\"",
"channel_modal.headerHelp": "Der Text der in der Kopfzeile des Kanals neben dem Namen steht. Zum Beispiel könnten Sie häufig genutzte Links durch Hinzufügen von [Link Titel](http://example.de) anzeigen lassen.",
"channel_modal.headerHelp": "Der Text, der in der Kopfzeile des Kanals neben dem Namen steht. Zum Beispiel könnten Sie häufig genutzte Links durch Hinzufügen von [Link Titel](http://example.de) anzeigen lassen.",
"channel_modal.name": "Name",
"channel_modal.nameEx": "Z.B.: \"Bugs\", \"Marketing\", \"客户支持\"",
"channel_modal.optional": "(optional)",
@@ -35,8 +35,11 @@
"channel_modal.purposeEx": "Z.B.: \"Ein Kanal um Fehler und Verbesserungsvorschläge abzulegen\"",
"channel_notifications.ignoreChannelMentions.settings": "@channel, @here, @all ignorieren",
"channel_notifications.muteChannel.settings": "Kanal stummschalten",
"channel.channelHasGuests": "Dieser Kanal hat Gäste",
"channel.hasGuests": "Diese Gruppennachricht hat Gäste",
"channel.isGuest": "Diese Person ist ein Gast.",
"combined_system_message.added_to_channel.many_expanded": "{users} und {lastUser} wurden durch {actor} **zum Kanal hinzugefügt**.",
"combined_system_message.added_to_channel.one": "{firstUser} von {actor} **zum Kanal hinzugefügt**.",
"combined_system_message.added_to_channel.one": "{firstUser} durch {actor} **zum Kanal hinzugefügt**.",
"combined_system_message.added_to_channel.one_you": "Sie wurden durch {actor} **zum Kanal hinzugefügt**.",
"combined_system_message.added_to_channel.two": "{firstUser} und {secondUser} wurden durch {actor} **zum Kanal hinzugefügt**.",
"combined_system_message.added_to_team.many_expanded": "{users} und {lastUser} wurden durch {actor} **zum Team hinzugefügt**.",
@@ -45,19 +48,19 @@
"combined_system_message.added_to_team.two": "{firstUser} und {secondUser} wurden durch {actor} **zum Team hinzugefügt**.",
"combined_system_message.joined_channel.many_expanded": "{users} und {lastUser} **sind dem Kanal beigetreten**.",
"combined_system_message.joined_channel.one": "{firstUser} **ist dem Kanal beigetreten**.",
"combined_system_message.joined_channel.one_you": "**sind dem Kanal beigetreten**.",
"combined_system_message.joined_channel.one_you": "Sie **sind dem Kanal beigetreten**.",
"combined_system_message.joined_channel.two": "{firstUser} und {secondUser} **sind dem Kanal beigetreten**.",
"combined_system_message.joined_team.many_expanded": "{users} und {lastUser} **sind dem Team beigetreten**.",
"combined_system_message.joined_team.one": "{firstUser} **ist dem Team beigetreten**.",
"combined_system_message.joined_team.one_you": "**ist dem Team beigetreten**.",
"combined_system_message.joined_team.one_you": "Sie **sind dem Team beigetreten**.",
"combined_system_message.joined_team.two": "{firstUser} und {secondUser} **sind dem Team beigetreten**.",
"combined_system_message.left_channel.many_expanded": "{users} und {lastUser} **haben den Kanal verlassen**.",
"combined_system_message.left_channel.one": "{firstUser} **hat den Kanal verlassen**.",
"combined_system_message.left_channel.one_you": "**hat den Kanal verlassen**.",
"combined_system_message.left_channel.one_you": "Sie **haben den Kanal verlassen**.",
"combined_system_message.left_channel.two": "{firstUser} und {secondUser} **haben den Kanal verlassen**.",
"combined_system_message.left_team.many_expanded": "{users} und {lastUser} **haben das Team verlassen**.",
"combined_system_message.left_team.one": "{firstUser} **hat das Team verlassen**.",
"combined_system_message.left_team.one_you": "**hat das Team verlassen**.",
"combined_system_message.left_team.one_you": "Sie **haben das Team verlassen**.",
"combined_system_message.left_team.two": "{firstUser} und {secondUser} **haben das Team verlassen**.",
"combined_system_message.removed_from_channel.many_expanded": "{users} und {lastUser} wurden **aus dem Kanal entfernt**.",
"combined_system_message.removed_from_channel.one": "{firstUser} wurde **aus dem Kanal entfernt**.",
@@ -70,21 +73,21 @@
"combined_system_message.you": "Sie",
"create_comment.addComment": "Kommentar hinzufügen...",
"create_post.deactivated": "Sie betrachten einen archivierten Kanal mit einem deaktivierten Benutzer.",
"create_post.write": "Write to {channelDisplayName}",
"create_post.write": "In {channelDisplayName} schreiben",
"date_separator.today": "Heute",
"date_separator.yesterday": "Gestern",
"edit_post.editPost": "Nachricht bearbeiten...",
"edit_post.save": "Speichern",
"error.team_not_found.title": "Team nicht gefunden",
"file_attachment.download": "Download",
"file_upload.fileAbove": "Datei über {max}MB kann nicht hochgeladen werden: {filename}",
"get_post_link_modal.title": "Kopiere Permalink",
"get_post_link_modal.title": "Link kopieren",
"integrations.add": "Hinzufügen",
"intro_messages.anyMember": " Jedes Mitglied kann diesem Kanal beitreten und folgen.",
"intro_messages.beginning": "Start von {name}",
"intro_messages.channel": "Kanal",
"intro_messages.creator": "Dies ist der Start von {type} {name}, erstellt durch {creator} am {date}.",
"intro_messages.group": "Privater Kanal",
"intro_messages.group_message": "Dies ist der Start ihres Gruppennachrichten-Verlaufs mit diesen Teammitgliedern. Nachrichten und hier geteilte Dateien sind für Personen außerhalb dieses Bereichs nicht sichtbar.",
"intro_messages.noCreator": "Dies ist der Start von {type} {name}, erstellt am {date}.",
"intro_messages.creator": "Dies ist der Start von {name}, erstellt durch {creator} am {date}.",
"intro_messages.creatorPrivate": "Dies ist der Start von {name}, erstellt durch {creator} am {date}.",
"intro_messages.group_message": "Dies ist der Start ihres Gruppennachrichtenverlaufs mit diesen Teammitgliedern. Nachrichten und hier geteilte Dateien sind für Personen außerhalb dieses Bereichs nicht sichtbar.",
"intro_messages.noCreator": "Dies ist der Start von {name}, erstellt am {date}.",
"intro_messages.onlyInvited": " Nur eingeladene Mitglieder können diesen privaten Kanal sehen.",
"last_users_message.added_to_channel.type": "wurden durch {actor} **dem Kanal hinzugefügt**.",
"last_users_message.added_to_team.type": "wurden durch {actor} **dem Team hinzugefügt**.",
@@ -98,9 +101,9 @@
"last_users_message.removed_from_team.type": "wurden **aus dem Team entfernt**.",
"login_mfa.enterToken": "Um ihre Anmeldung zu vervollständigen, geben Sie bitte den Token des Authenticators ein",
"login_mfa.token": "MFA Token",
"login_mfa.tokenReq": "Bitte geben Sie den MFA Token ein",
"login_mfa.tokenReq": "Bitte geben Sie den MFA-Token ein",
"login.email": "E-Mail-Adresse",
"login.forgot": "Ich habe mein Passwort vergessen",
"login.forgot": "Ich habe mein Passwort vergessen.",
"login.invalidPassword": "Ihr Passwort ist falsch.",
"login.ldapUsername": "AD/LDAP-Benutzername",
"login.ldapUsernameLower": "AD/LDAP-Benutzername",
@@ -128,7 +131,6 @@
"mobile.account_notifications.threads_mentions": "Erwähnungen in Antworten",
"mobile.account_notifications.threads_start": "Diskussionen die ich starte",
"mobile.account_notifications.threads_start_participate": "Diskussionen die ich starte oder an denen ich teilnehme",
"mobile.account.settings.cancel": "Abbrechen",
"mobile.account.settings.save": "Speichern",
"mobile.action_menu.select": "Wählen Sie eine Option",
"mobile.advanced_settings.clockDisplay": "Uhrzeit-Format",
@@ -137,36 +139,44 @@
"mobile.advanced_settings.delete_title": "Dokumente & Daten löschen",
"mobile.advanced_settings.timezone": "Zeitzone",
"mobile.advanced_settings.title": "Erweiterte Einstellungen",
"mobile.android.camera_permission_denied_description": "Um Fotos und Videos mit ihrer Kamera aufzunehmen, ändern Sie bitte ihre Berechtigungseinstellungen.",
"mobile.android.camera_permission_denied_title": "Kamerazugrif wird benötigt",
"mobile.android.permission_denied_dismiss": "Verwerfen",
"mobile.android.permission_denied_retry": "Berechtigung einstellen",
"mobile.android.photos_permission_denied_description": "Um Bilder aus ihrer Bibliothek hochzuladen, ändern Sie bitte ihre Berechtigungseinstellungen.",
"mobile.android.photos_permission_denied_title": "Zugriff auf Fotobibliothek wird benötigt",
"mobile.android.storage_permission_denied_description": "Um Bilder von ihrem Android-Gerät hochzuladen, ändern Sie bitte ihre Berechtigungseinstellungen.",
"mobile.android.storage_permission_denied_title": "Zugriff auf Dateisystem wird benötigt",
"mobile.android.videos_permission_denied_description": "Um Videos aus ihrer Bibliothek hochzuladen, ändern Sie bitte ihre Berechtigungseinstellungen.",
"mobile.android.videos_permission_denied_title": "Zugriff auf Videobibliothek wird benötigt",
"mobile.alert_dialog.alertCancel": "Abbrechen",
"mobile.android.photos_permission_denied_description": "Laden Sie Fotos auf ihre Mattermost-Instanz hoch oder speichern Sie sie auf Ihrem Gerät. Öffnen Sie die Einstellungen, um Mattermost Lese- und Schreibzugriff auf ihre Fotobibliothek zu gewähren.",
"mobile.android.photos_permission_denied_title": "{applicationName} möchte auf Ihre Fotos zugreifen",
"mobile.android.videos_permission_denied_description": "Laden Sie Videos auf ihre Mattermost-Instanz hoch oder speichern Sie sie auf Ihrem Gerät. Öffnen Sie die Einstellungen, um Mattermost Lese- und Schreibzugriff auf ihre Videobibliothek zu gewähren.",
"mobile.android.videos_permission_denied_title": "{applicationName} möchte auf Ihre Videos zugreifen",
"mobile.announcement_banner.title": "Ankündigung",
"mobile.authentication_error.message": "Mattermost hat einen Fehler festgestellt. Bitte authentifizieren Sie sich erneut, um eine neue Sitzung zu beginnen.",
"mobile.authentication_error.title": "Authentifizierungsfehler",
"mobile.calendar.dayNames": "Montag,Dienstag,Mittwoch,Donnerstag,Freitag, Samstag,Sonntag",
"mobile.calendar.dayNamesShort": "Mo,Di,Mi,Do,Fr,Sa,So",
"mobile.calendar.monthNames": "Januar,Februar,März,April,Mai,Juni,July,August,September,Oktober,November,Dezember",
"mobile.calendar.monthNamesShort": "Jan,Feb,Mär,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez",
"mobile.camera_photo_permission_denied_description": "Nehmen Sie Fotos auf und laden sie auf ihre Mattermost-Instanz hoch oder speichern Sie sie auf Ihrem Gerät. Öffnen Sie die Einstellungen, um Mattermost Lese- und Schreibzugriff auf ihre Kamera zu gewähren.",
"mobile.camera_photo_permission_denied_title": "{applicationName} möchte auf Ihre Kamera zugreifen",
"mobile.camera_video_permission_denied_description": "Nehmen Sie Videos auf und laden sie auf ihre Mattermost-Instanz hoch oder speichern Sie sie auf Ihrem Gerät. Öffnen Sie die Einstellungen, um Mattermost Lese- und Schreibzugriff auf ihre Kamera zu gewähren.",
"mobile.camera_video_permission_denied_title": "{applicationName} möchte auf Ihre Kamera zugreifen",
"mobile.channel_drawer.search": "Springe zu...",
"mobile.channel_info.alertMessageConvertChannel": "Wenn Sie **{displayName}**** in einen privaten Kanal umwandeln, bleiben Historie und Mitgliedschaft erhalten. Öffentlich freigegebene Dateien bleiben für jeden mit dem Link zugänglich. Die Mitgliedschaft in einem privaten Kanal ist nur auf Einladung möglich. \n\nDie Änderung ist dauerhaft und kann nicht rückgängig gemacht werden.\n\nSind Sie sicher, dass Sie **{displayName}**** in einen privaten Kanal umwandeln möchten?",
"mobile.channel_info.alertMessageDeleteChannel": "Sind Sie sicher, dass Sie den {term} {name} archivieren möchten?",
"mobile.channel_info.alertMessageLeaveChannel": "Sind Sie sicher, dass Sie den {term} {name} verlassen möchten?",
"mobile.channel_info.alertMessageUnarchiveChannel": "Sind Sie sicher, dass Sie den {term} {name} wiederherstellen möchten?",
"mobile.channel_info.alertNo": "Nein",
"mobile.channel_info.alertTitleConvertChannel": "{displayName} in privaten Kanal umwandeln?",
"mobile.channel_info.alertTitleDeleteChannel": "{term} archivieren",
"mobile.channel_info.alertTitleLeaveChannel": "{term} verlassen",
"mobile.channel_info.alertTitleUnarchiveChannel": "{term} wiederherstellen",
"mobile.channel_info.alertYes": "Ja",
"mobile.channel_info.convert": "In privaten Kanal umwandeln",
"mobile.channel_info.convert_failed": "Wir konnten {displayName} nicht in einen privaten Kanal umwandeln.",
"mobile.channel_info.convert_success": "{displayName} ist nun ein privater Kanal.",
"mobile.channel_info.copy_header": "Header kopieren",
"mobile.channel_info.copy_purpose": "Zweck kopieren",
"mobile.channel_info.delete_failed": "Der Kanal {displayName} konnte nicht archiviert werden. Bitte überprüfen Sie ihre Verbindung und versuchen es erneut.",
"mobile.channel_info.edit": "Kanal bearbeiten",
"mobile.channel_info.privateChannel": "Privater Kanal",
"mobile.channel_info.publicChannel": "Öffentlicher Kanal",
"mobile.channel_info.unarchive_failed": "Der Kanal {displayName} konnte nicht wiederhergestellt werden. Bitte überprüfen Sie ihre Verbindung und versuchen es erneut.",
"mobile.channel_list.alertNo": "Nein",
"mobile.channel_list.alertYes": "Ja",
"mobile.channel_list.archived": "ARCHIVIERT",
"mobile.channel_list.channels": "KANÄLE",
"mobile.channel_list.closeDM": "Direktnachricht schließen",
"mobile.channel_list.closeGM": "Gruppennachricht schließen",
@@ -174,14 +184,13 @@
"mobile.channel_list.not_member": "KEIN MITGLIED",
"mobile.channel_list.unreads": "UNGELESENE",
"mobile.channel_members.add_members_alert": "Sie müssen mindestens ein Mitglied auswählen, um es dem Kanal hinzuzufügen.",
"mobile.channel.markAsRead": "Als gelesen markieren",
"mobile.client_upgrade": "App aktualisieren",
"mobile.client_upgrade.can_upgrade_subtitle": "Es steht eine neue Version zum Herunterladen bereit.",
"mobile.client_upgrade.can_upgrade_title": "Aktualisierung verfügbar",
"mobile.client_upgrade.close": "Später Aktualisieren",
"mobile.client_upgrade.current_version": "Neueste Version: {version}",
"mobile.client_upgrade.download_error.message": "Es ist ein Fehler beim Herunterladen der neuen Version aufgetreten.",
"mobile.client_upgrade.download_error.title": "Aktualisierung konnte nicht installiert werden",
"mobile.client_upgrade.download_error.title": "Konnte Aktualisierung nicht installieren.",
"mobile.client_upgrade.latest_version": "Ihre Version: {version}",
"mobile.client_upgrade.listener.dismiss_button": "Verwerfen",
"mobile.client_upgrade.listener.learn_more_button": "Mehr erfahren",
@@ -204,25 +213,26 @@
"mobile.create_channel.public": "Neuer Öffentlicher Kanal",
"mobile.create_post.read_only": "Dieser Kanal ist schreibgeschützt",
"mobile.custom_list.no_results": "Keine Ergebnisse",
"mobile.display_settings.sidebar": "Seitenleiste",
"mobile.display_settings.theme": "Motiv",
"mobile.document_preview.failed_description": "Es trat ein Fehler beim Öffnen des Dokuments auf. Bitte stellen Sie sicher Sie haben einen Betrachter für {fileType} installiert und versuchen es erneut.\n",
"mobile.document_preview.failed_description": "Es ist ein Fehler beim Öffnen des Dokuments aufgetreten. Bitte stellen Sie sicher, dass Sie einen Betrachter für {fileType} installiert haben und versuchen es erneut.\n",
"mobile.document_preview.failed_title": "Dokument öffnen fehlgeschlagen",
"mobile.downloader.android_complete": "Herunterladen abgeschlossen",
"mobile.downloader.android_failed": "Herunterladen gescheitert",
"mobile.downloader.android_permission": "Wir benötigen Zugriff auf den Donwload-Ordner, um Dateien speichern zu können.",
"mobile.downloader.android_started": "Herunterladen gestartet",
"mobile.downloader.android_success": "Herunterladen erfolgreich",
"mobile.downloader.complete": "Herunterladen abgeschlossen",
"mobile.downloader.disabled_description": "Das Herunterladen von Dateien ist auf diesem Server deaktiviert. Bitte kontaktieren Sie ihren Systemadministrator.\n",
"mobile.downloader.disabled_title": "Herunterladen deaktiviert",
"mobile.downloader.downloading": "Wird heruntergeladen...",
"mobile.downloader.failed_description": "Beim Herunterladen der Datei ist ein Fehler aufgetreten. Überprüfen Sie ihre Internetverbindung und versuchen Sie es erneut.\n",
"mobile.downloader.failed_description": "Es ist ein Fehler beim Herunterladen der Datei aufgetreten. Überprüfen Sie ihre Internetverbindung und versuchen Sie es erneut.\n",
"mobile.downloader.failed_title": "Herunterladen gescheitert",
"mobile.downloader.image_saved": "Bild gespeichert",
"mobile.downloader.video_saved": "Video gespeichert",
"mobile.drawer.teamsTitle": "Teams",
"mobile.edit_channel": "Speichern",
"mobile.edit_post.title": "Nachricht bearbeiten",
"mobile.edit_profile.remove_profile_photo": "Foto entfernen",
"mobile.emoji_picker.activity": "AKTIVITÄTEN",
"mobile.emoji_picker.custom": "BENUTZERDEFINIERT",
"mobile.emoji_picker.flags": "FLAGGEN",
@@ -234,24 +244,31 @@
"mobile.emoji_picker.recent": "ZULETZT VERWENDET",
"mobile.emoji_picker.symbols": "SYMBOLE",
"mobile.error_handler.button": "Neustarten",
"mobile.error_handler.description": "\nKlicken Sie auf Neustarten um die App neu zu öffnen. Nach dem Neustart können Sie das Problem über das Einstellungsmenü melden.\n",
"mobile.error_handler.description": "\nTippen Sie auf Neustarten um die App neu zu öffnen. Nach dem Neustart können Sie das Problem über das Einstellungsmenü melden.\n",
"mobile.error_handler.title": "Ein unerwarteter Fehler ist aufgetreten",
"mobile.extension.authentication_required": "Authentifizierung erforderlich: Bitte melden Sie sich zuerst in der App an.",
"mobile.extension.file_error": "Es gab einen Fehler beim Lesen der zu teilenden Datei.\nBitte erneut versuchen.",
"mobile.extension.file_limit": "Dateiaustausch ist auf ein Maximum von 5 Dateien begrenzt.",
"mobile.extension.max_file_size": "Dateianhänge, die in Mattermost geteilt werden, müssen kleiner als {size} sein.",
"mobile.extension.permission": "Mattermost benötigt Zugriff auf den Gerätespeicher, um Dateien teilen zu können.",
"mobile.extension.team_required": "Sie müssen zu einem Team gehören, bevor Sie Dateien austauschen können.",
"mobile.extension.title": "In Mattermost teilen",
"mobile.failed_network_action.description": "Es scheint ein Problem mit ihrer Internetverbindung zu geben. Stellen Sie sicher, dass Sie über eine aktive Verbindung verfügen und versuchen Sie es erneut.",
"mobile.failed_network_action.retry": "Erneut versuchen",
"mobile.failed_network_action.shortDescription": "Stellen Sie sicher, dass Sie eine aktive Verbindung haben und versuchen es erneut.",
"mobile.failed_network_action.shortDescription": "Messages will load when you have an internet connection.",
"mobile.failed_network_action.teams_channel_description": "Channels could not be loaded for {teamName}.",
"mobile.failed_network_action.teams_description": "Teams could not be loaded.",
"mobile.failed_network_action.teams_title": "Something went wrong",
"mobile.failed_network_action.title": "Keine Internetverbindung",
"mobile.file_upload.browse": "Dateien durchsuchen",
"mobile.file_upload.camera_photo": "Foto aufnehmen",
"mobile.file_upload.camera_video": "Video aufnehmen",
"mobile.file_upload.library": "Foto-Bibliothek",
"mobile.file_upload.max_warning": "Uploads sind auf maximal fünf Dateien beschränkt.",
"mobile.file_upload.unsupportedMimeType": "Nur BMP-, JPG- oder PNG-Bilder sind als Profilbilder zugelassen.",
"mobile.file_upload.video": "Videobibliothek",
"mobile.files_paste.error_description": "Fehler beim Einfügen der Datei(en). Bitte erneut versuchen.",
"mobile.files_paste.error_dismiss": "Verwerfen",
"mobile.files_paste.error_title": "Einfügen fehlgeschlagen",
"mobile.flagged_posts.empty_description": "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.",
"mobile.flagged_posts.empty_title": "Keine markierte Nachrichten",
"mobile.help.title": "Hilfe",
@@ -259,8 +276,9 @@
"mobile.image_preview.save_video": "Video speichern",
"mobile.intro_messages.default_message": "Dies ist der Kanal, den Teammitglieder sehen, wenn sie sich anmelden - benutzen Sie ihn zum Veröffentlichen von Aktualisierungen, die jeder kennen muss.",
"mobile.intro_messages.default_welcome": "Willkommen bei {name}!",
"mobile.intro_messages.DM": "Dies ist der Start der Privatnachrichten mit {teammate}. Privatnachrichten und hier geteilte Dateien sind für Personen außerhalb dieses Bereichs nicht sichtbar.",
"mobile.ios.photos_permission_denied_description": "Um Fotos und Videos in ihrer Bibliothek speichern zu können, ändern Sie bitte ihre Berechtigungseinstellungen.",
"mobile.intro_messages.DM": "Dies ist der Start der Privatnachrichtenverlaufs mit {teammate}. Privatnachrichten und hier geteilte Dateien sind für Personen außerhalb dieses Bereichs nicht sichtbar.",
"mobile.ios.photos_permission_denied_description": "Laden Sie Fotos und Videos auf ihre Mattermost-Instanz hoch oder speichern Sie sie auf Ihrem Gerät. Öffnen Sie die Einstellungen, um Mattermost Lese- und Schreibzugriff auf ihre Foto- und Videobibliothek zu gewähren.",
"mobile.ios.photos_permission_denied_title": "{applicationName} möchte auf Ihre Fotos zugreifen",
"mobile.join_channel.error": "Dem Kanal {displayName} konnte nicht beigetreten werden. Bitte überprüfen Sie ihre Verbindung und versuchen es erneut.",
"mobile.loading_channels": "Lade Kanäle...",
"mobile.loading_members": "Lade Mitglieder...",
@@ -271,13 +289,17 @@
"mobile.managed.blocked_by": "Blockiert durch {vendor}",
"mobile.managed.exit": "Beenden",
"mobile.managed.jailbreak": "Geräten mit Jailbreak wird von {vendor} nicht vertraut, bitte beenden Sie die App.",
"mobile.managed.not_secured.android": "Dieses Gerät muss mit einer Bildschirmsperre gesichert werden, um Mattermost verwenden zu können.",
"mobile.managed.not_secured.ios": "Dieses Gerät muss mit einem Passcode gesichert werden, um Mattermost verwenden zu können.\n \nGehen Sie zu Einstellungen > Face ID & Passwort.",
"mobile.managed.not_secured.ios.touchId": "Dieses Gerät muss mit einem Passcode gesichert werden, um Mattermost zu verwenden.\n \nGehen Sie zu Einstellungen > Touch ID & Passwort.",
"mobile.managed.secured_by": "Gesichert durch {vendor}",
"mobile.managed.settings": "Zu Einstellungen gehen",
"mobile.markdown.code.copy_code": "Code kopieren",
"mobile.markdown.code.plusMoreLines": "+{count, number} weitere {count, plural, one {Zeile} other {Zeilen}}",
"mobile.markdown.image.too_large": "Bild überschreitet die maximale Auflösung von {maxWidth} x {maxHeight}:",
"mobile.markdown.link.copy_url": "Adresse (URL) kopieren",
"mobile.mention.copy_mention": "Erwähnung kopieren",
"mobile.message_length.message": "Die Nachricht ist zu lang. Anzahl der Zeichen: {max}/{count}",
"mobile.message_length.message": "Die Nachricht ist zu lang. Anzahl der Zeichen: {count}/{max}",
"mobile.message_length.title": "Länge der Nachricht",
"mobile.more_dms.add_more": "Sie können {remaining, number} weitere Benutzer hinzufügen",
"mobile.more_dms.cannot_add_more": "Sie können keine weiteren Benutzer hinzufügen.",
@@ -302,7 +324,7 @@
"mobile.notification_settings_mobile.sound": "Ton",
"mobile.notification_settings_mobile.sounds_title": "Benachrichtigungston",
"mobile.notification_settings_mobile.test": "Schicke mir eine Testbenachrichtigung",
"mobile.notification_settings_mobile.test_push": "Dies ist eine Test-Push-Benachrichtigung",
"mobile.notification_settings_mobile.test_push": "Dies ist eine Test-Push-Benachrichtigung.",
"mobile.notification_settings_mobile.vibrate": "Vibrieren",
"mobile.notification_settings.auto_responder_short": "Automatische Antworten",
"mobile.notification_settings.auto_responder.default_message": "Hallo, ich bin derzeit nicht im Büro und kann nicht auf Nachrichten antworten.",
@@ -332,20 +354,28 @@
"mobile.open_dm.error": "Der Direktnachrichtenkanal mit {displayName} konnte nicht geöffnet werden. Bitte überprüfen Sie ihre Verbindung und versuchen es erneut.",
"mobile.open_gm.error": "Der Gruppennachrichtenkanal mit diesen Benutzern konnte nicht geöffnet werden. Bitte überprüfen Sie ihre Verbindung und versuchen es erneut.",
"mobile.open_unknown_channel.error": "Konnte Kanal nicht beitreten. Bitte setzen Sie den Cache zurück und versuchen es erneut.",
"mobile.permission_denied_dismiss": "Nicht erlauben",
"mobile.permission_denied_retry": "Einstellungen",
"mobile.photo_library_permission_denied_description": "Um Fotos und Videos in ihrer Bibliothek speichern zu können, ändern Sie bitte ihre Berechtigungseinstellungen.",
"mobile.photo_library_permission_denied_title": "{applicationName} möchte auf Ihre Fotobibliothek zugreifen",
"mobile.pinned_posts.empty_description": "Wichtige Elemente anheften durch gedrückt halten einer Nachricht und wählen von \"An Kanal anheften\".",
"mobile.pinned_posts.empty_title": "Keine angehefteten Nachrichten",
"mobile.post_info.add_reaction": "Reaktion hinzufügen",
"mobile.post_info.copy_text": "Text kopieren",
"mobile.post_info.flag": "Markieren",
"mobile.post_info.mark_unread": "Als ungelesen markieren",
"mobile.post_info.pin": "An Kanal anheften",
"mobile.post_info.reply": "Antworten",
"mobile.post_info.unflag": "Markierung entfernen",
"mobile.post_info.unpin": "Vom Kanal abheften",
"mobile.post_pre_header.flagged": "Markiert",
"mobile.post_pre_header.pinned": "Angeheftet",
"mobile.post_pre_header.pinned_flagged": "Angeheftet und markiert",
"mobile.post_textbox.empty.message": "Sie versuchen, eine leere Nachricht zu senden.\nBitte stellen Sie sicher, dass die Nachricht Text oder eine angehängte Datei enthält.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Leere Nachricht",
"mobile.post_textbox.entire_channel.cancel": "Abbrechen",
"mobile.post_textbox.entire_channel.confirm": "Bestätigen",
"mobile.post_textbox.entire_channel.message": "Durch die Verwendung von @all oder @channel benachrichtigen Sie {totalMembers, number} {totalMembers, plural, one {Person} other {Personen}}. Sind Sie sicher, dass sie dies tun möchten?",
"mobile.post_textbox.entire_channel.message.with_timezones": "Durch die Verwendung von @all oder @channel benachrichtigen Sie {totalMembers, number} {totalMembers, plural, one {Person} other {Personen}} in {timezones, number} {timezones, plural, one {Zeitzone} other {Zeitzonen}}. Sind Sie sicher, dass sie dies tun möchten?",
"mobile.post_textbox.entire_channel.title": "Bestätigen Sie das Senden von Benachrichtigungen an den gesamten Kanal",
"mobile.post_textbox.uploadFailedDesc": "Einige Anhänge konnten nicht auf den Server hochgeladen werden. Sind Sie sicher, dass sie die Nachricht abschicken wollen?",
"mobile.post_textbox.uploadFailedTitle": "Anhang Fehler",
"mobile.post.cancel": "Abbrechen",
@@ -353,9 +383,14 @@
"mobile.post.delete_title": "Nachricht löschen",
"mobile.post.failed_delete": "Nachricht löschen",
"mobile.post.failed_retry": "Erneut versuchen",
"mobile.post.failed_title": "Ihre Nachricht konnte nicht gesendet werden",
"mobile.post.failed_title": "Konnte ihre Nachricht nicht senden.",
"mobile.post.retry": "Aktualisieren",
"mobile.posts_view.moreMsg": "Weitere neue Nachrichten oberhalb",
"mobile.privacy_link": "Datenschutzbedingungen",
"mobile.push_notification_reply.button": "Senden",
"mobile.push_notification_reply.placeholder": "Eine Antwort schreiben...",
"mobile.push_notification_reply.title": "Antworten",
"mobile.reaction_header.all_emojis": "Alle",
"mobile.recent_mentions.empty_description": "Hier werden Nachrichten auftauchen, die ihren Benutzernamen oder andere Wörter enthalten, die Erwähnungen auslösen.",
"mobile.recent_mentions.empty_title": "Keine letzten Erwähnungen",
"mobile.rename_channel.display_name_maxLength": "Kanalname muss kürzer als {maxLength, number} Zeichen sein",
@@ -378,6 +413,8 @@
"mobile.routes.channelInfo.createdBy": "Erstellt durch {creator} am ",
"mobile.routes.channelInfo.delete_channel": "Kanal archivieren",
"mobile.routes.channelInfo.favorite": "Favoriten",
"mobile.routes.channelInfo.groupManaged": "Mitglieder werden von verknüpften Gruppen verwaltet.",
"mobile.routes.channelInfo.unarchive_channel": "Kanal wiederherstellen",
"mobile.routes.code": "{language}-Code",
"mobile.routes.code.noLanguage": "Code",
"mobile.routes.edit_profile": "Profil bearbeiten",
@@ -393,6 +430,7 @@
"mobile.routes.thread": "{channelName} Diskussion",
"mobile.routes.thread_dm": "Direktnachrichten-Diskussion",
"mobile.routes.user_profile": "Profil",
"mobile.routes.user_profile.edit": "Bearbeiten",
"mobile.routes.user_profile.local_time": "LOKALE ZEIT",
"mobile.routes.user_profile.send_message": "Nachricht senden",
"mobile.search.after_modifier_description": "um Nachrichten nach einem spezifischen Datum zu finden",
@@ -405,13 +443,20 @@
"mobile.search.no_results": "Keine Ergebnisse gefunden",
"mobile.search.on_modifier_description": "um Nachrichten an einem spezifischen Datum zu finden",
"mobile.search.recent_title": "Letzte Suchen",
"mobile.select_team.guest_cant_join_team": "Ihr Gastkonto ist keinem Team oder Kanal zugeordnet. Bitte kontaktieren Sie den Administrator.",
"mobile.select_team.join_open": "Offene Teams, denen Sie beitreten können",
"mobile.select_team.no_teams": "Es sind keine Teams zum Betreten für Sie verfügbar.",
"mobile.server_link.error.text": "Der Link konnte auf diesem Server nicht gefunden werden.",
"mobile.server_link.error.title": "Link-Fehler",
"mobile.server_link.unreachable_channel.error": "Der Link gehört zu einem gelöschten Kanal oder einem Kanal auf den Sie keinen Zugriff haben.",
"mobile.server_link.unreachable_team.error": "Der Link verweist auf ein gelöschtes Team oder ein Team auf das Sie keinen Zugriff haben.",
"mobile.server_ssl.error.text": "The certificate from {host} is not trusted.\n\nPlease contact your System Administrator to resolve the certificate issues and allow connections to this server.",
"mobile.server_ssl.error.title": "Untrusted Certificate",
"mobile.server_upgrade.button": "OK",
"mobile.server_upgrade.description": "\nEine Serveraktualisierung ist erforderlich, um die Mattermost-App zu verwenden. Bitte Fragen Sie ihren Systemadministrator für Details.\n",
"mobile.server_upgrade.title": "Serveraktualisierung erforderlich",
"mobile.server_url.invalid_format": "URL muss mit http:// oder https:// beginnen",
"mobile.session_expired": "Sitzung abgelaufen: Bitte anmelden um weiterhin Benachrichtigungen zu erhalten.",
"mobile.session_expired": "Die Sitzung ist abgelaufen: Bitte melden Sie sich an, um weiterhin Benachrichtigungen zu erhalten. Sitzungen für {siteName} sind so konfiguriert, dass sie nach {daysCount, number} {daysCount, plural, one {Tag} other {Tagen}} ablaufen.",
"mobile.set_status.away": "Abwesend",
"mobile.set_status.dnd": "Nicht stören",
"mobile.set_status.offline": "Offline",
@@ -422,19 +467,33 @@
"mobile.share_extension.error_message": "Es ist ein Fehler bei der Verwendung der Teilen-Erweiterung aufgetreten.",
"mobile.share_extension.error_title": "Erweiterungs-Fehler",
"mobile.share_extension.team": "Team",
"mobile.share_extension.too_long_message": "Zeichenanzahl: {count}/{max}",
"mobile.share_extension.too_long_title": "Nachricht ist zu lang",
"mobile.sidebar_settings.permanent": "Permanente Seitenleiste",
"mobile.sidebar_settings.permanent_description": "Seitenleiste permanent geöffnet lassen",
"mobile.storage_permission_denied_description": "Laden Sie Dateien auf ihre Mattermost-Instanz hoch. Öffnen Sie die Einstellungen, um Mattermost Lese- und Schreibzugriff auf Dateien auf diesem Gerät zu gewähren.",
"mobile.storage_permission_denied_title": "{applicationName} möchte auf Ihre Dateien zugreifen.",
"mobile.suggestion.members": "Mitglieder",
"mobile.system_message.channel_archived_message": "{username} hat den Kanal archiviert.",
"mobile.system_message.channel_unarchived_message": "{username} hat den Kanal wiederhergestellt",
"mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} hat den Kanal-Anzeigenamen geändert von: {oldDisplayName} auf: {newDisplayName}",
"mobile.system_message.update_channel_header_message_and_forget.removed": "{username} hat die Kanalüberschrift entfernt (war: {oldHeader})",
"mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} hat die Kanalüberschrift aktualisiert von: {oldHeader} auf: {newHeader}",
"mobile.system_message.update_channel_header_message_and_forget.updated_to": "{username} hat die Kanalüberschrift geändert zu: {newHeader}",
"mobile.system_message.update_channel_purpose_message.removed": "{username} hat den Kanalzweck entfernt (war: {oldPurpose})",
"mobile.system_message.update_channel_purpose_message.updated_from": "{username} hat den Kanalzweck aktualisiert von: {oldPurpose} auf: {newPurpose}",
"mobile.system_message.update_channel_purpose_message.updated_to": "{username} hat den Kanalzweck geändert zu: {newPurpose}",
"mobile.terms_of_service.alert_cancel": "Abbrechen",
"mobile.terms_of_service.alert_ok": "OK",
"mobile.terms_of_service.alert_retry": "Erneut versuchen",
"mobile.terms_of_service.get_terms_error_description": "Stellen Sie sicher, dass Sie über eine Internetverbindung verfügen und probieren Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie ihren Systemadministrator.",
"mobile.terms_of_service.get_terms_error_title": "Konnte Nutzungsbedingungen nicht laden.",
"mobile.terms_of_service.terms_rejected": "Sie müssen die Nutzungsbedingungen akzeptieren, bevor Sie {siteName} verwenden können. Bitte kontaktieren Sie ihren Systemadministrator für mehr Details.",
"mobile.timezone_settings.automatically": "Automatisch einstellen",
"mobile.timezone_settings.manual": "Zeitzone ändern",
"mobile.timezone_settings.select": "Zeitzone auswählen",
"mobile.tos_link": "Nutzungsbedingungen",
"mobile.user_list.deactivated": "Deaktiviert",
"mobile.user.settings.notifications.email.fifteenMinutes": "Alle 15 Minuten",
"mobile.video_playback.failed_description": "Beim Abspielen des Videos ist ein Fehler aufgetreten.\n",
"mobile.video_playback.failed_description": "Es ist ein Fehler beim Abspielen des Videos aufgetreten.\n",
"mobile.video_playback.failed_title": "Videowiedergabe fehlgeschlagen",
"mobile.video.save_error_message": "Um das Video zu speichern, müssen Sie es zuerst herunterladen.",
"mobile.video.save_error_title": "Fehler beim Speichern des Videos",
@@ -445,11 +504,17 @@
"modal.manual_status.auto_responder.message_dnd": "Möchten Sie ihren Status auf \"Nicht stören\" umschalten und automatische Antworten deaktivieren?",
"modal.manual_status.auto_responder.message_offline": "Möchten Sie ihren Status auf \"Offline\" umschalten und automatische Antworten deaktivieren?",
"modal.manual_status.auto_responder.message_online": "Möchten Sie ihren Status auf \"Online\" umschalten und automatische Antworten deaktivieren?",
"more_channels.archivedChannels": "Archivierte Kanäle",
"more_channels.dropdownTitle": "Anzeigen",
"more_channels.noMore": "Keine weiteren Kanäle zum Betreten",
"more_channels.publicChannels": "Öffentliche Kanäle",
"more_channels.showArchivedChannels": "Anzeigen: Archivierte Kanäle",
"more_channels.showPublicChannels": "Anzeigen: Öffentliche Kanäle",
"more_channels.title": "Weitere Kanäle",
"msg_typing.areTyping": "{users} und {last} tippen gerade...",
"msg_typing.isTyping": "{user} tippt...",
"navbar_dropdown.logout": "Abmelden",
"navbar.channel_drawer.button": "Kanäle und Teams",
"navbar.channel_drawer.hint": "Öffnet die Kanal- und Team-Schublade.",
"navbar.leave": "Kanal verlassen",
"password_form.title": "Passwort zurücksetzen",
"password_send.checkInbox": "Bitte prüfen Sie den Posteingang.",
@@ -458,25 +523,26 @@
"password_send.link": "Falls das Konto existiert, wurde eine E-Mail zur Passwortzurücksetzung gesendet an:",
"password_send.reset": "Mein Passwort zurücksetzen",
"permalink.error.access": "Der dauerhafte Link verweist auf eine gelöschte Nachricht oder einen Kanal auf den Sie keinen Zugriff haben.",
"permalink.error.link_not_found": "Link nicht gefunden",
"post_body.check_for_out_of_channel_groups_mentions.message": "wurde durch diese Erwähnung nicht benachrichtigt, da sich der Benutzer nicht im Kanal befinden. Er kann dem Kanal nicht hinzugefügt werden, da er nicht Mitglied der verknüpften Gruppen ist. Um ihn zu diesem Kanal hinzuzufügen, müssen er zu den verknüpften Gruppen hinzugefügt werden.",
"post_body.check_for_out_of_channel_mentions.link.and": " und ",
"post_body.check_for_out_of_channel_mentions.link.private": "sie zu diesem privaten Kanal hinzufügen",
"post_body.check_for_out_of_channel_mentions.link.public": "sie zu diesem Kanal hinzufügen",
"post_body.check_for_out_of_channel_mentions.message_last": "? Sie werden Zugriff auf den Nachrichtenverlauf haben.",
"post_body.check_for_out_of_channel_mentions.message.multiple": "wurden erwähnt, befinden sich aber nicht im Kanal. Möchten Sie ",
"post_body.check_for_out_of_channel_mentions.message.one": "wurde erwähnt, befinden sich aber nicht im Kanal. Möchten Sie ",
"post_body.check_for_out_of_channel_mentions.message.multiple": "wurde durch diese Erwähnung nicht benachrichtigt, da der Benutzer sich nicht im Kanal befindet. Möchten Sie ",
"post_body.check_for_out_of_channel_mentions.message.one": "wurde durch diese Erwähnung nicht benachrichtigt, da der Benutzer sich nicht im Kanal befindet. Möchten Sie ",
"post_body.commentedOn": "Kommentierte auf die Nachricht von {name}: ",
"post_body.deleted": "(Nachricht gelöscht)",
"post_info.auto_responder": "AUTOMATISCHE ANTWORT",
"post_info.bot": "BOT",
"post_info.del": "Löschen",
"post_info.edit": "Bearbeiten",
"post_info.guest": "GAST",
"post_info.message.show_less": "Weniger anzeigen",
"post_info.message.show_more": "Mehr anzeigen",
"post_info.system": "System",
"post_message_view.edited": "(bearbeitet)",
"posts_view.newMsg": "Neue Nachrichten",
"rename_channel.handleHolder": "Kleinbuchstaben oder Ziffern",
"rename_channel.url": "URL",
"rhs_thread.rootPostDeletedMessage.body": "Ein Teil dieses Nachrichtenverlaufes wurde wegen einer Datenaufbewahrungsrichtlinie gelöscht. Sie können nicht länger auf diesen Strang antworten.",
"search_bar.search": "Suche",
"search_header.results": "Suchergebnisse",
@@ -498,20 +564,22 @@
"status_dropdown.set_offline": "Offline",
"status_dropdown.set_online": "Online",
"status_dropdown.set_ooo": "Nicht im Büro",
"suggestion.mention.all": "ACHTUNG: Dies erwähnt jeden im Kanal",
"suggestion.mention.all": "Benachrichtigt jeden in diesem Kanal",
"suggestion.mention.channel": "Benachrichtigt jeden in diesem Kanal",
"suggestion.mention.channels": "Meine Kanäle",
"suggestion.mention.here": "Benachrichtigt jeden der im Kanal und online ist",
"suggestion.mention.here": "Benachrichtigt jeden in diesem Kanal",
"suggestion.mention.members": "Kanalmitglieder",
"suggestion.mention.morechannels": "Andere Kanäle",
"suggestion.mention.nonmembers": "Nicht im Kanal",
"suggestion.mention.special": "Spezielle Erwähnungen",
"suggestion.mention.you": "(Sie)",
"suggestion.search.direct": "Direktnachrichten",
"suggestion.search.private": "Private Kanäle",
"suggestion.search.public": "Öffentliche Kanäle",
"terms_of_service.agreeButton": "Ich stimme zu",
"terms_of_service.api_error": "Konnte die Anfrage nicht abschließen. Falls der Fehler weiterhin besteht, fragen Sie den Systemadministrator.",
"user.settings.display.clockDisplay": "Uhrzeit-Format",
"user.settings.display.custom_theme": "Benutzerdefiniertes Motiv",
"user.settings.display.militaryClock": "24-Stunden-Format (z.B.: 16:00)",
"user.settings.display.normalClock": "12-Stunden-Format (z.B.: 4:00 PM)",
"user.settings.display.preferTime": "Wählen Sie das bevorzugte Zeitformat aus.",
@@ -538,7 +606,7 @@
"user.settings.notifications.email.immediately": "Sofort",
"user.settings.notifications.email.never": "Nie",
"user.settings.notifications.email.send": "E-Mail-Benachrichtigungen senden",
"user.settings.notifications.emailInfo": "E-Mail-Benachrichtigungen werden bei Erwähnungen und Direktnachrichten gesendet, sobald Sie von {siteName} mehr als 5 Minuten offline oder abwesend waren.",
"user.settings.notifications.emailInfo": "E-Mail-Benachrichtigungen werden bei Erwähnungen und Direktnachrichten gesendet, sobald Sie mehr als 5 Minuten offline oder abwesend sind.",
"user.settings.notifications.never": "Nie",
"user.settings.notifications.onlyMentions": "Nur für Erwähnungen und Direktnachrichten",
"user.settings.push_notification.away": "Abwesend oder offline",
@@ -547,4 +615,4 @@
"user.settings.push_notification.offline": "Offline",
"user.settings.push_notification.online": "Online, abwesend oder offline",
"web.root.signup_info": "Die gesamte Teamkommunikation an einem Ort, durchsuchbar und überall verfügbar"
}
}

View File

@@ -253,8 +253,11 @@
"mobile.extension.permission": "Mattermost needs access to the device storage to share files.",
"mobile.extension.team_required": "You must belong to a team before you can share files.",
"mobile.extension.title": "Share in Mattermost",
"mobile.failed_network_action.retry": "try again",
"mobile.failed_network_action.shortDescription": "Messages will load when you have an internet connection or {tryAgainAction}.",
"mobile.failed_network_action.retry": "Try again",
"mobile.failed_network_action.shortDescription": "Messages will load when you have an internet connection.",
"mobile.failed_network_action.teams_channel_description": "Channels could not be loaded for {teamName}.",
"mobile.failed_network_action.teams_description": "Teams could not be loaded.",
"mobile.failed_network_action.teams_title": "Something went wrong",
"mobile.failed_network_action.title": "No internet connection",
"mobile.file_upload.browse": "Browse Files",
"mobile.file_upload.camera_photo": "Take Photo",

View File

@@ -9,7 +9,7 @@
"about.teamEditionSt": "Todas las comunicaciones de tu equipo en un solo lugar, con búsquedas instantáneas y accesible de todas partes.",
"about.teamEditiont0": "Edición Team T0",
"about.teamEditiont1": "Edición Team T1",
"about.title": "Acerca de Mattermost",
"about.title": "Acerca de {appTitle}",
"announcment_banner.dont_show_again": "No volver a mostrar",
"api.channel.add_member.added": "{addedUsername} agregado al canal por {username}",
"archivedChannelMessage": "Estás viendo un **canal archivado**. No serán publicados nuevos mensajes.",
@@ -17,7 +17,7 @@
"channel_header.addMembers": "Agregar Miembros",
"channel_header.directchannel.you": "{displayname} (tu) ",
"channel_header.manageMembers": "Administrar Miembros",
"channel_header.pinnedPosts": "Mensajes Anclados",
"channel_header.pinnedPosts": "Mensajes Destacados",
"channel_header.viewMembers": "Ver Miembros",
"channel_info.header": "Encabezado:",
"channel_info.purpose": "Propósito:",
@@ -35,6 +35,9 @@
"channel_modal.purposeEx": "Ej: \"Un canal para describir errores y mejoras\"",
"channel_notifications.ignoreChannelMentions.settings": "Ignorar @channel, @here, @all",
"channel_notifications.muteChannel.settings": "Silenciar canal",
"channel.channelHasGuests": "Este canal tiene huéspedes",
"channel.hasGuests": "Este grupo de mensajes tiene huéspedes",
"channel.isGuest": "Esta persona es un huésped",
"combined_system_message.added_to_channel.many_expanded": "{users} y {lastUser} fueron **agregados al canal** por {actor}.",
"combined_system_message.added_to_channel.one": "{firstUser} **agregado al canal** por {actor}.",
"combined_system_message.added_to_channel.one_you": "Fuiste **agregado al canal** por {actor}.",
@@ -45,19 +48,19 @@
"combined_system_message.added_to_team.two": "{firstUser} y {secondUser} **agregados al equipo** por {actor}.",
"combined_system_message.joined_channel.many_expanded": "{users} and {lastUser} **se unieron al canal**.",
"combined_system_message.joined_channel.one": "{firstUser} **se unió al canal**.",
"combined_system_message.joined_channel.one_you": "**unieron al canal**.",
"combined_system_message.joined_channel.one_you": "Tú **te uniste al canal**.",
"combined_system_message.joined_channel.two": "{firstUser} y {secondUser} **se unieron al canal**.",
"combined_system_message.joined_team.many_expanded": "{users} y {lastUser} **se unieron al equipo**.",
"combined_system_message.joined_team.one": "{firstUser} **se unió al equipo**.",
"combined_system_message.joined_team.one_you": "**unieron al equipo**.",
"combined_system_message.joined_team.one_you": "Tú **te uniste al equipo**.",
"combined_system_message.joined_team.two": "{firstUser} y {secondUser} **se unieron al equipo**.",
"combined_system_message.left_channel.many_expanded": "{users} y {lastUser} **abandonaron el canal**.",
"combined_system_message.left_channel.one": "{firstUser} **abandonó el canal**.",
"combined_system_message.left_channel.one_you": "**abandonaron el canal**.",
"combined_system_message.left_channel.one_you": "**abandonaste el canal**.",
"combined_system_message.left_channel.two": "{firstUser} y {secondUser} **abandonaron el canal**.",
"combined_system_message.left_team.many_expanded": "{users} y {lastUser} **abandonaron el equipo**.",
"combined_system_message.left_team.one": "{firstUser} **abandonó el equipo**.",
"combined_system_message.left_team.one_you": "**abandonó el equipo**.",
"combined_system_message.left_team.one_you": "**abandonaste el equipo**.",
"combined_system_message.left_team.two": "{firstUser} y {secondUser} **abandonaron el equipo**.",
"combined_system_message.removed_from_channel.many_expanded": "{users} y {lastUser} fueron **eliminados del canal**.",
"combined_system_message.removed_from_channel.one": "{firstUser} fue **eliminado del canal**.",
@@ -70,21 +73,21 @@
"combined_system_message.you": "Tu",
"create_comment.addComment": "Agregar un comentario...",
"create_post.deactivated": "Estás viendo un canal archivado con un usuario desactivado.",
"create_post.write": "Write to {channelDisplayName}",
"create_post.write": "Escribir en {channelDisplayName}",
"date_separator.today": "Hoy",
"date_separator.yesterday": "Ayer",
"edit_post.editPost": "Editar el mensaje...",
"edit_post.save": "Guardar",
"error.team_not_found.title": "Equipo no encontrado",
"file_attachment.download": "Descargar",
"file_upload.fileAbove": "No se puede cargar un archivo de más de {max}MB: {filename}",
"get_post_link_modal.title": "Copiar enlace Permanente",
"get_post_link_modal.title": "Copiar Enlace",
"integrations.add": "Agregar",
"intro_messages.anyMember": " Cualquier miembro se puede unir y leer este canal.",
"intro_messages.beginning": "Inicio de {name}",
"intro_messages.channel": "canal",
"intro_messages.creator": "Este es el inicio del {type} {name}, creado por {creator} el {date}.",
"intro_messages.group": "canal privado",
"intro_messages.creator": "Este es el inicio del canal {name}, creado por {creator} el {date}.",
"intro_messages.creatorPrivate": "Este es el inicio del canal privado {name}, creado por {creator} el {date}.",
"intro_messages.group_message": "Este es el inicio de tu historial del grupo de mensajes con estos compañeros. Los mensajes y archivos que se comparten aquí no son mostrados a personas fuera de esta área.",
"intro_messages.noCreator": "Este es el inicio del {type} {name}, creado el {date}.",
"intro_messages.noCreator": "Este es el inicio del canal {name}, creado el {date}.",
"intro_messages.onlyInvited": " Sólo miembros invitados pueden ver este canal privado.",
"last_users_message.added_to_channel.type": "fueron **agregados al canal** por {actor}.",
"last_users_message.added_to_team.type": "fueron **agregados al equipo** por {actor}.",
@@ -100,7 +103,7 @@
"login_mfa.token": "Token MFA",
"login_mfa.tokenReq": "Por favor ingresa un token MFA",
"login.email": "Correo electrónico",
"login.forgot": "Olvide mi contraseña",
"login.forgot": "Olvide mi contraseña.",
"login.invalidPassword": "La Contraseña es incorrecta.",
"login.ldapUsername": "Usuario AD/LDAP",
"login.ldapUsernameLower": "Nombre de Usuario AD/LDAP",
@@ -128,7 +131,6 @@
"mobile.account_notifications.threads_mentions": "Menciones en hilos",
"mobile.account_notifications.threads_start": "Hilos que yo comience",
"mobile.account_notifications.threads_start_participate": "Hilos que yo comience o participe",
"mobile.account.settings.cancel": "Cancelar",
"mobile.account.settings.save": "Guardar",
"mobile.action_menu.select": "Selecciona una opción",
"mobile.advanced_settings.clockDisplay": "Visualización de la hora",
@@ -137,36 +139,44 @@
"mobile.advanced_settings.delete_title": "Eliminar Documentos & Datos",
"mobile.advanced_settings.timezone": "Zona horaria",
"mobile.advanced_settings.title": "Configuración Avanzada",
"mobile.android.camera_permission_denied_description": "Para tomar fotos y videos con la cámara, por favor, cambia la configuración de permisos.",
"mobile.android.camera_permission_denied_title": "Acceso a la cámara es necesario",
"mobile.android.permission_denied_dismiss": "Cerrar",
"mobile.android.permission_denied_retry": "Establecer permisos",
"mobile.android.photos_permission_denied_description": "Para cargar imágenes desde tu biblioteca, por favor, cambia la configuración de permisos.",
"mobile.android.photos_permission_denied_title": "Acceso a la biblioteca de fotos es necesario",
"mobile.android.storage_permission_denied_description": "Para cargar imágenes desde tu biblioteca de Android, por favor, cambia la configuración de permisos.",
"mobile.android.storage_permission_denied_title": "Acceso a los Archivos es necesario",
"mobile.android.videos_permission_denied_description": "Para subir los vídeos de tu biblioteca, por favor, cambia la configuración de permisos.",
"mobile.android.videos_permission_denied_title": "Acceso a la biblioteca de vídeos es necesario",
"mobile.alert_dialog.alertCancel": "Cancelar",
"mobile.android.photos_permission_denied_description": "Subir fotos a tu servidor de Mattermost o guardarlos en el dispositivo. Abre la Configuración y otorga a Mattermost permisos de Lectura y Escritura a tu librería de fotos.",
"mobile.android.photos_permission_denied_title": "{applicationName} desea acceder a tus fotos",
"mobile.android.videos_permission_denied_description": "Subir videos a tu servidor de Mattermost o guardarlos en el dispositivo. Abre la Configuración y otorga a Mattermost permisos de Lectura y Escritura a tu librería de videos.",
"mobile.android.videos_permission_denied_title": "{applicationName} desea acceder a tus videos",
"mobile.announcement_banner.title": "Anuncio",
"mobile.authentication_error.message": "Mattermost a encontrado un error. Por favor vuelve a autenticar tu usuario para iniciar una nueva sesión.",
"mobile.authentication_error.title": "Error de Autenticación",
"mobile.calendar.dayNames": "Domingo,Lunes,Martes,Miércoles,Jueves,Viernes,Sábado",
"mobile.calendar.dayNamesShort": "Dom,Lun,Mar,Mié,Jue,Vie,Sab",
"mobile.calendar.monthNames": "Enero,Febrero,Marzo,Abril,Mayo,Junio,Julio,Agosto,Septiembre,Octubre,Noviembre,Diciembre",
"mobile.calendar.monthNamesShort": "Ene,Feb,Mar,Abr,May,Jun,Jul,Ago,Sep,Oct,Nov,Dic",
"mobile.camera_photo_permission_denied_description": "Captura fotos y súbelas a tu servidor de Mattermost o guárdalas en tu dispositivo. Abre la Configuración y otorga a Mattermost permisos de Lectura y Escritura para usar la cámara.",
"mobile.camera_photo_permission_denied_title": "{applicationName} desea acceder a tu cámara",
"mobile.camera_video_permission_denied_description": "Captura videos y súbelos a a tu servidor de Mattermost o guárdalos en tu dispositivo. Abre la Configuración y otorga a Mattermost permisos de Lectura y Escritura para acceder tu cámara.",
"mobile.camera_video_permission_denied_title": "{applicationName} desea acceder a tu cámara",
"mobile.channel_drawer.search": "Saltar a...",
"mobile.channel_info.alertMessageConvertChannel": "Al convertir {displayName} a un canal privado, la historia y la membresía son preservados. Archivos compartidos públicamente permanecerán accesibles para cualquier que tenga el enlace. La membresía en un canal privado es solo por invitación \n\nEste es un cambio permanente y no puede deshacerse.\n\n¿Estás seguro que quieres convertir {displayName} a un canal privado?",
"mobile.channel_info.alertMessageDeleteChannel": "¿Seguro quieres archivar el {term} {name}?",
"mobile.channel_info.alertMessageLeaveChannel": "¿Seguro quieres abandonar el {term} {name}?",
"mobile.channel_info.alertMessageUnarchiveChannel": "¿Seguro quieres restaurar el {term} {name}?",
"mobile.channel_info.alertNo": "No",
"mobile.channel_info.alertTitleConvertChannel": "¿Convertir {displayName} a un canal privado?",
"mobile.channel_info.alertTitleDeleteChannel": "Archivar {term}",
"mobile.channel_info.alertTitleLeaveChannel": "Abandonar {term}",
"mobile.channel_info.alertTitleUnarchiveChannel": "Restaurar {term}",
"mobile.channel_info.alertYes": "Sí",
"mobile.channel_info.convert": "Convertir a Canal Privado",
"mobile.channel_info.convert_failed": "No se pudo convertir {displayName} a canal privado.",
"mobile.channel_info.convert_success": "{displayName} ahora es un canal privado.",
"mobile.channel_info.copy_header": "Copiar Encabezado",
"mobile.channel_info.copy_purpose": "Copiar Propósito",
"mobile.channel_info.delete_failed": "No se pudo archivar el canal {displayName}. Por favor revisa tu conexión e intenta de nuevo.",
"mobile.channel_info.edit": "Editar Canal",
"mobile.channel_info.privateChannel": "Canal Privado",
"mobile.channel_info.publicChannel": "Canal Público",
"mobile.channel_info.unarchive_failed": "No se pudo restaurar el canal {displayName}. Por favor revisa tu conexión e intenta de nuevo.",
"mobile.channel_list.alertNo": "No",
"mobile.channel_list.alertYes": "Sí",
"mobile.channel_list.archived": "ARCHIVADO",
"mobile.channel_list.channels": "CANALES",
"mobile.channel_list.closeDM": "Cerrar Mensaje Directo",
"mobile.channel_list.closeGM": "Cerrar Mensaje de Grupo",
@@ -174,7 +184,6 @@
"mobile.channel_list.not_member": "NO MIEMBRO DE",
"mobile.channel_list.unreads": "SIN LEER",
"mobile.channel_members.add_members_alert": "Debes seleccionar al menos un miembro a agregar al canal.",
"mobile.channel.markAsRead": "Marcar como leído",
"mobile.client_upgrade": "Actualizar App",
"mobile.client_upgrade.can_upgrade_subtitle": "Una nueva versión está disponible para su descarga.",
"mobile.client_upgrade.can_upgrade_title": "Actualización Disponible",
@@ -204,6 +213,7 @@
"mobile.create_channel.public": "Nuevo Canal Público",
"mobile.create_post.read_only": "Este canal es de sólo lectura",
"mobile.custom_list.no_results": "Sin resultados",
"mobile.display_settings.sidebar": "Barra lateral",
"mobile.display_settings.theme": "Tema",
"mobile.document_preview.failed_description": "Ocurrió un error al abrir el documento. Por favor verifica que tienes instalado un visor para archivos {fileType} e intenta de nuevo.\n",
"mobile.document_preview.failed_title": "Error Abriendo Documento",
@@ -211,7 +221,6 @@
"mobile.downloader.android_failed": "Descarga fallida",
"mobile.downloader.android_permission": "Se necesita acceso al directorio de descargas para guardar los archivos.",
"mobile.downloader.android_started": "Descarga iniciada",
"mobile.downloader.android_success": "descarga con éxito",
"mobile.downloader.complete": "Descarga completa",
"mobile.downloader.disabled_description": "La descarga de archivos ha sido desactivada en este servidor. Por favor contacta a tu Administrador de Sistemas para más detalles.\n",
"mobile.downloader.disabled_title": "Descarga desactivada",
@@ -223,6 +232,7 @@
"mobile.drawer.teamsTitle": "Equipos",
"mobile.edit_channel": "Guardar",
"mobile.edit_post.title": "Editando Mensaje",
"mobile.edit_profile.remove_profile_photo": "Quitar Foto",
"mobile.emoji_picker.activity": "ACTIVIDAD",
"mobile.emoji_picker.custom": "PERSONALIZADO",
"mobile.emoji_picker.flags": "BANDERAS",
@@ -241,17 +251,24 @@
"mobile.extension.file_limit": "Compartir está limitado a un máximo de 5 archivos.",
"mobile.extension.max_file_size": "Los archivos a compartir en Mattermost no deben superar los {size}.",
"mobile.extension.permission": "Mattermost necesita acceso al almacenamiento del dispositivo para poder compartir archivos.",
"mobile.extension.team_required": "Debes pertenecer a un equipo antes de poder compartir archivos.",
"mobile.extension.title": "Compartir en Mattermost",
"mobile.failed_network_action.description": "Parece haber un problema con tu conexión de internet. Asegura que tienes una conexión activa e intenta de nuevo.",
"mobile.failed_network_action.retry": "Intentar de nuevo",
"mobile.failed_network_action.shortDescription": "Asegura de tener una conexión activa e intenta de nuevo.",
"mobile.failed_network_action.shortDescription": "Los mensajes serán cargados una vez tengas conexión a internet.",
"mobile.failed_network_action.teams_channel_description": "Los canales de {teamName} no pudieron ser cargados.",
"mobile.failed_network_action.teams_description": "Los Equipos no pudieron ser cargados.",
"mobile.failed_network_action.teams_title": "Algo salió mal",
"mobile.failed_network_action.title": "Sin conexión a Internet",
"mobile.file_upload.browse": "Explorar Archivos",
"mobile.file_upload.camera_photo": "Capturar Foto",
"mobile.file_upload.camera_video": "Capturar Vídeo",
"mobile.file_upload.library": "Librería de Fotos",
"mobile.file_upload.max_warning": "Se pueden subir un máximo de 5 archivos.",
"mobile.file_upload.unsupportedMimeType": "Sólo pueden ser utilizadas imágenes BMP, JPG o PNG como imagen de perfil.",
"mobile.file_upload.video": "Librería de Videos",
"mobile.files_paste.error_description": "Ocurrió un error mientras se pegaba(n) archivo(s). Por favor inténtelo de nuevo.",
"mobile.files_paste.error_dismiss": "Descartar",
"mobile.files_paste.error_title": "Pegar falló",
"mobile.flagged_posts.empty_description": "Las banderas son una forma de marcar los mensajes para hacerles seguimiento. Tus banderas son personales, y no puede ser vistas por otros usuarios.",
"mobile.flagged_posts.empty_title": "No hay Mensajes Marcados",
"mobile.help.title": "Ayuda",
@@ -260,7 +277,8 @@
"mobile.intro_messages.default_message": "Es es el primer canal que tus compañeros ven cuando se registran - puedes utilizarlo para enviar mensajes que todos deben leer.",
"mobile.intro_messages.default_welcome": "¡Bienvenido a {name}!",
"mobile.intro_messages.DM": "Este es el inicio de tu historial de mensajes directos con {teammate}.Los mensajes directos y archivos que se comparten aquí no son mostrados a personas fuera de esta área.",
"mobile.ios.photos_permission_denied_description": "Para guardar imágenes y vídeos en tu librería, por favor, cambia la configuración de permisos.",
"mobile.ios.photos_permission_denied_description": "Sube fotos y videos a tu servidor de Mattermost o guárdalos en tu dispositivo. Abre la Configuración y otorga a Mattermost permisos de Lectura y Escritura para acceder tu librería de fotos y videos.",
"mobile.ios.photos_permission_denied_title": "{applicationName} desea acceder a tu librería de fotos",
"mobile.join_channel.error": "No pudimos unirnos al canal {displayName}. Por favor revisa tu conexión e intenta de nuevo.",
"mobile.loading_channels": "Cargando Canales...",
"mobile.loading_members": "Cargando Miembros...",
@@ -271,13 +289,17 @@
"mobile.managed.blocked_by": "Bloqueado por {vendor}",
"mobile.managed.exit": "Salir",
"mobile.managed.jailbreak": "{vendor} no confía en los dispositivos con jailbreak, por favor, salga de la aplicación.",
"mobile.managed.not_secured.android": "Este dispositivo debe estar asegurado con un bloqueo de pantalla para utilizar Mattermost.",
"mobile.managed.not_secured.ios": "Este dispositivo debe estar protegido con un código de acceso para utilizar Mattermost.\n\nVaya a Configuración > Identificación Facial y clave de acceso.",
"mobile.managed.not_secured.ios.touchId": "Este dispositivo debe estar protegido con un código de acceso para utilizar Mattermost.\n\nVaya a Configuración > Identificación Táctil y clave de acceso.",
"mobile.managed.secured_by": "Asegurado por {vendor}",
"mobile.managed.settings": "Ir a configuración",
"mobile.markdown.code.copy_code": "Copiar código",
"mobile.markdown.code.plusMoreLines": "+{count, number} más {count, plural, one {línea} other {líneas}}",
"mobile.markdown.image.too_large": "La imagen excede la dimensión máxima de {maxWidth} x {maxHeight}:",
"mobile.markdown.link.copy_url": "Copiar URL",
"mobile.mention.copy_mention": "Copiar Mención",
"mobile.message_length.message": "El mensaje es demasiado largo. Actual número de caracteres: {max}/{count}",
"mobile.message_length.message": "El mensaje es demasiado largo. Actual número de caracteres: {count}/{max}",
"mobile.message_length.title": "Longitud del Mensaje",
"mobile.more_dms.add_more": "Puedes agregar {remaining, number} usuarios más",
"mobile.more_dms.cannot_add_more": "No puedes agregar más usuarios.",
@@ -289,16 +311,16 @@
"mobile.notice_platform_link": "servidor",
"mobile.notice_text": "Mattermost es hecho posible con software de código abierto utilizado en nuestra {platform} y {mobile}.",
"mobile.notification_settings_mentions.keywords": "Palabras clave",
"mobile.notification_settings_mentions.keywordsDescription": "Otras palabras que desencadenan menciones",
"mobile.notification_settings_mentions.keywordsDescription": "Otras palabras que disparan menciones",
"mobile.notification_settings_mentions.keywordsHelp": "Las palabras clave son sin distinción de mayúsculas y deben estar separadas por comas.",
"mobile.notification_settings_mentions.wordsTrigger": "PALABRAS QUE DESENCADENAN MENCIONES",
"mobile.notification_settings_mentions.wordsTrigger": "PALABRAS QUE DISPARAN MENCIONES",
"mobile.notification_settings_mobile.default_sound": "Predeterminado ({sound})",
"mobile.notification_settings_mobile.light": "Luz",
"mobile.notification_settings_mobile.no_sound": "Ninguno",
"mobile.notification_settings_mobile.push_activity": "ENVIAR NOTIFICACIONES",
"mobile.notification_settings_mobile.push_activity_android": "Enviar notificaciones",
"mobile.notification_settings_mobile.push_status": "ACTIVA LAS NOTIFICACIONES A DISPOSITIVOS MÓVILES CUANDO",
"mobile.notification_settings_mobile.push_status_android": "Activa las notificaciones a dispositivos móviles cuando",
"mobile.notification_settings_mobile.push_status": "DISPARAR NOTIFICACIONES A DISPOSITIVOS MÓVILES CUANDO",
"mobile.notification_settings_mobile.push_status_android": "disparar notificaciones a dispositivos móviles cuando",
"mobile.notification_settings_mobile.sound": "Sonido",
"mobile.notification_settings_mobile.sounds_title": "Sonido de la notificación",
"mobile.notification_settings_mobile.test": "Notificación de prueba",
@@ -307,7 +329,7 @@
"mobile.notification_settings.auto_responder_short": "Respuestas Automáticas",
"mobile.notification_settings.auto_responder.default_message": "Hola, actualmente estoy fuera de la oficina y no puedo responder mensajes.",
"mobile.notification_settings.auto_responder.enabled": "Activada",
"mobile.notification_settings.auto_responder.footer_message": "Asigna un mensaje personalizado que será enviado automáticamente en respuesta a Mensajes Directos. Las menciones en canales Públicos y Privados no enviarán una respuesta automática. Al habilitar la Respuestas Automáticas tu estado será Fuera de Oficina y apagará las notificaciones por correo electrónico y a dispositivos móviles.",
"mobile.notification_settings.auto_responder.footer_message": "Asigna un mensaje personalizado que será enviado automáticamente en respuesta a Mensajes Directos. Las menciones en canales Públicos y Privados no dispararán una respuesta automática. Al habilitar la Respuestas Automáticas tu estado será Fuera de Oficina y apagará las notificaciones por correo electrónico y a dispositivos móviles.",
"mobile.notification_settings.auto_responder.message_placeholder": "Mensaje",
"mobile.notification_settings.auto_responder.message_title": "MENSAJE PERSONALIZADO",
"mobile.notification_settings.email": "Correo electrónico",
@@ -332,20 +354,28 @@
"mobile.open_dm.error": "No pudimos abrir el canal de mensajes directos con {displayName}. Por favor revisa tu conexión e intenta de nuevo.",
"mobile.open_gm.error": "No pudimos abrir el canal del grupo con esos usuarios. Por favor revisa tu conexión e intenta de nuevo.",
"mobile.open_unknown_channel.error": "No se pudo unir al canal. Por favor elimina el cache e intenta de nuevo.",
"mobile.pinned_posts.empty_description": "Ancla elementos importantes manteniendo pulsado cualquier mensaje y selecciona la opción \"Anclar al Canal\".",
"mobile.pinned_posts.empty_title": "No hay Mensajes Anclados",
"mobile.permission_denied_dismiss": "No Permitir",
"mobile.permission_denied_retry": "Ajustes",
"mobile.photo_library_permission_denied_description": "Para guardar imágenes y vídeos en tu librería, por favor, cambia la configuración de permisos.",
"mobile.photo_library_permission_denied_title": "{applicationName} desea acceder a tu librería de fotos",
"mobile.pinned_posts.empty_description": "Destaca elementos importantes manteniendo pulsado cualquier mensaje y selecciona la opción \"Destacar\".",
"mobile.pinned_posts.empty_title": "No hay Mensajes Destacados",
"mobile.post_info.add_reaction": "Reaccionar",
"mobile.post_info.copy_text": "Copiar Texto",
"mobile.post_info.flag": "Marcar",
"mobile.post_info.pin": "Anclar al Canal",
"mobile.post_info.mark_unread": "Marcar No leído",
"mobile.post_info.pin": "Destacar",
"mobile.post_info.reply": "Responder",
"mobile.post_info.unflag": "Desmarcar",
"mobile.post_info.unpin": "Desprender del Canal",
"mobile.post_info.unpin": "No Destacar",
"mobile.post_pre_header.flagged": "Marcado",
"mobile.post_pre_header.pinned": "Anclado",
"mobile.post_pre_header.pinned_flagged": "Anclado y Marcado",
"mobile.post_textbox.empty.message": "Estas intentando enviar un mensaje vacío.\nPor favor asegurate de estar enviando un mensaje con texto o con al menos un archivo adjunto.",
"mobile.post_textbox.empty.ok": "Aceptar",
"mobile.post_textbox.empty.title": "Mensaje vacío",
"mobile.post_pre_header.pinned": "Destacado",
"mobile.post_pre_header.pinned_flagged": "Destacado y Marcado",
"mobile.post_textbox.entire_channel.cancel": "Cancelar",
"mobile.post_textbox.entire_channel.confirm": "Confirmar",
"mobile.post_textbox.entire_channel.message": "Al utilizar @all ó @channel estás a punto de enviar notificaciones a {totalMembers, number} {totalMembers, plural, one {persona} other {personas}}. ¿Estás seguro de querer hacer esto?",
"mobile.post_textbox.entire_channel.message.with_timezones": "Al utilizar @all ó @channel estás a punto de enviar notificaciones a {totalMembers, number} {totalMembers, plural, one {persona} other {personas}} en {timezones, number} {zonas horarias, plural, one {zona horaria} other {zonas horarias}}. ¿Estás seguro de querer hacer esto?",
"mobile.post_textbox.entire_channel.title": "Confirma el envío de notificaciones a todo el canal",
"mobile.post_textbox.uploadFailedDesc": "Algunos archivos adjuntos no se han subido al servidor. ¿Quieres publicar el mensaje?",
"mobile.post_textbox.uploadFailedTitle": "Error Adjuntando",
"mobile.post.cancel": "Cancelar",
@@ -356,7 +386,12 @@
"mobile.post.failed_title": "No se pudo enviar el mensaje",
"mobile.post.retry": "Actualizar",
"mobile.posts_view.moreMsg": "Más Mensajes Arriba",
"mobile.recent_mentions.empty_description": "Mensajes que contienen tu nombre de usuario u otras palabras que desencadenan menciones aparecerán aquí.",
"mobile.privacy_link": "Política de Privacidad",
"mobile.push_notification_reply.button": "Enviar",
"mobile.push_notification_reply.placeholder": "Escribe una respuesta...",
"mobile.push_notification_reply.title": "Responder",
"mobile.reaction_header.all_emojis": "Todos",
"mobile.recent_mentions.empty_description": "Mensajes que contienen tu nombre de usuario u otras palabras que disparan menciones aparecerán aquí.",
"mobile.recent_mentions.empty_title": "No hay Menciones recientes",
"mobile.rename_channel.display_name_maxLength": "El nombre del canal debe tener menos de {maxLength, number} caracteres",
"mobile.rename_channel.display_name_minLength": "El nombre del canal debe ser de {minLength, number} o más caracteres",
@@ -378,6 +413,8 @@
"mobile.routes.channelInfo.createdBy": "Creado por {creator} el ",
"mobile.routes.channelInfo.delete_channel": "Archivar Canal",
"mobile.routes.channelInfo.favorite": "Favorito",
"mobile.routes.channelInfo.groupManaged": "Los miembros son gestionados por grupos enlazados",
"mobile.routes.channelInfo.unarchive_channel": "Restaurar Canal",
"mobile.routes.code": "Código {language}",
"mobile.routes.code.noLanguage": "Código",
"mobile.routes.edit_profile": "Editar Perfil",
@@ -393,6 +430,7 @@
"mobile.routes.thread": "Hilo en {channelName}",
"mobile.routes.thread_dm": "Hilo de Mensaje Directo",
"mobile.routes.user_profile": "Perfil",
"mobile.routes.user_profile.edit": "Editar",
"mobile.routes.user_profile.local_time": "HORA LOCAL",
"mobile.routes.user_profile.send_message": "Enviar Mensaje",
"mobile.search.after_modifier_description": "encontrar mensajes después de una fecha específica",
@@ -405,13 +443,20 @@
"mobile.search.no_results": "No se han encontrado resultados",
"mobile.search.on_modifier_description": "encontrar mensajes de una fecha específica",
"mobile.search.recent_title": "Búsquedas recientes",
"mobile.select_team.guest_cant_join_team": "Tu cuenta de huésped no tiene equipos o canales asignados. Por favor contacta a un administrador.",
"mobile.select_team.join_open": "Equipos a los que te puedes unir",
"mobile.select_team.no_teams": "No hay equipos disponibles a los que te puedas unir.",
"mobile.server_link.error.text": "No se pudo encontrar el enlace en este servidor.",
"mobile.server_link.error.title": "Error de enlace",
"mobile.server_link.unreachable_channel.error": "Este enlace pertenece a un canal eliminado o a un canal al cual no tienes acceso.",
"mobile.server_link.unreachable_team.error": "Este enlace pertenece a un equipo eliminado o a un equipo al cual no tienes acceso.",
"mobile.server_ssl.error.text": "El certificado de {host} no es confiable.\n\nPor favor contacta a tu Administrador de Sistema para resolver los problemas con el certificado y permitir la conexión a este servidor.",
"mobile.server_ssl.error.title": "Certificado no confiable",
"mobile.server_upgrade.button": "Aceptar",
"mobile.server_upgrade.description": "\nEs necesaria una actualización del servidor para utilizar la aplicación de Mattermost. Por favor, preguntale a tu Administrador del Sistema para obtener más detalles.\n",
"mobile.server_upgrade.title": "Es necesario actualizar el Servidor",
"mobile.server_url.invalid_format": "URL debe comenzar con http:// o https://",
"mobile.session_expired": "Sesión Caducada: Por favor, inicia sesión para seguir recibiendo notificaciones.",
"mobile.session_expired": "Sesión expirada: Inicia sesión para continuar recibiendo notificaciones. Las sesiones para {siteName} están configuradas para caducar cada {daysCount, number} {daysCount, plural, un {día} other {días}}.",
"mobile.set_status.away": "Ausente",
"mobile.set_status.dnd": "No Molestar",
"mobile.set_status.offline": "Desconectado",
@@ -422,16 +467,30 @@
"mobile.share_extension.error_message": "Ocurrió un error utilizando la extensión para compartir.",
"mobile.share_extension.error_title": "Error en la Extensión",
"mobile.share_extension.team": "Equipo",
"mobile.share_extension.too_long_message": "Número de letras: {count}/{max}",
"mobile.share_extension.too_long_title": "Mensaje es un largo",
"mobile.sidebar_settings.permanent": "Barra lateral permanente",
"mobile.sidebar_settings.permanent_description": "Mantiene la barra lateral abierta permanentemente",
"mobile.storage_permission_denied_description": "Sube archivos a tu servidor de Mattermost. Abre la Configuración para otorgar permisos de Lectura y Escritura para acceder archivos en este dispositivo.",
"mobile.storage_permission_denied_title": "{applicationName} desea acceder a tus archivos",
"mobile.suggestion.members": "Miembros",
"mobile.system_message.channel_archived_message": "{username} archivó el canal",
"mobile.system_message.channel_unarchived_message": "{username} restauró el canal",
"mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} actualizó el nombre del canal de: {oldDisplayName} a: {newDisplayName}",
"mobile.system_message.update_channel_header_message_and_forget.removed": "{username} eliminó el encabezado del canal (era: {oldHeader})",
"mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} ha actualizado el encabezado del canal de: {oldHeader} a: {newHeader}",
"mobile.system_message.update_channel_header_message_and_forget.updated_to": "{username} ha actualizado el encabezado del canal a: {newHeader}",
"mobile.system_message.update_channel_purpose_message.removed": "{username} eliminó el propósito del canal (era: {oldPurpose})",
"mobile.system_message.update_channel_purpose_message.updated_from": "{username} ha actualizado el propósito del canal de: {oldPurpose} a: {newPurpose}",
"mobile.system_message.update_channel_purpose_message.updated_to": "{username} ha actualizado el propósito del canal a: {newPurpose}",
"mobile.terms_of_service.alert_cancel": "Cancelar",
"mobile.terms_of_service.alert_ok": "Aceptar",
"mobile.terms_of_service.alert_retry": "Intentar de nuevo",
"mobile.terms_of_service.get_terms_error_description": "Asegúrate de tener una conexión activa a internet e inténtalo de nuevo. Si este problema persiste, ponte en contacto con tu Administrador del Sistema.",
"mobile.terms_of_service.get_terms_error_title": "No se pueden cargar los términos de servicio.",
"mobile.terms_of_service.terms_rejected": "Debes aceptar los términos de servicio antes de acceder a {siteName}. Por favor, ponte en contacto con el Administrador del Sistema para obtener más detalles.",
"mobile.timezone_settings.automatically": "Asignar automáticamente",
"mobile.timezone_settings.manual": "Cambiar zona horaria",
"mobile.timezone_settings.select": "Seleccione la zona horaria",
"mobile.tos_link": "Términos de Servicio",
"mobile.user_list.deactivated": "Desactivado",
"mobile.user.settings.notifications.email.fifteenMinutes": "Cada 15 minutos",
"mobile.video_playback.failed_description": "Ocurrió un error al reproducir el vídeo.\n",
@@ -445,11 +504,17 @@
"modal.manual_status.auto_responder.message_dnd": "¿Quieres cambiar to estado a \"No Molestar\" e inhabilitar las respuestas automáticas?",
"modal.manual_status.auto_responder.message_offline": "¿Quieres cambiar to estado a \"Desconectado\" e inhabilitar las respuestas automáticas?",
"modal.manual_status.auto_responder.message_online": "¿Quieres cambiar to estado a \"En línea\" e inhabilitar las respuestas automáticas?",
"more_channels.archivedChannels": "Canales Archivados",
"more_channels.dropdownTitle": "Mostrar",
"more_channels.noMore": "No hay más canales para unirse",
"more_channels.publicChannels": "Canales Públicos",
"more_channels.showArchivedChannels": "Mostrar: Canales Archivados",
"more_channels.showPublicChannels": "Mostrar: Canales Públicos",
"more_channels.title": "Más Canales",
"msg_typing.areTyping": "{users} y {last} están escribiendo...",
"msg_typing.isTyping": "{user} está escribiendo...",
"navbar_dropdown.logout": "Cerrar sesión",
"navbar.channel_drawer.button": "Canales y equipos",
"navbar.channel_drawer.hint": "Abrir la lista de canales y equipos",
"navbar.leave": "Abandonar Canal",
"password_form.title": "Restablecer Contraseña",
"password_send.checkInbox": "Por favor revisa tu bandeja de entrada.",
@@ -458,25 +523,26 @@
"password_send.link": "Si la cuenta existe, una correo electrónico de reinicio de contraseña será enviado a:",
"password_send.reset": "Restablecer mi contraseña",
"permalink.error.access": "El Enlace permanente pertenece a un mensaje eliminado o a un canal al cual no tienes acceso.",
"permalink.error.link_not_found": "Enlace no encontrado",
"post_body.check_for_out_of_channel_groups_mentions.message": "no fueron notificados por esta mención porque no se encuentra en este canal. No pueden ser agregados al canal porque no son miembros de los grupos enlazados. Para agregarlos a este canal, deben ser agregados a alguno de los grupos enlazados.",
"post_body.check_for_out_of_channel_mentions.link.and": " y ",
"post_body.check_for_out_of_channel_mentions.link.private": "agregarlos a este canal privado",
"post_body.check_for_out_of_channel_mentions.link.public": "agregarlos al canal",
"post_body.check_for_out_of_channel_mentions.message_last": "? Tendrán acceso al historial de mensajes.",
"post_body.check_for_out_of_channel_mentions.message.multiple": "fueron mencionados pero no son parte de este canal. Quieres ",
"post_body.check_for_out_of_channel_mentions.message.one": "fue mencionado pero no es parte de este canal. Quieres ",
"post_body.check_for_out_of_channel_mentions.message.multiple": "no fueron notificados por esta mención porque no se encuentran en el canal. Quieres ",
"post_body.check_for_out_of_channel_mentions.message.one": "no fue notificado por esta mención porque no se encuentra en el canal. Quieres ",
"post_body.commentedOn": "Comento en el mensaje de {name}: ",
"post_body.deleted": "(mensaje eliminado)",
"post_info.auto_responder": "RESPUESTA AUTOMÁTICA",
"post_info.bot": "BOT",
"post_info.del": "Borrar",
"post_info.edit": "Editar",
"post_info.message.show_less": "Ver Menos",
"post_info.message.show_more": "Ver s",
"post_info.guest": "HUÉSPEDES",
"post_info.message.show_less": "Ver menos",
"post_info.message.show_more": "Ver más",
"post_info.system": "Sistema",
"post_message_view.edited": "(editado)",
"posts_view.newMsg": "Nuevos Mensajes",
"rename_channel.handleHolder": "Debe tener caracteres alfanuméricos y en minúscula",
"rename_channel.url": "URL",
"rhs_thread.rootPostDeletedMessage.body": "Parte de esta conversación ha sido eliminada debido a la política de retención de datos. No se puede seguir respondiendo a esta conversación.",
"search_bar.search": "Buscar",
"search_header.results": "Resultados de la Búsqueda",
@@ -498,20 +564,22 @@
"status_dropdown.set_offline": "Desconectado",
"status_dropdown.set_online": "En línea",
"status_dropdown.set_ooo": "Fuera de Oficina",
"suggestion.mention.all": "PRECAUCIÓN: Esto menciona a todos los usuarios en el canal",
"suggestion.mention.channel": "Notifica a todas las personas en el canal",
"suggestion.mention.all": "Notifica a todas las personas en este canal",
"suggestion.mention.channel": "Notifica a todas las personas en este canal",
"suggestion.mention.channels": "Mis Canales",
"suggestion.mention.here": "Notifica a todos en el canal que estén en línea",
"suggestion.mention.here": "Notifica a todas las personas disponibles en este canal",
"suggestion.mention.members": "Miembros del Canal",
"suggestion.mention.morechannels": "Otros Canales",
"suggestion.mention.nonmembers": "No en el Canal",
"suggestion.mention.special": "Menciones especiales",
"suggestion.mention.you": "(tú)",
"suggestion.search.direct": "Mensajes Directos",
"suggestion.search.private": "Canales Privados",
"suggestion.search.public": "Canales Públicos",
"terms_of_service.agreeButton": "Acepto",
"terms_of_service.api_error": "No se puede completar la solicitud. Si el problema persiste, contacta a tu Administrador de Sistema.",
"user.settings.display.clockDisplay": "Visualización del Reloj",
"user.settings.display.custom_theme": "Tema Personalizado",
"user.settings.display.militaryClock": "Reloj de 24 horas (ejemplo: 16:00)",
"user.settings.display.normalClock": "Reloj de 12 horas (ejemplo: 4:00 pm)",
"user.settings.display.preferTime": "Selecciona como prefieres mostrar la hora.",
@@ -538,7 +606,7 @@
"user.settings.notifications.email.immediately": "Inmediatamente",
"user.settings.notifications.email.never": "Nunca",
"user.settings.notifications.email.send": "Enviar notificaciones de correo electrónico",
"user.settings.notifications.emailInfo": "El correo electrónico que se envía para notificaciones de menciones y mensajes directos cuando estás ausente o desconectado de {siteName} por más de 5 minutos.",
"user.settings.notifications.emailInfo": "El correo electrónico que se envía para notificaciones de menciones y mensajes directos cuando estás ausente o desconectado por más de 5 minutos.",
"user.settings.notifications.never": "Nunca",
"user.settings.notifications.onlyMentions": "Sólo para menciones y mensajes directos",
"user.settings.push_notification.away": "Ausente o desconectado",
@@ -547,4 +615,4 @@
"user.settings.push_notification.offline": "Desconectado",
"user.settings.push_notification.online": "En línea, ausente o desconectado",
"web.root.signup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte"
}
}

View File

@@ -9,7 +9,7 @@
"about.teamEditionSt": "Toute la communication de votre équipe en un seul endroit, consultable instantanément et accessible de partout.",
"about.teamEditiont0": "Édition Team",
"about.teamEditiont1": "Édition Entreprise",
"about.title": "À propos de Mattermost",
"about.title": "A propos de {appTitle}",
"announcment_banner.dont_show_again": "Ne plus montrer",
"api.channel.add_member.added": "{addedUsername} a été ajouté au canal par {username}.",
"archivedChannelMessage": "Vous consultez un **canal archivé**. Vous ne pouvez pas publier de nouveaux messages.",
@@ -35,6 +35,9 @@
"channel_modal.purposeEx": "Ex. : « Un canal pour rapporter des bogues ou des améliorations »",
"channel_notifications.ignoreChannelMentions.settings": "Ignorer @channel, @here et @all",
"channel_notifications.muteChannel.settings": "Mettre le canal en sourdine",
"channel.channelHasGuests": "The canal dispose d'utilisateurs invités",
"channel.hasGuests": "Ce groupe dispose d'utilisateurs invités",
"channel.isGuest": "Cet utilisateur est un utilisateur invité",
"combined_system_message.added_to_channel.many_expanded": "{users} et {lastUser} ont été **ajoutés au canal** par {actor}.",
"combined_system_message.added_to_channel.one": "{firstUser} **ajouté au canal** par {actor}.",
"combined_system_message.added_to_channel.one_you": "Vous avez été **ajouté au canal** par {actor}.",
@@ -45,19 +48,19 @@
"combined_system_message.added_to_team.two": "{firstUser} et {secondUser} ont été **ajoutés à l'équipe** par {actor}.",
"combined_system_message.joined_channel.many_expanded": "{users} et {lastUser} ont **rejoint le canal**.",
"combined_system_message.joined_channel.one": "{firstUser} a **rejoint le canal**.",
"combined_system_message.joined_channel.one_you": "a **rejoint le canal**.",
"combined_system_message.joined_channel.one_you": "Vous avez **rejoint le canal**.",
"combined_system_message.joined_channel.two": "{firstUser} et {secondUser} ont **rejoint le canal**.",
"combined_system_message.joined_team.many_expanded": "{users} et {lastUser} ont **rejoint l'équipe**.",
"combined_system_message.joined_team.one": "{firstUser} a **rejoint l'équipe**.",
"combined_system_message.joined_team.one_you": "a **rejoint l'équipe**.",
"combined_system_message.joined_team.one_you": "Vous avez **rejoint l'équipe**.",
"combined_system_message.joined_team.two": "{firstUser} et {secondUser} ont **rejoint l'équipe**.",
"combined_system_message.left_channel.many_expanded": "{users} et {lastUser} ont **quitté le canal**.",
"combined_system_message.left_channel.one": "{firstUser} a **quitté le canal**.",
"combined_system_message.left_channel.one_you": "a **quitté le canal**.",
"combined_system_message.left_channel.one_you": "Vous avez **quitté le canal**.",
"combined_system_message.left_channel.two": "{firstUser} et {secondUser} ont **quitté le canal**.",
"combined_system_message.left_team.many_expanded": "{users} et {lastUser} ont **quitté l'équipe**.",
"combined_system_message.left_team.one": "{firstUser} a **quitté l'équipe**.",
"combined_system_message.left_team.one_you": "a **quitté l'équipe**.",
"combined_system_message.left_team.one_you": "Vous avez **quitté l'équipe**.",
"combined_system_message.left_team.two": "{firstUser} et {secondUser} ont **quitté l'équipe**.",
"combined_system_message.removed_from_channel.many_expanded": "{users} et {lastUser} ont été **retirés du canal**.",
"combined_system_message.removed_from_channel.one": "{firstUser} a été **retiré du canal**.",
@@ -70,21 +73,21 @@
"combined_system_message.you": "Vous",
"create_comment.addComment": "Commenter...",
"create_post.deactivated": "Ceci est un canal de messages personnels archivé contenant une discussion avec un utilisateur désactivé.",
"create_post.write": "Write to {channelDisplayName}",
"create_post.write": "Écrire à {channelDisplayName}",
"date_separator.today": "Aujourd'hui",
"date_separator.yesterday": "Hier",
"edit_post.editPost": "Modifier le message...",
"edit_post.save": "Enregistrer",
"error.team_not_found.title": "Équipe introuvable",
"file_attachment.download": "Télécharger",
"file_upload.fileAbove": "Le fichier plus grand que {max}Mo ne peut pas être téléchargé : {filename}",
"get_post_link_modal.title": "Copier le lien permanent",
"get_post_link_modal.title": "Copier le lien",
"integrations.add": "Ajouter",
"intro_messages.anyMember": " Tout membre peut rejoindre et lire ce canal.",
"intro_messages.beginning": "Début de {name}",
"intro_messages.channel": "canal",
"intro_messages.creator": "Ceci est le début de {type} {name}, créé par {creator} le {date}.",
"intro_messages.group": "canal privé",
"intro_messages.creator": "Ceci est le début du canal {name}, créé par {creator} le {date}.",
"intro_messages.creatorPrivate": "Ceci est le début du canal privé {name}, créé par {creator} le {date}.",
"intro_messages.group_message": "Vous êtes au début de votre historique de messages de groupe avec ces utilisateurs. Les messages privés et les fichiers partagés ici ne sont pas visibles par les autres utilisateurs.",
"intro_messages.noCreator": "Ceci est le début de {name} {type}, créé le {date}.",
"intro_messages.noCreator": "Ceci est le début du canal {name}, créé le {date}.",
"intro_messages.onlyInvited": " Seuls les membres invités peuvent voir ce canal privé.",
"last_users_message.added_to_channel.type": "a été **ajouté au canal** par {actor}.",
"last_users_message.added_to_team.type": "a été **ajouté à l'équipe** par {actor}.",
@@ -97,10 +100,10 @@
"last_users_message.removed_from_channel.type": "ont été **retirés de ce canal**.",
"last_users_message.removed_from_team.type": "ont été **retirés de cette équipe**.",
"login_mfa.enterToken": "Pour terminer le processus de connexion, veuillez spécifier le jeton apparaissant dans l'application d'authentification de votre smartphone.",
"login_mfa.token": "Jeton MFA",
"login_mfa.token": "Jeton d'authentification multi-facteurs (MFA)",
"login_mfa.tokenReq": "Veuillez spécifier un jeton d'authentification multi-facteurs (MFA)",
"login.email": "Adresse e-mail",
"login.forgot": "Mot de passe perdu",
"login.forgot": "J'ai perdu mon mot de passe.",
"login.invalidPassword": "Votre mot de passe est incorrect.",
"login.ldapUsername": "Nom dutilisateur AD/LDAP",
"login.ldapUsernameLower": "Nom dutilisateur AD/LDAP",
@@ -128,7 +131,6 @@
"mobile.account_notifications.threads_mentions": "Mentions dans les fils de discussion",
"mobile.account_notifications.threads_start": "Fils de discussion que je démarre",
"mobile.account_notifications.threads_start_participate": "Fils de discussion que je démarre ou auxquels je participe",
"mobile.account.settings.cancel": "Annuler",
"mobile.account.settings.save": "Enregistrer",
"mobile.action_menu.select": "Sélectionnez une option",
"mobile.advanced_settings.clockDisplay": "Affichage de l'horloge",
@@ -137,36 +139,44 @@
"mobile.advanced_settings.delete_title": "Supprimer les documents et les données",
"mobile.advanced_settings.timezone": "Fuseau horaire",
"mobile.advanced_settings.title": "Paramètres avancés",
"mobile.android.camera_permission_denied_description": "Pour prendre des photos et des vidéos avec votre appareil photo, veuillez modifier vos paramètres d'autorisation.",
"mobile.android.camera_permission_denied_title": "L'accès à la caméra est requis",
"mobile.android.permission_denied_dismiss": "Rejeter",
"mobile.android.permission_denied_retry": "Définir les permissions",
"mobile.android.photos_permission_denied_description": "Pour envoyer des images à partir de votre bibliothèque, veuillez modifier vos paramètres d'autorisation.",
"mobile.android.photos_permission_denied_title": "L'accès à la bibliothèque de photos est requis",
"mobile.android.storage_permission_denied_description": "Pour envoyer des images à partir de votre appareil Android, veuillez modifier vos permissions.",
"mobile.android.storage_permission_denied_title": "L'accès au stockage de fichiers est requis",
"mobile.android.videos_permission_denied_description": "Pour envoyer des vidéos à partir de votre bibliothèque, veuillez modifier vos paramètres d'autorisation.",
"mobile.android.videos_permission_denied_title": "L'accès à la bibliothèque de vidéos est requis",
"mobile.alert_dialog.alertCancel": "Annuler",
"mobile.android.photos_permission_denied_description": "Utile pour envoyer des photos à votre instance Mattermost ou les sauvegarder sur votre appareil. Ouvrez les paramètres de votre appareil et accordez à Mattermost les accès de lecture et d'écriture à votre bibliothèque de photos.",
"mobile.android.photos_permission_denied_title": "{applicationName} aimerait accéder à vos photos",
"mobile.android.videos_permission_denied_description": "Utile pour envoyer des vidéos à votre instance Mattermost ou les sauvegarder sur votre appareil. Ouvrez les paramètres de votre appareil et accordez à Mattermost les accès de lecture et d'écriture à votre bibliothèque de vidéos.",
"mobile.android.videos_permission_denied_title": "{applicationName} aimerait accéder à vos vidéos",
"mobile.announcement_banner.title": "Annonce",
"mobile.authentication_error.message": "Mattermost a rencontré un problème. Veuillez vous authentifier à nouveau pour démarrer une nouvelle session.",
"mobile.authentication_error.title": "Erreur d'authentification",
"mobile.calendar.dayNames": "Dimanche,Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi",
"mobile.calendar.dayNamesShort": "Dim,Lun,Mar,Mer,Jeu,Ven,Sam",
"mobile.calendar.monthNames": "Janvier,Février,Mars,Avril,Mai,Juin,Juillet,Août,Septembre,Octobre,Novembre,Décembre",
"mobile.calendar.monthNamesShort": "Jan,Fév,Mar,Avr,Mai,Juin,Juil,Aou,Sep,Oct,Nov,Déc",
"mobile.camera_photo_permission_denied_description": "Utile pour prendre des photos et les envoyer à votre instance Mattermost ou les sauvegarder sur votre appareil. Ouvrez les paramètres de votre appareil et accordez à Mattermost les accès de lecture et d'écriture à votre appareil photo.",
"mobile.camera_photo_permission_denied_title": "{applicationName} aimerait accéder à votre appareil photo",
"mobile.camera_video_permission_denied_description": "Utile pour prendre des vidéos et les envoyer à votre instance Mattermost ou les sauvegarder sur votre appareil. Ouvrez les paramètres de votre appareil et accordez à Mattermost les accès de lecture et d'écriture à votre appareil photo.",
"mobile.camera_video_permission_denied_title": "{applicationName} aimerait accéder à votre appareil photo",
"mobile.channel_drawer.search": "Aller à...",
"mobile.channel_info.alertMessageConvertChannel": "Lorsque vous convertissez {displayName} en canal privé, l'historique du canal ainsi que ses membres sont préservés. Les fichiers partagés publiquement restent accessibles à toute personne qui dispose du lien. Devenir membre d'un canal privé se fait sur invitation uniquement.\n\nCe changement est permanent et ne peut pas être annulé.\n\nVoulez-vous vraiment convertir {displayName} en canal privé ?",
"mobile.channel_info.alertMessageDeleteChannel": "Voulez-vous vraiment archiver le {term} {name} ?",
"mobile.channel_info.alertMessageLeaveChannel": "Voulez-vous vraiment quitter le {term} {name} ?",
"mobile.channel_info.alertMessageUnarchiveChannel": "Voulez-vous vraiment archiver le {term} {name} ?",
"mobile.channel_info.alertNo": "Non",
"mobile.channel_info.alertTitleConvertChannel": "Convertir {displayName} en canal privé ?",
"mobile.channel_info.alertTitleDeleteChannel": "Archiver {term}",
"mobile.channel_info.alertTitleLeaveChannel": "Quitter {term}",
"mobile.channel_info.alertTitleUnarchiveChannel": "Archiver {term}",
"mobile.channel_info.alertYes": "Oui",
"mobile.channel_info.convert": "Convertir en canal privé",
"mobile.channel_info.convert_failed": "Impossible de convertir {displayName} en canal privé.",
"mobile.channel_info.convert_success": "{displayName} est maintenant un canal privé.",
"mobile.channel_info.copy_header": "Copier l'entête",
"mobile.channel_info.copy_purpose": "Copier la description",
"mobile.channel_info.delete_failed": "Impossible d'archiver le canal {displayName}. Veuillez vérifier votre connexion et essayer à nouveau.",
"mobile.channel_info.edit": "Modifier le canal",
"mobile.channel_info.privateChannel": "Canal privé",
"mobile.channel_info.publicChannel": "Canal public",
"mobile.channel_info.unarchive_failed": "Impossible d'archiver le canal {displayName}. Veuillez vérifier votre connexion et essayer à nouveau.",
"mobile.channel_list.alertNo": "Non",
"mobile.channel_list.alertYes": "Oui",
"mobile.channel_list.archived": "ARCHIVÉ",
"mobile.channel_list.channels": "CANAUX",
"mobile.channel_list.closeDM": "Fermer le message personnel",
"mobile.channel_list.closeGM": "Fermer le groupe de message",
@@ -174,7 +184,6 @@
"mobile.channel_list.not_member": "PAS UN MEMBRE",
"mobile.channel_list.unreads": "NON LUS",
"mobile.channel_members.add_members_alert": "Vous devez sélectionner au moins un membre à ajouter à ce canal.",
"mobile.channel.markAsRead": "Marquer comme lu",
"mobile.client_upgrade": "Mettre à jour l'application",
"mobile.client_upgrade.can_upgrade_subtitle": "Une nouvelle version est disponible au téléchargement.",
"mobile.client_upgrade.can_upgrade_title": "Mise à jour disponible",
@@ -183,7 +192,7 @@
"mobile.client_upgrade.download_error.message": "Une erreur s'est produite lors du téléchargement de la nouvelle version.",
"mobile.client_upgrade.download_error.title": "Impossible d'installer la mise à jour",
"mobile.client_upgrade.latest_version": "Votre version : {version}",
"mobile.client_upgrade.listener.dismiss_button": "Rejeter",
"mobile.client_upgrade.listener.dismiss_button": "Annuler",
"mobile.client_upgrade.listener.learn_more_button": "En savoir plus",
"mobile.client_upgrade.listener.message": "Une mise jour du client est disponible !",
"mobile.client_upgrade.listener.upgrade_button": "Mettre à jour",
@@ -204,25 +213,26 @@
"mobile.create_channel.public": "Nouveau canal public",
"mobile.create_post.read_only": "Ce canal est en lecture seule",
"mobile.custom_list.no_results": "Aucun résultat",
"mobile.display_settings.sidebar": "Barre latérale",
"mobile.display_settings.theme": "Thème",
"mobile.document_preview.failed_description": "Une erreur s'est produite lors de l'ouverture du document. Veuillez vous assurer que vous ayez un lecteur de {fileType} installé et réessayez.\n",
"mobile.document_preview.failed_title": "L'ouverture du document a échoué",
"mobile.downloader.android_complete": "Téléchargement terminé",
"mobile.downloader.android_failed": "Échec du téléchargement",
"mobile.downloader.android_permission": "Des permissions au dossier de téléchargements sont requises pour pouvoir sauvegarder les fichiers.",
"mobile.downloader.android_permission": "Des permissions au dossier de téléchargements sont requises pour pouvoir enregistrer des fichiers.",
"mobile.downloader.android_started": "Téléchargement commencé",
"mobile.downloader.android_success": "Téléchargement réussi",
"mobile.downloader.complete": "Téléchargement terminé",
"mobile.downloader.disabled_description": "Le téléchargement de fichiers est désactivé sur ce serveur. Veuillez contacter votre administrateur système pour en savoir plus.\n",
"mobile.downloader.disabled_title": "Téléchargement désactivé",
"mobile.downloader.downloading": "Téléchargement en cours...",
"mobile.downloader.failed_description": "Une erreur s'est produite lors du téléchargement du fichier. Veuillez vérifier votre connexion internet et réessayez.\n",
"mobile.downloader.failed_title": "Échec du téléchargement",
"mobile.downloader.image_saved": "Image sauvegardée",
"mobile.downloader.video_saved": "Vidéo sauvegardée",
"mobile.downloader.image_saved": "Image enregistrée",
"mobile.downloader.video_saved": "Vidéo enregistrée",
"mobile.drawer.teamsTitle": "Équipes",
"mobile.edit_channel": "Enregistrer",
"mobile.edit_post.title": "Edition du message",
"mobile.edit_profile.remove_profile_photo": "Supprimer la photo",
"mobile.emoji_picker.activity": "ACTIVITÉ",
"mobile.emoji_picker.custom": "PERSONNALISÉ",
"mobile.emoji_picker.flags": "DRAPEAUX",
@@ -241,17 +251,24 @@
"mobile.extension.file_limit": "Le partage est limité à un maximum de 5 fichiers.",
"mobile.extension.max_file_size": "Les fichiers partagés dans Mattermost doivent faire moins de {size}.",
"mobile.extension.permission": "Mattermost requiert l'accès au stockage de l'appareil pour partager des fichiers.",
"mobile.extension.team_required": "Vous devez appartenir à une équipe avant de pouvoir partager des fichiers.",
"mobile.extension.title": "Partager dans Mattermost",
"mobile.failed_network_action.description": "Il semble y avoir un problème avec votre connexion Internet. Veuillez vous assurer d'avoir une connexion active et réessayez.",
"mobile.failed_network_action.retry": "essayer",
"mobile.failed_network_action.shortDescription": "Assurez-vous d'avoir une connexion active et réessayez.",
"mobile.failed_network_action.retry": "Essayer à nouveau",
"mobile.failed_network_action.shortDescription": "Messages will load when you have an internet connection.",
"mobile.failed_network_action.teams_channel_description": "Channels could not be loaded for {teamName}.",
"mobile.failed_network_action.teams_description": "Teams could not be loaded.",
"mobile.failed_network_action.teams_title": "Something went wrong",
"mobile.failed_network_action.title": "Aucune connexion Internet",
"mobile.file_upload.browse": "Parcourir les fichiers",
"mobile.file_upload.camera_photo": "Prendre une photo",
"mobile.file_upload.camera_video": "Enregistrer une vidéo",
"mobile.file_upload.library": "Bibliothèque de photos",
"mobile.file_upload.max_warning": "Envois limités à maximum 5 fichiers.",
"mobile.file_upload.unsupportedMimeType": "Seules les images BMP, JPG ou PNG peuvent être utilisées comme photos de profil.",
"mobile.file_upload.video": "Bibliothèque vidéo",
"mobile.files_paste.error_description": "Une erreur s'est produite lors de l'opération de collage du(des) fichier(s). Veuillez réessayer.",
"mobile.files_paste.error_dismiss": "Annuler",
"mobile.files_paste.error_title": "L'opération de collage a échoué",
"mobile.flagged_posts.empty_description": "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.",
"mobile.flagged_posts.empty_title": "Aucun message marqué d'un indicateur",
"mobile.help.title": "Aide",
@@ -260,7 +277,8 @@
"mobile.intro_messages.default_message": "Il s'agit du premier canal que les utilisateurs voient lorsqu'ils s'inscrivent. Utilisezle pour poster des informations que tout le monde devrait connaître.",
"mobile.intro_messages.default_welcome": "Bienvenue {name} !",
"mobile.intro_messages.DM": "Vous êtes au début de votre historique de messages avec {teammate}. Les messages personnels et les fichiers partagés ici ne sont pas visibles par les autres utilisateurs.",
"mobile.ios.photos_permission_denied_description": "Pour sauvegarder des photos et des vidéos dans votre bibliothèque, veuillez modifier vos paramètres d'autorisation.",
"mobile.ios.photos_permission_denied_description": "Utile pour envoyer des photos ou vidéos à votre instance Mattermost ou les sauvegarder sur votre appareil. Ouvrez les paramètres de votre appareil et accordez à Mattermost les accès de lecture et d'écriture à votre bibliothèque de photos et vidéos.",
"mobile.ios.photos_permission_denied_title": "{applicationName} aimerait accéder à vos photos",
"mobile.join_channel.error": "Impossible de joindre le canal {displayName}. Veuillez vérifier votre connexion et essayer à nouveau.",
"mobile.loading_channels": "Chargement des canaux...",
"mobile.loading_members": "Chargement des membres...",
@@ -271,13 +289,17 @@
"mobile.managed.blocked_by": "Bloqué par {vendor}",
"mobile.managed.exit": "Quitter",
"mobile.managed.jailbreak": "Les dispositifs jailbreakés ne sont pas approuvés par {vendor}, veuillez quitter l'application.",
"mobile.managed.not_secured.android": "Cet appareil doit être sécurisé avec un verrouillage d'écran pour pouvoir utiliser Mattermost.",
"mobile.managed.not_secured.ios": "Cet appareil doit être sécurisé avec un code pour pouvoir utiliser Mattermost.\n\nAllez dans Réglages > Face ID et code.",
"mobile.managed.not_secured.ios.touchId": "Cet appareil doit être sécurisé avec un code pour pouvoir utiliser Mattermost.\n\nAllez dans Réglages > Touch ID et code.",
"mobile.managed.secured_by": "Sécurisé par {vendor}",
"mobile.managed.settings": "Aller dans les paramètres",
"mobile.markdown.code.copy_code": "Copier le code",
"mobile.markdown.code.plusMoreLines": "+{count, number} other {count, plural, one {ligne} other {lignes}}",
"mobile.markdown.image.too_large": "L'image dépasse les dimensions maximales de {maxWidth} par {maxHeight} :",
"mobile.markdown.link.copy_url": "Copier l'URL",
"mobile.mention.copy_mention": "Copier la mention",
"mobile.message_length.message": "Votre message courant est trop long. Nombre actuel de caractères : {max}/{count}",
"mobile.message_length.message": "Votre message courant est trop long. Nombre actuel de caractères : {count}/{max}",
"mobile.message_length.title": "Longueur de message",
"mobile.more_dms.add_more": "Vous pouvez encore ajouter {remaining, number} utilisateurs",
"mobile.more_dms.cannot_add_more": "Vous ne pouvez plus ajouter d'utilisateurs",
@@ -323,7 +345,7 @@
"mobile.notification_settings.modal_cancel": "ANNULER",
"mobile.notification_settings.modal_save": "ENREGISTRER",
"mobile.notification_settings.ooo_auto_responder": "Réponses automatiques aux messages personnels",
"mobile.notification_settings.save_failed_description": "Les paramètres de notification n'ont pas pu être enregistrés à cause d'un problème de connexion, merci d'essayer à nouveau.",
"mobile.notification_settings.save_failed_description": "Les paramètres de notification n'ont pas pu être enregistrés à cause d'un problème de connexion, veuillez réessayer.",
"mobile.notification_settings.save_failed_title": "Erreur de connexion",
"mobile.notification.in": " dans ",
"mobile.offlineIndicator.connected": "Connecté",
@@ -332,20 +354,28 @@
"mobile.open_dm.error": "Impossible d'ouvrir un message personnel avec {displayName}. Veuillez vérifier votre connexion et essayer à nouveau.",
"mobile.open_gm.error": "Impossible d'ouvrir un message de groupe avec ces utilisateurs. Veuillez vérifier votre connexion et essayer à nouveau.",
"mobile.open_unknown_channel.error": "Impossible de rejoindre le canal. Veuillez réinitialiser le cache et réessayer.",
"mobile.permission_denied_dismiss": "Ne pas autoriser",
"mobile.permission_denied_retry": "Paramètres",
"mobile.photo_library_permission_denied_description": "Pour enregistrer des photos et des vidéos dans votre bibliothèque, veuillez modifier vos paramètres de permissions.",
"mobile.photo_library_permission_denied_title": "{applicationName} aimerait accéder à votre bibliothèque de photos",
"mobile.pinned_posts.empty_description": "Épingle des éléments importants en maintenant appuyé sur un message, puis en sélectionnant « Épingler au canal ».",
"mobile.pinned_posts.empty_title": "Aucun message épinglé",
"mobile.post_info.add_reaction": "Ajouter une réaction",
"mobile.post_info.copy_text": "Copier le texte",
"mobile.post_info.flag": "Marquer avec un indicateur",
"mobile.post_info.mark_unread": "Marquer comme non lu",
"mobile.post_info.pin": "Épingler au canal",
"mobile.post_info.reply": "Répondre",
"mobile.post_info.unflag": "Supprimer l'indicateur",
"mobile.post_info.unpin": "Désépingler du canal",
"mobile.post_pre_header.flagged": "Marqué d'un indicateur",
"mobile.post_pre_header.pinned": "Épinglé",
"mobile.post_pre_header.pinned_flagged": "Épinglé et marqué d'un indicateur",
"mobile.post_textbox.empty.message": "Vous êtes en train d'envoyer un message vide.\nVeuillez-vous assurer d'avoir spécifié un message ou d'avoir joint au moins un fichier.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Message vide",
"mobile.post_textbox.entire_channel.cancel": "Annuler",
"mobile.post_textbox.entire_channel.confirm": "Confirmer",
"mobile.post_textbox.entire_channel.message": "En utilisant @all ou @channel, vous êtes sur le point d'envoyer des notifications à {totalMembers, number} {totalMembers, plural, one {utilisateur} other {utilisateurs}}. Voulez-vous vraiment continuer ?",
"mobile.post_textbox.entire_channel.message.with_timezones": "En utilisant @all ou @channel, vous êtes sur le point d'envoyer des notifications à {totalMembers, number} {totalMembers, plural, one {utilisateur} other {utilisateurs}} dans {timezones, number} {timezones, plural, one {fuseau horaire} other {fuseaux horaires}}. Voulez-vous vraiment continuer ?",
"mobile.post_textbox.entire_channel.title": "Confirmez l'envoi de notifications au canal en entier",
"mobile.post_textbox.uploadFailedDesc": "Certains fichiers joints n'ont pas pu être envoyés au serveur. Voulez-vous vraiment envoyer votre message ?",
"mobile.post_textbox.uploadFailedTitle": "Erreur de pièces jointes",
"mobile.post.cancel": "Annuler",
@@ -356,6 +386,11 @@
"mobile.post.failed_title": "Impossible d'envoyer votre message",
"mobile.post.retry": "Rafraîchir",
"mobile.posts_view.moreMsg": "Plus de nouveaux messages au-dessus",
"mobile.privacy_link": "Politique de respect de la vie privée",
"mobile.push_notification_reply.button": "Envoyer",
"mobile.push_notification_reply.placeholder": "Écrire une réponse...",
"mobile.push_notification_reply.title": "Répondre",
"mobile.reaction_header.all_emojis": "Toutes",
"mobile.recent_mentions.empty_description": "Les messages qui contiennent votre nom d'utilisateur et d'autres mots qui déclenchent des mentions apparaissent ici.",
"mobile.recent_mentions.empty_title": "Aucune mention récente",
"mobile.rename_channel.display_name_maxLength": "Ce champ doit faire moins de {maxLength, number} caractères",
@@ -378,6 +413,8 @@
"mobile.routes.channelInfo.createdBy": "Créé par {creator} le ",
"mobile.routes.channelInfo.delete_channel": "Archiver le canal",
"mobile.routes.channelInfo.favorite": "Favoris",
"mobile.routes.channelInfo.groupManaged": "Les membres sont gérés par groupes liés.",
"mobile.routes.channelInfo.unarchive_channel": "Archiver le canal",
"mobile.routes.code": "Code {language}",
"mobile.routes.code.noLanguage": "Code",
"mobile.routes.edit_profile": "Éditer le profil",
@@ -393,6 +430,7 @@
"mobile.routes.thread": "Fil de discussion de {channelName}",
"mobile.routes.thread_dm": "Fil de discussion de messages personnels",
"mobile.routes.user_profile": "Profil",
"mobile.routes.user_profile.edit": "Modifier",
"mobile.routes.user_profile.local_time": "HEURE LOCALE",
"mobile.routes.user_profile.send_message": "Envoyer un message",
"mobile.search.after_modifier_description": "pour trouver des messages publiés après une date spécifique",
@@ -405,13 +443,20 @@
"mobile.search.no_results": "Aucun résultat trouvé",
"mobile.search.on_modifier_description": "pour trouver des messages publiés à une date spécifique",
"mobile.search.recent_title": "Recherches récentes",
"mobile.select_team.guest_cant_join_team": "Votre compte d'utilisateur invité n'a pas d'équipe ou de canal assigné. Veuillez contacter un administrateur.",
"mobile.select_team.join_open": "Les autres équipes que vous pouvez rejoindre.",
"mobile.select_team.no_teams": "Il n'y a aucune équipe disponible que vous pouvez rejoindre.",
"mobile.server_link.error.text": "Le lien n'a pas pu être trouvé sur ce serveur.",
"mobile.server_link.error.title": "Erreur de lien",
"mobile.server_link.unreachable_channel.error": "Ce lien correspond à un canal supprimé ou appartenant à un canal auquel vous n'avez pas accès.",
"mobile.server_link.unreachable_team.error": "Ce lien correspond à une équipe supprimée ou appartenant à une équipe à laquelle vous n'avez pas accès.",
"mobile.server_ssl.error.text": "The certificate from {host} is not trusted.\n\nPlease contact your System Administrator to resolve the certificate issues and allow connections to this server.",
"mobile.server_ssl.error.title": "Untrusted Certificate",
"mobile.server_upgrade.button": "OK",
"mobile.server_upgrade.description": "\nUne mise à jour du serveur est requise pour utiliser l'application Mattermost. Veuillez demander à votre administrateur système pour plus de détails.\n",
"mobile.server_upgrade.description": "\nUne mise à jour du serveur est requise pour utiliser l'application Mattermost. Veuillez demander à votre administrateur système pour en savoir plus.\n",
"mobile.server_upgrade.title": "Mise à jour du serveur requise",
"mobile.server_url.invalid_format": "L'URL doit commencer par http:// ou https://",
"mobile.session_expired": "La session a expiré. Veuillez vous connecter pour continuer à recevoir les notifications.",
"mobile.session_expired": "La session a expiré : Veuillez vous connecter pour continuer à recevoir des notifications. Les sessions pour {siteName} sont configurées pour expirer tous les {daysCount, number} {daysCount, plural, one {jour} other {jours}}.",
"mobile.set_status.away": "Absent",
"mobile.set_status.dnd": "Ne pas déranger",
"mobile.set_status.offline": "Hors ligne",
@@ -422,22 +467,36 @@
"mobile.share_extension.error_message": "Une erreur s'est produite lors de l'utilisation de l'extension de partage.",
"mobile.share_extension.error_title": "Erreur d'extension",
"mobile.share_extension.team": "Équipe",
"mobile.share_extension.too_long_message": "Nombre de caractères : {count}/{max}",
"mobile.share_extension.too_long_title": "Le message est trop long",
"mobile.sidebar_settings.permanent": "Barre latérale permanente",
"mobile.sidebar_settings.permanent_description": "Conserver la barre latérale ouverte en permanence",
"mobile.storage_permission_denied_description": "Utile pour envoyer des fichiers à votre instance Mattermost. Ouvrez les paramètres de votre appareil et accordez à Mattermost les accès de lecture et d'écriture aux fichiers.",
"mobile.storage_permission_denied_title": "{applicationName} aimerait accéder à vos fichiers",
"mobile.suggestion.members": "Membres",
"mobile.system_message.channel_archived_message": "{username} a archivé le canal",
"mobile.system_message.channel_unarchived_message": "{username} a archivé le canal",
"mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} à modifié le nom d'affichage du canal de : {oldDisplayName} en : {newDisplayName}",
"mobile.system_message.update_channel_header_message_and_forget.removed": "{username} a supprimé l'entête du canal (précédemment : {oldHeader})",
"mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} a mis à jour l'entête du canal de : {oldHeader} en : {newHeader}",
"mobile.system_message.update_channel_header_message_and_forget.updated_to": "{username} a mis à jour l'entête du canal en : {newHeader}",
"mobile.system_message.update_channel_purpose_message.removed": "{username} a supprimé la description du canal (précédemment: {oldPurpose})",
"mobile.system_message.update_channel_purpose_message.updated_from": "{username} a mis à jour la description du canal de : {oldPurpose} en : {newPurpose}",
"mobile.system_message.update_channel_purpose_message.updated_to": "{username} a mis à jour la description du canal en : {newPurpose}",
"mobile.terms_of_service.alert_cancel": "Annuler",
"mobile.terms_of_service.alert_ok": "OK",
"mobile.terms_of_service.alert_retry": "Réessayer",
"mobile.terms_of_service.get_terms_error_description": "Assurez-vous d'avoir une connexion internet active et réessayez. Si ce problème persiste, contactez votre administrateur système.",
"mobile.terms_of_service.get_terms_error_title": "Impossible de charger les conditions d'utilisation.",
"mobile.terms_of_service.terms_rejected": "Vous devez accepter les conditions d'utilisation avant de pouvoir accéder à {siteName}. Veuillez contacter votre administrateur système pour plus d'informations.",
"mobile.terms_of_service.terms_rejected": "Vous devez accepter les conditions d'utilisation avant de pouvoir accéder à {siteName}. Veuillez contacter votre administrateur système pour en savoir plus..",
"mobile.timezone_settings.automatically": "Définir automatiquement",
"mobile.timezone_settings.manual": "Changer le fuseau horaire",
"mobile.timezone_settings.select": "Sélectionner un fuseau horaire",
"mobile.tos_link": "Conditions d'utilisation",
"mobile.user_list.deactivated": "Désactivé",
"mobile.user.settings.notifications.email.fifteenMinutes": "Toutes les 15 minutes",
"mobile.video_playback.failed_description": "Une erreur s'est produite lors de la tentative de lecture de la vidéo.\n",
"mobile.video_playback.failed_title": "La lecture de la vidéo a échoué",
"mobile.video.save_error_message": "Pour enregistrer la vidéo, vous devez d'abord la télécharger.",
"mobile.video.save_error_title": "Erreur lors de la sauvegarde de la vidéo",
"mobile.video.save_error_title": "Erreur lors de l'enregistrement de la vidéo",
"mobile.youtube_playback_error.description": "Une erreur s'est produite lors de la lecture de la vidéo YouTube.\nDétails : {details}",
"mobile.youtube_playback_error.title": "Erreur de lecture YouTube",
"modal.manual_status.auto_responder.message_": "Voulez-vous changer votre statut sur « {status} » et désactiver les Réponses Automatiques ?",
@@ -445,11 +504,17 @@
"modal.manual_status.auto_responder.message_dnd": "Voulez-vous changer votre statut sur « Ne pas déranger » et désactiver les Réponses Automatiques ?",
"modal.manual_status.auto_responder.message_offline": "Voulez-vous changer votre statut sur « Hors ligne » et désactiver les Réponses Automatiques ?",
"modal.manual_status.auto_responder.message_online": "Voulez-vous changer votre statut sur « En ligne » et désactiver les Réponses Automatiques ?",
"more_channels.archivedChannels": "Canaux archivés",
"more_channels.dropdownTitle": "Afficher",
"more_channels.noMore": "Il n'y a plus d'autre canal que vous pouvez rejoindre",
"more_channels.publicChannels": "Canaux publics",
"more_channels.showArchivedChannels": "Afficher : Canaux archivés",
"more_channels.showPublicChannels": "Afficher : Canaux publics",
"more_channels.title": "Plus de canaux",
"msg_typing.areTyping": "{users} et {last} sont en train d'écrire...",
"msg_typing.isTyping": "{user} est en train d'écrire...",
"navbar_dropdown.logout": "Se déconnecter",
"navbar.channel_drawer.button": "Canaux et équipes",
"navbar.channel_drawer.hint": "Ouvre le menu de canaux et d'équipes",
"navbar.leave": "Quitter le canal",
"password_form.title": "Réinitialisation du mot de passe",
"password_send.checkInbox": "Veuillez vérifier votre boîte de réception.",
@@ -458,25 +523,26 @@
"password_send.link": "Si le compte existe, un e-mail de redéfinition de mot de passe sera envoyé à :",
"password_send.reset": "Réinitialiser mon mot de passe",
"permalink.error.access": "Ce lien correspond à un message supprimé ou appartenant à un canal auquel vous n'avez pas accès.",
"permalink.error.link_not_found": "Lien introuvable",
"post_body.check_for_out_of_channel_groups_mentions.message": "n'a pas été notifié par cette mention. L'utilisateur n'est pas dans le canal et ne peut pas être ajouté à ce canal, car il ne fait pas partie des groupes liés. Pour l'ajouter à ce canal, l'utilisateur doit être ajouté aux groupes liés.",
"post_body.check_for_out_of_channel_mentions.link.and": " et ",
"post_body.check_for_out_of_channel_mentions.link.private": "ajouter à ce canal privé",
"post_body.check_for_out_of_channel_mentions.link.public": "ajouter à ce canal",
"post_body.check_for_out_of_channel_mentions.message_last": "? Ils auront alors accès à tout l'historique de messages pour ce canal.",
"post_body.check_for_out_of_channel_mentions.message.multiple": "ont été mentionnés, mais ne sont pas dans le canal. Voulez-vous ",
"post_body.check_for_out_of_channel_mentions.message.one": "a été mentionné, mais n'est pas dans le canal. Voulez-vous ",
"post_body.check_for_out_of_channel_mentions.message.multiple": "n'a pas été notifié par cette mention, car l'utilisateur n'est pas dans le canal. Voulez-vous ",
"post_body.check_for_out_of_channel_mentions.message.one": "n'ont pas été notifiés par cette mention, car ces utilisateurs ne sont pas dans le canal. Voulez-vous ",
"post_body.commentedOn": "a commenté le message de {name} : ",
"post_body.deleted": "(message supprimé)",
"post_info.auto_responder": "RÉPONSE AUTOMATIQUE",
"post_info.bot": "BOT",
"post_info.del": "Supprimer",
"post_info.edit": "Éditer",
"post_info.guest": "INVITÉ",
"post_info.message.show_less": "Afficher moins",
"post_info.message.show_more": "Afficher plus",
"post_info.system": "Système",
"post_message_view.edited": "(édité)",
"posts_view.newMsg": "Nouveaux messages",
"rename_channel.handleHolder": "caractères alphanumériques minuscules",
"rename_channel.url": "URL",
"rhs_thread.rootPostDeletedMessage.body": "Une partie de ce fil de discussion a été supprimée à cause d'une politique de rétention de données. Vous ne pouvez plus répondre à ce fil.",
"search_bar.search": "Rechercher",
"search_header.results": "Résultats de la recherche",
@@ -498,28 +564,30 @@
"status_dropdown.set_offline": "Hors ligne",
"status_dropdown.set_online": "En ligne",
"status_dropdown.set_ooo": "Absent du bureau",
"suggestion.mention.all": "ATTENTION : Ceci mentionne tout le monde dans le canal",
"suggestion.mention.channel": "Notifier tout le monde dans le canal",
"suggestion.mention.all": "Envoie une notification à tous les membres de ce canal",
"suggestion.mention.channel": "Envoie une notification à tous les membres de ce canal",
"suggestion.mention.channels": "Mes canaux",
"suggestion.mention.here": "Notifier toutes les personnes connectées dans ce canal",
"suggestion.mention.here": "Envoie une notification à tous les membres considérés comme en ligne de ce canal",
"suggestion.mention.members": "Membres du canal",
"suggestion.mention.morechannels": "Autres canaux",
"suggestion.mention.nonmembers": "Pas dans le canal",
"suggestion.mention.special": "Mentions spéciales",
"suggestion.mention.you": "(vous)",
"suggestion.search.direct": "Messages personnels",
"suggestion.search.private": "Canaux privés",
"suggestion.search.public": "Canaux publics",
"terms_of_service.agreeButton": "Je suis d'accord",
"terms_of_service.api_error": "Impossible de terminer la requête. Si ce problème persiste, contactez votre administrateur système.",
"user.settings.display.clockDisplay": "Affichage de l'horloge",
"user.settings.display.custom_theme": "Thème personnalisé",
"user.settings.display.militaryClock": "Horloge 24 heures (ex. : 16:00)",
"user.settings.display.normalClock": "Horloge 12 heures (ex. : 4:00 PM)",
"user.settings.display.preferTime": "Choisissez la façon dont vous préférez voir les heures affichées dans l'application.",
"user.settings.general.email": "E-mail",
"user.settings.general.emailCantUpdate": "L'adresse e-mail ne peut être mise à jour qu'en utilisant un navigateur web ou l'application de bureau.",
"user.settings.general.emailGitlabCantUpdate": "La connexion s'effectue par Gitlab. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email}.",
"user.settings.general.emailGoogleCantUpdate": "La connexion s'effectue par Gitlab. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email} .",
"user.settings.general.emailHelp2": "L'envoi d'e-mails a été désactivé par votre administrateur système. Aucun e-mail de notification ne peut être envoyé.",
"user.settings.general.emailGoogleCantUpdate": "La connexion s'effectue par Google. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email} .",
"user.settings.general.emailHelp2": "L'envoi d'e-mails a été désactivé par votre administrateur système. Aucune notification par e-mail ne peut être envoyée.",
"user.settings.general.emailLdapCantUpdate": "La connexion s'effectue par AD/LDAP. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email}.",
"user.settings.general.emailOffice365CantUpdate": "La connexion s'effectue par Office 365. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email} .",
"user.settings.general.emailSamlCantUpdate": "La connexion s'effectue via SAML. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email}.",
@@ -538,7 +606,7 @@
"user.settings.notifications.email.immediately": "Immédiatement",
"user.settings.notifications.email.never": "Jamais",
"user.settings.notifications.email.send": "Envoyer des notifications de bureau",
"user.settings.notifications.emailInfo": "Les e-mails de notification sont envoyés pour les mentions et les messages personnels reçus après que vous soyez passé hors-ligne ou absent de {siteName} pendant plus de 5 minutes.",
"user.settings.notifications.emailInfo": "Les notifications par e-mail sont envoyées pour les mentions et les messages personnels reçus lorsque vous êtes hors-ligne ou absent pour plus de 5 minutes.",
"user.settings.notifications.never": "Jamais",
"user.settings.notifications.onlyMentions": "Seulement pour les mentions et messages personnels",
"user.settings.push_notification.away": "Absent ou hors ligne",
@@ -547,4 +615,4 @@
"user.settings.push_notification.offline": "Hors ligne",
"user.settings.push_notification.online": "En ligne, absent(e) ou hors ligne",
"web.root.signup_info": "Toute la communication de votre équipe au même endroit, accessible de partout"
}
}

Some files were not shown because too many files have changed in this diff Show More