Merge beta into master (#2143)

* [FIX] Messages being sent but showing as temp status (#1469)

* [FIX] Missing messages after reconnect (#1470)

* [FIX] Few fixes on themes (#1477)

* [I18N] Missing German translations (#1465)

* Missing German translation

* adding a missing space behind colon

* added a missing space after colon

* and another attempt to finally fix this – got confused by all the branches

* some smaller fixes for the translation

* better wording

* fixed another typo

* [FIX] Crash while displaying the attached image with http on file name (#1401)

* [IMPROVEMENT] Tap app and server version to copy to clipboard (#1425)

* [NEW] Reply notification (#1448)

* [FIX] Incorrect background color login on iPad (#1480)

* [FIX] Prevent multiple tap on send (Share Extension) (#1481)

* [NEW] Image Viewer (#1479)

* [DOCS] Update Readme (#1485)

* [FIX] Jitsi with Hermes Enabled (#1523)

* [FIX] Draft messages not working with themed Messagebox (#1525)

* [FIX] Go to direct message from members list (#1519)

* [FIX] Make SAML wait for idp token instead of creating it on client (#1527)

* [FIX] Server Test Push Notification (#1508)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Update to new server response (#1509)

* [FIX] Insert messages with blank users (#1529)

* Bump version to 4.2.1 (#1530)

* [FIX] Error when normalizing empty messages (#1532)

* [REGRESSION] CAS (#1570)

* Bump version to 4.2.2 (#1571)

* [FIX] Add username block condition to prevent error (#1585)

* Bump version to 4.2.3

* Bump version to 4.2.4

* Bump version to 4.3.0 (#1630)

* [FIX] Channels doesn't load (#1586)

* [FIX] Channels doesn't load

* [FIX] Update roomsUpdatedAt when subscriptions.length is 0

* [FIX] Remove unnecessary changes

* [FIX] Improve the code

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Make SAML to work on Rocket.Chat < 2.3.0 (#1629)

* [NEW] Invite links (#1534)

* [FIX] Set the http-agent to the form that Rocket.Chat requires for logging (#1482)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] "Following thread" and "Unfollowed Thread" is hardcoded and not translated (#1625)

* [FIX] Disable reset button if form didn't changed (#1569)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Header title of RoomInfoView (#1553)

* [I18N] Gallery Permissions DE (#1542)

* [FIX] Not allow to send messages to archived room (#1623)

* [FIX] Profile fields automatically reset (#1502)

* [FIX] Show attachment on ThreadMessagesView (#1493)

* [NEW] Wordpress auth (#1633)

* [CHORE] Add Start Packager script (#1639)

* [CHORE] Update RN to 0.61.5 (#1638)

* [CHORE] Update RN to 0.61.5

* [CHORE] Update react-native patch

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* Bump version to 4.3.1 (#1641)

* [FIX] Change force logout rule (#1640)

* Bump version to 4.4.0 (#1643)

* [IMPROVEMENT] Use MessagingStyle on Android Notification (#1575)

* [NEW] Request review (#1627)

* [NEW] Pull to refresh RoomView (#1657)

* [FIX] Unsubscribe from room (#1655)

* [FIX] Server with subdirs (#1646)

* [NEW] Clear cache (#1660)

* [IMPROVEMENT] Memoize and batch subscriptions updates (#1642)

* [FIX] Disallow empty sharing (#1664)

* [REGRESSION] Use HTTPS links for sharing and markets protocol for review (#1663)

* [FIX] In some cases, share extension doesn't load images (#1649)

* [i18n] DE translations for new invite function and some minor fixes (#1631)

* [FIX] Remove duplicate jetify step (#1628)

minor: also remove 'cd' calls

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [REGRESSION] Read messages (#1666)

* [i18n] German translations missing (#1670)

* [FIX] Notifications crash on older Android Versions (#1672)

* [i18n] Added Dutch translation (#1676)

* [NEW] Omnichannel Beta (#1674)

* [NEW] Confirm logout/clear cache (#1688)

* [I18N] Add es-ES language  (#1495)

* [NEW] UiKit Beta (#1497)

* [IMPROVEMENT] Use reselect (#1696)

* [FIX] Notification in Android API level less than 24 (#1692)

* [IMPROVEMENT] Send tmid on slash commands and media (#1698)

* [FIX] Unhandled action on UIKit (#1703)

* [NEW] Pull to refresh RoomsList (#1701)

* [IMPROVEMENT] Reset app when language is changed (#1702)

* [FIX] Small fixes on UIKit (#1709)

* [FIX] Spotlight (#1719)

* [CHORE] Update react-native-image-crop-picker (#1712)

* [FIX] Messages Overlapping (Android) and MessageBox Scroll (iOS) (#1720)

* [REGRESSION] Remove @ and # from mention (#1721)

* [NEW] Direct message from user info (#1516)

* [FIX] Delete slash commands (#1723)

* [IMPROVEMENT] Hold URL to copy (#1684)

* [FIX] Different sourcemaps generation for Hermes (#1724)

* [FIX] Different sourcemaps generation for Hermes

* Upload sourcemaps after build

* [REVERT] Show emoji keyboard on Android (#1738)

* [FIX] Stop logging react-native-image-crop-picker (#1745)

* [FIX] Prevent toast ref error (#1744)

* [FIX] Prevent reaction map error (#1743)

* [FIX] Add missing calls to user info (#1741)

* [FIX] Catch room unsubscribe error (#1739)

* [i18n] Missing German keys (#1735)

* [FIX] Missing i18n on MessagesView title (#1733)

* [FIX]  UIKit Modal: Weird behavior on Android Tablet (#1742)

* [i18n] Missing key on German (#1747)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Add Italian (#1736)

* [CHORE] Memory leaks investigation (#1675)

* [IMPROVEMENT] Alert verify email when enabled (#1725)

* [NEW] Jitsi JWT added to URL (#1746)

* [FIX] UIKit submit when connection lost (#1748)

* Bump version to 4.5.0 (#1761)

* [NEW] Default browser (#1752)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] HTTP Basic Auth (#1753)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Honor profile fields edit settings (#1687)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Room announcements (#1726)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Honor Register/Login settings (#1727)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Make links clickable on Room Info (#1730)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Hide system messages (#1755)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Honor "Message_AudioRecorderEnabled" (#1764)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Missing de keys (#1765)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Redirect user to SetUsernameView (#1728)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Join Room (#1769)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Accept all media types using * (#1770)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Use RealName when necessary (#1758)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Markdown Line Break (#1783)

* [IMPROVEMENT] Remove useMarkdown (#1774)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Open browser rather than webview on Create Workspace (#1788)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Markdown perf (#1796)

* [FIX] Stop video when modal is closed (#1787)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Hide reply notification action when there are missing data (#1771)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Added Japanese translation (#1781)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Reset password error message (#1772)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Close tablet modal (#1773)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Setting not present (#1775)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Thread header (#1776)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Keyboard tracking loses input ref (#1784)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Mark message as unread (#1785)

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [IMPROVEMENT] Log server version (#1786)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Add loading message on long running tasks (#1798)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Switch Apple account on Fastlane (#1810)

* [FIX] Watermelon throwing "Cannot update a record with pending updates" (#1754)

* [FIX] Detox tests (#1790)

* [CHORE] Use markdown preview on RoomView Header (#1807)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] LoginSignup blink services (#1809)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Request user presence on demand (#1813)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Remove all invited users when create a channel (#1814)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Pop from room which you have been removed (#1819)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Room Info styles (#1820)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Add missing German keys (#1800)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Empty mentions for @all and @here when real name is enabled (#1822)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Markdown added to Storybook (#1812)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [REGRESSION] Room View header title (#1827)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Storybook snapshots (#1831)

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [FIX] Mentions (#1829)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Thread message not found (#1830)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Separate delete and remove channel (#1832)

* Rename to delete room

* Separate delete and remove channel

* handleRemoved -> handleRoomRemoved

* [FIX] Navigate to RoomsList & Handle tablet case

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [NEW] Filter system messages per room (#1815)

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] e2e tests (#1838)

* [FIX] Consecutive clear cache calls freezing app (#1851)

* Bump version to 4.5.1 (#1853)

* [FIX][iOS] Ignore silent mode on audio player (#1862)

* [IMPROVEMENT] Create App Group property on Info.plist (#1858)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Make username clickable on message (#1618)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Show proper error message on profile (#1768)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Show toast when a message is starred/unstarred (#1616)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Incorrect size params to avatar endpoint (#1875)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Remove unrecognized emoji flags on android  (#1887)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Remove react-native global installs (#1886)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Emojis transparent on android (#1881)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump acorn from 5.7.3 to 5.7.4 (#1876)

Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump version to 4.6.0 (#1911)

* [FIX] Encode Image URI (#1909)

* [FIX] Encode Image URI

* [FIX] Check if Image is Valid

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Adaptive Icons (#1904)

* Remove unnecessary stuff from debug build

* Adaptive icon for experimental app

* [FIX] Stop showing message on leave channel (#1896)

* [FIX] Leave room don't show 'was removed' message

* [FIX] Remove duplicated code

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Added missing German translations(#1900)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Linkedin OAuth login (#1913)

* [CHORE] Fix typo in CreateChannel View (#1930)

* [FIX] Respect protocol in HTTP Auth IPs (#1933)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Use new LinkedIn OAuth url (#1935)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [CHORE] Use storyboard on splash screen (#1939)

* Update react-native-bootsplash

* iOS

* Fix android

* [FIX] Check if avatar exists before create Icon (#1927)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Ignore self typing event (#1950)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Change default directory listing to Users (#1948)

* fix: change default directory listing to Users

* follow server settings

* Fix state to props

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Onboarding layout (#1954)

* Onboarding texts

* OnboardingView

* FormContainer

* Minor fixes

* NewServerView

* Remove code

* Refactor

* WorkspaceView

* Stash

* Login with email working

* Login with

* Join open

* Revert "Login with"

This reverts commit d05dc507d2e9a2db76d433b9b1f62192eba35dbd.

* Fix create account styles

* Register

* Refactor

* LoginServices component

* Refactor

* Multiple servers

* Remove native images

* Refactor styles

* Fix testid

* Fix add server on tablet

* i18n

* Fix close modal

* Fix TOTP

* [FIX] Registration disabled

* [FIX] Login Services separator

* Fix logos

* Fix AppVersion name

* I18n

* Minor fixes

* [FIX] Custom Fields

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [NEW] Create discussions (#1942)

* [WIP][NEW] Create Discussion

* [FIX] Clear multiselect & Translations

* [NEW] Create Discussion at MessageActions

* [NEW] Disabled Multiselect

* [FIX] Initial channel

* [NEW] Create discussion on MessageBox Actions

* [FIX] Crashing on edit name

* [IMPROVEMENT] New message layout

* [CHORE] Update README

* [NEW] Avatars on MultiSelect

* [FIX] Select Users

* [FIX] Add redirect and Handle tablet

* [IMPROVEMENT] Split CreateDiscussionView

* [FIX] Create a discussion inner discussion

* [FIX] Create a discussion

* [I18N] Add pt-br

* Change icons

* [FIX] Nav to discussion & header title

* Fix header

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Load messages (#1910)

* Create updateLastOpen param on readMessages

* Remove InteractionManager from load messages

* [NEW] Custom Status (#1811)

* [NEW] Custom Status

* [FIX] Subscribe to changes

* [FIX] Improve code using Banner component

* [IMPROVEMENT] Toggle modal

* [NEW] Edit custom status from Sidebar

* [FIX] Modal when tablet

* [FIX] Styles

* [FIX] Switch to react-native-promp-android

* [FIX] Custom Status UI

* [TESTS] E2E Custom Status

* Fix banner

* Fix banner

* Fix subtitle

* status text

* Fix topic header

* Fix RoomActionsView topic

* Fix header alignment on Android

* [FIX] RoomInfo crashes when without statusText

* [FIX] Use users.setStatus

* [FIX] Remove customStatus of ProfileView

* [FIX] Room View Thread Header

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] UI issues of Create Discussion View (#1965)

* [NEW] Direct Message between multiple users (#1958)

* [WIP] DM between multiple users

* [WIP][NEW] Create new DM between multiple users

* [IMPROVEMENT] Improve createChannel Sagas

* [IMPROVEMENT] Selected Users view

* [IMPROVEMENT] Room Actions of Group DM

* [NEW] Create new DM between multiple users

* [NEW] Group DM avatar

* [FIX] Directory border

* [IMPROVEMENT] Use isGroupChat

* [CHORE] Remove legacy getRoomMemberId

* [NEW] RoomTypeIcon

* [FIX] No use legacy method on RoomInfoView

* [FIX] Blink header when create new DM

* [FIX] Only show create direct message option when allowed

* [FIX] RoomInfoView

* pt-BR

* Few fixes

* Create button name

* Show create button only after a user is selected

* Fix max users issues

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Add server and hide login (#1968)

* Navigate to new server workspace from ServerDropdown if there's no token

* Hide login button based on login services and Accounts_ShowFormLogin setting

* [FIX] Lint

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [FIX] MultiSelect Keyboard behavior (Android) (#1969)

* fixed-modal-position

* made-changes

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [FIX] Bottom border style on DirectoryView (#1963)

* [FIX] Border style

* [FIX] Refactoring

* [FIX] fix color of border

* Undo

Co-authored-by: Aroo <azhaubassar@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Clear settings on server change (#1967)

* [FIX] Deeplinking without RoomId (#1925)

* [FIX] Deeplinking without rid

* [FIX] Join channel

* [FIX] Deep linking without rid

* Update app/lib/methods/canOpenRoom.js

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Two Factor authentication via email (#1961)

* First api call working

* [NEW] REST API Post wrapper 2FA

* [NEW] Send 2FA on Email

* [I18n] Add translations

* [NEW] Translations & Cancel totp

* [CHORE] Totp -> TwoFactor

* [NEW] Two Factor by email

* [NEW] Tablet Support

* [FIX] Text colors

* [NEW] Password 2fa

* [FIX] Encrypt password on 2FA

* [NEW] MethodCall2FA

* [FIX] Password fallback

* [FIX] Wrap all post/methodCall with 2fa

* [FIX] Wrap missed function

* few fixes

* [FIX] Use new TOTP on Login

* [improvement] 2fa methodCall

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [FIX] Correct message for manual approval user Registration (#1906)

* [FIX] Correct message for manual approval from admin shown on Registeration

* lint fix - added semicolon

* Updated the translations

* [FIX] Translations

* i18n to match server

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Direct Message between multiple users REST (#1974)

* [FIX] Investigate app losing connection issues (#1890)

* [WIP] Reopen without timeOut & ping with 5 sec & Fix Unsubscribe

* [FIX] Remove duplicated close

* [FIX] Use no-dist lib

* [FIX] Try minor fix

* [FIX] Try reopen connection when app was put on foreground

* [FIX] Remove timeout

* [FIX] Build

* [FIX] Patch

* [FIX] Snapshot

* [IMPROVEMENT] Decrease time to reopen

* [FIX] Some fixes

* [FIX] Update sdk version

* [FIX] Subscribe Room Once

* [CHORE] Update sdk

* [FIX] Subscribe Room

* [FIX] Try to resend missed subs

* [FIX] Users never show status when start app without network

* [FIX] Subscribe to room

* [FIX] Multiple servers

* [CHORE] Update SDK

* [FIX] Don't duplicate streams on subscribeAll

* [FIX] Server version when start the app offline

* [FIX] Server version cached

* [CHORE] Remove unnecessary code

* [FIX] Offline server version

* [FIX] Subscribe before connect

* [FIX] Remove unncessary props

* [FIX] Update sdk

* [FIX] User status & Unsubscribe Typing

* [FIX] Typing at incorrect room

* [FIX] Multiple Servers

* [CHORE] Update SDK

* [REVERT] Undo some changes on SDK

* [CHORE] Update sdk to prevent incorrect subscribes

* [FIX] Prevent no reconnect

* [FIX] Remove close on open

* [FIX] Clear typing when disconnect/connect to SDK

* [CHORE] Update SDK

* [CHORE] Update SDK

* Update SDK

* fix merge develop

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Single message thread inserting thread without rid (#1999)

* [FIX] ThreadMessagesView crashing on load (#1997)

* [FIX] Saml (#1996)

* [FIX] SAML incorrect close

* [FIX] Pathname

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Change user own status (#1995)

* [FIX] Change user own status

* [IMPROVEMENT] Set activeUsers

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Loading all updated rooms after app resume (#1998)

* [FIX] Loading all updated rooms after app resume

* Fix room date on RoomItem

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Change notifications preferences (#2000)

* [FIX] Change notifications preferences

* [IMPROVEMENT] Picker View

* [I18N] Translations

* [FIX] Picker Selection

* [FIX] List border

* [FIX] Prevent crash

* [FIX] Not-Pref tablet

* [FIX] Use same style of LanguageView

* [IMPROVEMENT] Send listItem title

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump version to 4.6.1 (#2001)

* [FIX] DM header blink (#2011)

* [FIX] Split get settings into two requests (#2017)

* [FIX] Split get settings into two requests

* [FIX] Clear settings only when change server

* [IMPROVEMENT] Move the way to clear settings

* [REVERT] Revert some changes

* [FIX] Server Icon

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [REGRESSION] Invite Links (#2007)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Read only channel/broadcast (#1951)

* [FIX] Read only channel/broadcast

* [FIX] Roles missing

* [FIX] Check roles to readOnly

* [FIX] Can post

* [FIX] Respect post-readonly permission

* [FIX] Search a room readOnly

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Cas auth (#2024)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Login TOTP Compatibility to older servers (#2018)

* [FIX] Login TOTP Compatibility to older servers

* [FIX] Android crashes if use double negation

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump version to 4.6.4 (#2029)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Lint (#2030)

* [FIX] UIKit with only one block (#2022)

* [FIX] Message with only one block

* [FIX] Update headers

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Bump version to 4.7.0 (#2035)

* [FIX] Action Tint Color on Black theme (#2081)

* [FIX] Prevent crash when thread is not found (#2080)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Prevent double click (#2079)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Show slash commands when disconnected (#2078)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Backhandler onboarding (#2077)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Respect UI_Allow_room_names_with_special_chars setting (#2076)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] RoomsList update sometimes isn't fired (#2071)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Stop inserting last message as message object from rooms stream if room is focused (#2069)

* [IMPROVEMENT] No insert last message if the room is focused

* fix discussion/threads

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Hide system messages (#2067)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Pending update (#2066)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Prevent crash when room.uids was not inserted yet (#2055)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FEATURE] Save video (#2063)

* added-feature-save-video

* fix sha256

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Send totp-code to meteor call (#2050)

* fixed-issue

* removed-variable-name-errors

* reverted-last-commit

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] MessageBox mention shouldn't show group DMs (#2049)

* fixed-issue

* [FIX] Filter users only if it's not a group chat

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] AttachmentView (Android)(Tablet) (#2047)

* [fix]Tablet attachment View and Room Navigation

* fix weird navigation and margin bottom

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Allow special chars in Filename (#2020)

* fixed-filename-issue

* improve

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Recorded audio on Android doesn't play on iOS (#2073)

* react-native-video -> expo-av

* remove react-native-video

* Add audio mode

* update mocks

* [FIX] Loading bigger than play/pause

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Message Touchable (#2082)

* [FIX] Avatar touchable

* [IMPROVEMENT] onLongPress on all Message Touchables

* [IMPROVEMENT] User & baseUrl on MessageContext

* [FIX] Context Access

* [FIX] BaseURL

* Fix User

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] ReactionsModal (#2085)

* [NEW] Delete Server (#1975)

* [NEW] Delete server

Co-authored-by: Bruno Dantas <oliveiradantas96@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>

* [FIX] Revert removed function

Co-authored-by: Bruno Dantas <oliveiradantas96@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>

* pods

* i18n

* Revert "pods"

This reverts commit 2854a1650538159aeeafe90fdb2118d12b76a82f.

Co-authored-by: Bruno Dantas <oliveiradantas96@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Change server while connecting/updating (#1981)

* [IMPROVEMENT] Change server while connecting

* [FIX] Not login/reconnect to previous server

* [FIX] Abort all fetch while connecting

* [FIX] Abort sdk fetch

* [FIX] Patch-package

* Add comments

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Keep screen awake while recording/playing some audio (#2089)

* [IMPROVEMENT] Keep screen awake while recording/playing some audio

* [FIX] Add expo-keep-awake mock

* [FIX] UIKit crashing when UIKitModal receive update event (#2088)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Close announcement banner (#2064)

* [NEW] Created new field in subscription table

Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com>

* [NEW] New field added to obeserver in room view

Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com>

* [NEW] Added icon and new design to banner

Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com>

* [NEW] Close banner function works

Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com>

* [IMPROVEMENT] closed banner status now update correctly

Signed-off-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com>

* improve banner style

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Update all dependencies (#2008)

* Android RN 62

* First steps iOS

* Second step iOS

* iOS compiling

* "New" build system

* Finish iOS

* Flipper

* Update to RN 0.62.1

* expo libs

* Hermes working

* Fix lint

* Fix android build

* Patches

* Dev patches

* Patch WatermelonDB: https://github.com/Nozbe/WatermelonDB/pull/660

* Fix jitsi

* Update several minors

* Update dev minors and lint

* react-native-keyboard-input

* Few updates

* device info

* react-native-fast-image

* Navigation bar color

* react-native-picker-select

* webview

* reactotron-react-native

* Watermelondb

* RN 0.62.2

* Few updates

* Fix selection

* update gems

* remove lib

* finishing

* tests

* Use node 10

* Re-enable app bundle

* iOS build

* Update jitsi ios

* [NEW] Passcode and biometric unlock (#2059)

* Update expo libs

* Configure expo-local-authentication

* ScreenLockedView

* Authenticate server change

* Auth on app resume

* localAuthentication util

* Add servers.lastLocalAuthenticatedSession column

* Save last session date on background

* Use our own version of app state redux

* Fix libs

* Remove inactive

* ScreenLockConfigView

* Apply on saved data

* Auto lock option label

* Starting passcode

* Basic passcode flow working

* Change passcode

* Check if biometry is enrolled

* Use fork

* Migration

* Patch expo-local-authentication

* Use async storage

* Styling

* Timer

* Refactor

* Lock orientation portrait when not on tablet

* share extension

* Deep linking

* Share extension

* Refactoring passcode

* use state

* Stash

* Refactor

* Change passcode

* Animate dots on error

* Matching passcodes

* Shake

* Remove lib

* Delete button

* Fade animation on modal

* Refactoring

* ItemInfo

* I18n

* I18n

* Remove unnecessary prop

* Save biometry column

* Raise time to lock to 30 seconds

* Vibrate on wrong confirmation passcode

* Reset attempts and save last authentication on local passcode confirmation

* Remove inline style

* Save last auth

* Fix header blink

* Change function name

* Fix android modal

* Fix vibration permission

* PasscodeEnter calls biometry

* Passcode on the state

* Biometry button on PasscodeEnter

* Show whole passcode

* Secure passcode

* Save passcode with promise to prevent empty passcodes and immediately lock

* Patch expo-local-authentication

* I18n

* Fix biometry being called every time

* Blur screen on app inactive

* Revert "Blur screen on app inactive"

This reverts commit a4ce812934adcf6cf87eb1a92aec9283e2f26753.

* Remove immediately because of how Activities work on Android

* Pods

* New layout

* stash

* Layout refactored

* Fix icons

* Force set passcode from server

* Lint

* Improve permission message

* Forced passcode subtitle

* Disable based on admin's choice

* Require local authentication on login success

* Refactor

* Update tests

* Update react-native-device-info to fix notch

* Lint

* Fix modal

* Fix icons

* Fix min auto lock time

* Review

* keep enabled on mobile

* fix forced by admin when enable unlock with passcode

* use DEFAULT_AUTO_LOCK when manual enable screenLock

* fix check has passcode

* request biometry on first password

* reset auto time lock when disabled on server

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>

* [FIX] Messages View (#2090)

* [FIX] Messages View

* [FIX] Opening PDF from Files View

* [FIX] Audio

* [FIX] SearchMessagesView

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Big names overflow (#2072)

* [FIX] Big names overflow

* [FIX] Message time

Co-authored-by: devyaniChoubey <devyanichoubey16@gmail.com>

* [FIX] Some alignments

* fix user item overflow

* some adjustments

Co-authored-by: devyaniChoubey <devyanichoubey16@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Avatar of message as an emoji (#2038)

* fixed-issue

* removed-hardcoded-emoji

* Merge develop

* replaced markdown with emoji componenent

* made-changes

* use avatar onPress

Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [NEW] Livechat (#2004)

* [WIP][NEW] Livechat info/actions

* [IMPROVEMENT] RoomActionsView

* [NEW] Visitor Navigation

* [NEW] Get Department REST

* [FIX] Borders

* [IMPROVEMENT] Refactor RoomInfo View

* [FIX] Error while navigate from mention -> roomInfo

* [NEW] Livechat Fields

* [NEW] Close Livechat

* [WIP] Forward livechat

* [NEW] Return inquiry

* [WIP] Comment when close livechat

* [WIP] Improve roomInfo

* [IMPROVEMENT] Forward room

* [FIX] Department picker

* [FIX] Picker without results

* [FIX] Superfluous argument

* [FIX] Check permissions on RoomActionsView

* [FIX] Livechat permissions

* [WIP] Show edit to livechat

* [I18N] Add pt-br translations

* [WIP] Livechat Info

* [IMPROVEMENT] Livechat info

* [WIP] Livechat Edit

* [WIP] Livechat edit

* [WIP] Livechat Edit

* [WIP] Livechat edit scroll

* [FIX] Edit customFields

* [FIX] Clean livechat customField

* [FIX] Visitor Navigation

* [NEW] Next input logic LivechatEdit

* [FIX] Add livechat data to subscription

* [FIX] Revert change

* [NEW] Livechat user Status

* [WIP] Livechat tags

* [NEW] Edit livechat tags

* [FIX] Prevent some crashes

* [FIX] Forward

* [FIX] Return Livechat error

* [FIX] Prevent livechat info crash

* [IMPROVEMENT] Use input style on forward chat

* OnboardingSeparator -> OrSeparator

* [FIX] Go to next input

* [NEW] Added some icons

* [NEW] Livechat close

* [NEW] Forward Room Action

* [FIX] Livechat edit style

* [FIX] Change status logic

* [CHORE] Remove unnecessary logic

* [CHORE] Remove unnecessary code

* [CHORE] Remove unecessary case

* [FIX] Superfluous argument

* [IMPROVEMENT] Submit livechat edit

* [CHORE] Remove textInput type

* [FIX] Livechat edit

* [FIX] Livechat Edit

* [FIX] Use same effect

* [IMPROVEMENT] Tags input

* [FIX] Add empty tag

* Fix minor issues

* Fix typo

* insert livechat room data to our room object

* review

* add method calls server version

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Delete Subs (#2091)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Android build (#2094)

* [FIX] Blink header DM (#2093)

* [FIX] Blink header DM

* Remove query

* [FIX] Push RoomInfoView

* remove unnecessary try/catch

* [FIX] RoomInfo > Message (Tablet)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Default biometry enabled (#2095)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [IMPROVEMENT] Enable navigating to a room from auth deep linking (#2115)

* Wait for login success to navigate

* Enable auth and room deep linking at the same time

* [FIX] NewMessageView Press Item should open DM (#2116)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Roles throwing error (#2110)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Wait attach activity before changeNavigationBarColor (#2111)

* [FIX] Wait attach activity before changeNavigationBarColor

* Remove timeout and add try/catch

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] UIKit crash when some app send a list (#2117)

* [FIX] StoryBook

* [FIX] UIKit crash when some app send a list

* [CHORE] Update snapshot

* [CHORE] Remove token & id

* [FIX] Change bar color while no activity attached (#2130)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Screen Lock options i18n (#2120)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [i18n] Added missing German translation strings (#2105)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Sometimes SDK is null when try to connect (#2131)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [FIX] Autocomplete position on Android (#2106)

* [FIX] Autocomplete position on Android

* [FIX] Set selection to 0 when needed

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* Revert "[FIX] Autocomplete position on Android (#2106)" (#2136)

This reverts commit e8c38d6f6f.

* [FIX] Here and all mentions shouldn't refer to users (#2137)

* [FIX] No send data to bugsnag if it's an aborted request (#2133)

Co-authored-by: Diego Mello <diegolmello@gmail.com>

* [TESTS] Update and separate E2E tests (#2126)

* Tests passing until roomslist

* create room

* roominfo

* change server

* broadcast

* profile

* custom status

* forgot password

* working

* room and onboarding

* Tests separated

* config.yml refactor

* Revert "config.yml refactor"

This reverts commit 0e984d3029.

* CI

* lint

* CI refactor

* Onboarding tests

* npx detox

* Add all tests

* Save brew cache

* mac-env executor

* detox-test command

* Update readme

* Remove folder

* [FIX] Screen Lock Time respect local value (#2141)

* [FIX] Screen Lock Time respect local value

* [FIX] Enable biometry at the first passcode change

Co-authored-by: phriedrich <info@phriedrich.de>
Co-authored-by: Guilherme Siqueira <guilhersiqueira@gmail.com>
Co-authored-by: Prateek Jain <44807945+Prateek93a@users.noreply.github.com>
Co-authored-by: Djorkaeff Alexandre <djorkaeff.unb@gmail.com>
Co-authored-by: Prateek Jain <prateek93a@gmail.com>
Co-authored-by: devyaniChoubey <52153085+devyaniChoubey@users.noreply.github.com>
Co-authored-by: Bernard Seow <ssbing99@gmail.com>
Co-authored-by: Hiroki Ishiura <ishiura@ja2.so-net.ne.jp>
Co-authored-by: Exordian <jakob.englisch@gmail.com>
Co-authored-by: Daanchaam <daanhendriks97@gmail.com>
Co-authored-by: Youssef Muhamad <emaildeyoussefmuhamad@gmail.com>
Co-authored-by: Iván Álvarez <ialvarezpereira@gmail.com>
Co-authored-by: Sarthak Pranesh <41206172+sarthakpranesh@users.noreply.github.com>
Co-authored-by: Michele Pellegrini <pellettiero@users.noreply.github.com>
Co-authored-by: Tanmoy Bhowmik <tanmoy.openroot@gmail.com>
Co-authored-by: Hibikine Kage <14365761+hibikine@users.noreply.github.com>
Co-authored-by: Ezequiel de Oliveira <ezequiel1de1oliveira@gmail.com>
Co-authored-by: Neil Agarwal <neil@neilagarwal.me>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Govind Dixit <GOVINDDIXIT93@GMAIL.COM>
Co-authored-by: Zhaubassarova Aruzhan <49000079+azhaubassar@users.noreply.github.com>
Co-authored-by: Aroo <azhaubassar@gmail.com>
Co-authored-by: Sarthak Pranesh <sarthak.pranesh2018@vitstudent.ac.in>
Co-authored-by: Siddharth Padhi <padhisiddharth31@gmail.com>
Co-authored-by: Bruno Dantas <oliveiradantas96@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>
Co-authored-by: devyaniChoubey <devyanichoubey16@gmail.com>
This commit is contained in:
Diego Mello 2020-05-25 17:54:27 -03:00 committed by GitHub
parent 933362eca1
commit 370128f566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3753 changed files with 366107 additions and 71532 deletions

View File

@ -1,12 +1,132 @@
defaults: &defaults
working_directory: ~/repo
version: 2
macos: &macos
macos:
xcode: "11.2.1"
bash-env: &bash-env
BASH_ENV: "~/.nvm/nvm.sh"
install-npm-modules: &install-npm-modules
name: Install NPM modules
command: yarn
restore-npm-cache-linux: &restore-npm-cache-linux
name: Restore NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
save-npm-cache-linux: &save-npm-cache-linux
key: node-modules-{{ checksum "yarn.lock" }}
name: Save NPM cache
paths:
- ./node_modules
restore-npm-cache-mac: &restore-npm-cache-mac
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
save-npm-cache-mac: &save-npm-cache-mac
key: node-v1-mac-{{ checksum "yarn.lock" }}
name: Save NPM cache
paths:
- ./node_modules
install-node: &install-node
name: Install Node 10
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 10
echo 'export PATH="/home/circleci/.nvm/versions/node/v10.20.1/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
restore-gems-cache: &restore-gems-cache
name: Restore gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
save-gems-cache: &save-gems-cache
name: Save gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
paths:
- vendor/bundle
update-fastlane: &update-fastlane
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
bundle install
working_directory: ios
restore-brew-cache: &restore-brew-cache
name: Restore Brew cache
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
save-brew-cache: &save-brew-cache
name: Save brew cache
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
paths:
- /usr/local/Homebrew
install-apple-sim-utils: &install-apple-sim-utils
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
rebuild-detox: &rebuild-detox
name: Rebuild Detox framework cache
command: |
npx detox clean-framework-cache
npx detox build-framework-cache
version: 2.1
# EXECUTORS
executors:
mac-env:
<<: *macos
environment:
<<: *bash-env
# COMMANDS
commands:
detox-test:
parameters:
folder:
type: string
steps:
- checkout
- attach_workspace:
at: .
- restore_cache: *restore-npm-cache-mac
- restore_cache: *restore-brew-cache
- run: *install-node
- run: *install-apple-sim-utils
- run: *install-npm-modules
- run: *rebuild-detox
- run:
name: Test
command: |
npx detox test << parameters.folder >> --configuration ios.sim.release --cleanup
# JOBS
jobs:
lint-testunit:
<<: *defaults
docker:
- image: circleci/node:8
- image: circleci/node:10
environment:
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
@ -14,14 +134,9 @@ jobs:
steps:
- checkout
- restore_cache:
name: Restore NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-linux
- run:
name: Install NPM modules
command: |
yarn
- run: *install-npm-modules
- run:
name: Lint
@ -38,162 +153,79 @@ jobs:
command: |
yarn codecov
- save_cache:
key: node-modules-{{ checksum "yarn.lock" }}
name: Save NPM cache
paths:
- ./node_modules
- save_cache: *save-npm-cache-linux
# E2E
e2e-build:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
executor: mac-env
steps:
- checkout
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-mac
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- restore_cache: *restore-brew-cache
- run:
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
- run: *install-node
- run:
name: Install NPM modules
command: |
yarn global add detox-cli
yarn
- run: *install-apple-sim-utils
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run: *install-npm-modules
- run: *rebuild-detox
- run:
name: Build
command: |
detox build --configuration ios.sim.release
npx detox build --configuration ios.sim.release
- persist_to_workspace:
root: .
paths:
- ios/build/Build/Products/Release-iphonesimulator/RocketChatRN.app
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
- save_cache: *save-npm-cache-mac
e2e-test:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
- save_cache: *save-brew-cache
e2e-test-onboarding:
executor: mac-env
steps:
- checkout
- detox-test:
folder: "./e2e/tests/onboarding"
- attach_workspace:
at: .
e2e-test-room:
executor: mac-env
steps:
- detox-test:
folder: "./e2e/tests/room"
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run:
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
- run:
name: Install NPM modules
command: |
yarn global add detox-cli
yarn
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run:
name: Test
command: |
detox test --configuration ios.sim.release --cleanup
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
e2e-test-assorted:
executor: mac-env
steps:
- detox-test:
folder: "./e2e/tests/assorted"
# Android builds
android-build:
<<: *defaults
docker:
- image: circleci/android:api-28-node
environment:
# GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
# GRADLE_OPTS: -Xmx2048m -Dorg.gradle.daemon=false
# JVM_OPTS: -Xmx4096m
JAVA_OPTS: '-Xms512m -Xmx2g'
GRADLE_OPTS: '-Xmx3g -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx2g -XX:+HeapDumpOnOutOfMemoryError"'
TERM: dumb
BASH_ENV: "~/.nvm/nvm.sh"
<<: *bash-env
steps:
- checkout
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
echo 'export PATH="/home/circleci/.nvm/versions/node/v8.16.0/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile
- run: *install-node
- restore_cache:
name: Restore NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-linux
- run:
name: Install NPM modules
command: |
yarn
- run: *install-npm-modules
- restore_cache:
name: Restore gradle cache
@ -206,6 +238,7 @@ jobs:
# echo -e "android.enableAapt2=false" >> ./gradle.properties
echo -e "android.useAndroidX=true" >> ./gradle.properties
echo -e "android.enableJetifier=true" >> ./gradle.properties
echo -e "FLIPPER_VERSION=0.33.1" >> ./gradle.properties
if [[ $KEYSTORE ]]; then
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
@ -234,8 +267,7 @@ jobs:
name: Build Android App
command: |
if [[ $KEYSTORE ]]; then
# TODO: enable app bundle again
./gradlew assembleRelease
./gradlew bundleRelease
else
./gradlew assembleDebug
fi
@ -261,11 +293,7 @@ jobs:
- store_artifacts:
path: /tmp/build/outputs
- save_cache:
name: Save NPM cache
key: node-modules-{{ checksum "yarn.lock" }}
paths:
- ./node_modules
- save_cache: *save-npm-cache-linux
- save_cache:
name: Save gradle cache
@ -273,44 +301,22 @@ jobs:
paths:
- ~/.gradle
# iOS builds
ios-build:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
executor: mac-env
steps:
- checkout
- restore_cache:
name: Restore gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
- restore_cache: *restore-gems-cache
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- restore_cache: *restore-npm-cache-mac
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run: *install-node
- run:
name: Install NPM modules
command: |
yarn
- run: *install-npm-modules
- run:
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
bundle install
working_directory: ios
- run: *update-fastlane
- run:
name: Set Google Services
@ -348,17 +354,9 @@ jobs:
fi
working_directory: ios
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
- save_cache: *save-npm-cache-mac
- save_cache:
name: Save gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
paths:
- vendor/bundle
- save_cache: *save-gems-cache
- store_artifacts:
path: ios/RocketChatRN.ipa
@ -370,8 +368,7 @@ jobs:
- ios/fastlane/report.xml
ios-testflight:
macos:
xcode: "11.2.1"
executor: mac-env
steps:
- checkout
@ -379,16 +376,9 @@ jobs:
- attach_workspace:
at: ios
- restore_cache:
name: Restore gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
- restore_cache: *restore-gems-cache
- run:
name: Update Fastlane
command: |
echo "ruby-2.4" > ~/.ruby-version
bundle install
working_directory: ios
- run: *update-fastlane
- run:
name: Fastlane Tesflight Upload
@ -396,14 +386,9 @@ jobs:
bundle exec fastlane ios beta
working_directory: ios
- save_cache:
name: Save gems cache
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}
paths:
- vendor/bundle
- save_cache: *save-gems-cache
workflows:
version: 2
build-and-test:
jobs:
- lint-testunit
@ -415,7 +400,13 @@ workflows:
- e2e-build:
requires:
- e2e-hold
- e2e-test:
- e2e-test-onboarding:
requires:
- e2e-build
- e2e-test-room:
requires:
- e2e-build
- e2e-test-assorted:
requires:
- e2e-build

View File

@ -87,6 +87,7 @@ module.exports = {
"no-regex-spaces": 2,
"no-undef": 2,
"no-unreachable": 2,
"no-unused-expressions": 0,
"no-unused-vars": [2, {
"vars": "all",
"args": "after-used"
@ -131,7 +132,23 @@ module.exports = {
"react-native/no-unused-styles": 2,
"react/jsx-one-expression-per-line": 0,
"require-await": 2,
"func-names": 0
"func-names": 0,
"react/sort-comp": ["error", {
"order": [
"static-variables",
"static-methods",
"lifecycle",
"everything-else",
"render"
]
}],
"react/static-property-placement": [0],
"arrow-parens": ["error", "as-needed", { requireForBlockBody: true }],
"react/jsx-props-no-spreading": [1],
"react/jsx-curly-newline": [0],
"react/state-in-constructor": [0],
"no-async-promise-executor": [0],
"max-classes-per-file": [0]
},
"globals": {
"__DEV__": true

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ coverage/
buck-out/
\.buckd/
*.keystore
*.jks
# fastlane
#

View File

@ -208,13 +208,15 @@ Readme will guide you on how to config.
- Build your app
```bash
$ detox build --configuration ios.sim.release
$ npx detox build --configuration ios.sim.release
```
- Run tests
```bash
$ detox test --configuration ios.sim.release
$ npx detox test ./e2e/tests/onboarding --configuration ios.sim.release
$ npx detox test ./e2e/tests/room --configuration ios.sim.release
$ npx detox test ./e2e/tests/assorted --configuration ios.sim.release
```
## Storybook

14
__mocks__/expo-av.js Normal file
View File

@ -0,0 +1,14 @@
export class Sound {
loadAsync = () => {};
playAsync = () => {};
pauseAsync = () => {};
stopAsync = () => {};
setOnPlaybackStatusUpdate = () => {};
setPositionAsync = () => {};
}
export const Audio = { Sound };

View File

@ -0,0 +1,4 @@
export default {
activateKeepAwake: () => '',
deactivateKeepAwake: () => ''
};

View File

@ -2,5 +2,6 @@ export default {
getModel: () => '',
getReadableVersion: () => '',
getBundleId: () => '',
isTablet: () => false
isTablet: () => false,
hasNotch: () => false
};

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,9 @@ import com.android.build.OutputFile
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
@ -80,7 +82,6 @@ import com.android.build.OutputFile
*/
project.ext.react = [
entryFile: "index.js",
bundleAssetName: "app.bundle",
iconFontNames: [ 'custom.ttf' ],
enableHermes: true, // clean and rebuild if changing
@ -138,9 +139,10 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.6.4"
versionName "4.7.0"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
}
signingConfigs {
@ -168,6 +170,14 @@ android {
signingConfig signingConfigs.release
}
}
packagingOptions {
pickFirst '**/armeabi-v7a/libc++_shared.so'
pickFirst '**/x86/libc++_shared.so'
pickFirst '**/arm64-v8a/libc++_shared.so'
pickFirst '**/x86_64/libc++_shared.so'
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
@ -202,6 +212,7 @@ dependencies {
implementation project(":reactnativekeyboardinput")
implementation project(':@react-native-community_viewpager')
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "com.google.firebase:firebase-messaging:18.0.0"
implementation "com.google.firebase:firebase-core:16.0.9"
@ -209,6 +220,16 @@ dependencies {
implementation('com.crashlytics.sdk.android:crashlytics:2.9.9@aar') {
transitive = true
}
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.rndiffapp;
import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
import okhttp3.OkHttpClient;
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
if (FlipperUtils.shouldEnableFlipper(context)) {
final FlipperClient client = AndroidFlipperClient.getInstance(context);
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
client.addPlugin(new ReactFlipperPlugin());
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(CrashReporterPlugin.getInstance());
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
NetworkingModule.setCustomClientBuilder(
new NetworkingModule.CustomClientBuilder() {
@Override
public void apply(OkHttpClient.Builder builder) {
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
}
});
client.addPlugin(networkFlipperPlugin);
client.start();
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
// Hence we run if after all native modules have been initialized
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext == null) {
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext reactContext) {
reactInstanceManager.removeReactInstanceEventListener(this);
reactContext.runOnNativeModulesQueueThread(
new Runnable() {
@Override
public void run() {
client.addPlugin(new FrescoFlipperPlugin());
}
});
}
});
} else {
client.addPlugin(new FrescoFlipperPlugin());
}
}
}
}

View File

@ -3,13 +3,13 @@
package="chat.rocket.reactnative">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
<!-- <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> -->
<application
android:name=".MainApplication"
@ -30,7 +30,7 @@
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">

View File

@ -9,9 +9,11 @@ import com.facebook.react.PackageList;
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import chat.rocket.reactnative.generated.BasePackageList;
@ -39,7 +41,7 @@ import java.util.List;
public class MainApplication extends Application implements ReactApplication, INotificationsApplication {
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(new BasePackageList().getPackageList(), Arrays.<SingletonModule>asList());
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(new BasePackageList().getPackageList(), null);
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
@ -58,7 +60,11 @@ public class MainApplication extends Application implements ReactApplication, IN
packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new WatermelonDBPackage());
packages.add(new RNCViewPagerPackage());
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
// packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
List<ReactPackage> unimodules = Arrays.<ReactPackage>asList(
new ModuleRegistryAdapter(mModuleRegistryProvider)
);
packages.addAll(unimodules);
return packages;
}
@ -82,6 +88,38 @@ public class MainApplication extends Application implements ReactApplication, IN
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*
* @param context
* @param reactInstanceManager
*/
private static void initializeFlipper(
Context context, ReactInstanceManager reactInstanceManager) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("chat.rocket.reactnative");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
@Override

View File

@ -11,6 +11,9 @@ public class BasePackageList {
new expo.modules.constants.ConstantsPackage(),
new expo.modules.filesystem.FileSystemPackage(),
new expo.modules.haptics.HapticsPackage(),
new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.localauthentication.LocalAuthenticationPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);

View File

@ -18,7 +18,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.firebase:perf-plugin:1.2.1'
@ -42,16 +42,14 @@ allprojects {
url("$rootDir/../node_modules/jsc-android/dist")
}
maven {
// We should change it when Jitsi-SDK release v2.4
url("$rootDir/../node_modules/react-native-jitsi-meet/jitsi-sdk")
// url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
}
google()
jcenter()
maven { url 'https://maven.google.com' }
maven { url "https://jitpack.io" }
maven { url 'https://www.jitpack.io' }
}
}
@ -64,6 +62,12 @@ subprojects { subproject ->
defaultConfig {
targetSdkVersion 28
}
variantFilter { variant ->
def names = variant.flavors*.name
if (names.contains("reactNative59")) {
setIgnore(true)
}
}
}
}
}

View File

@ -19,7 +19,16 @@
# android.enableAapt2=false # commenting this makes notifications to stop working
# android.useDeprecatedNdk=true
org.gradle.jvmargs=-Xmx2048M -XX\:MaxHeapSize\=32g
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.33.1
# App properties
VERSIONCODE=999999999
BugsnagAPIKey=""

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

8
android/gradlew vendored
View File

@ -7,7 +7,7 @@
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
@ -44,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=''
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@ -125,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

View File

@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER'
]);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'DELETE', 'REMOVED', 'USER_TYPING']);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
@ -64,3 +64,4 @@ export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [
...defaultTypes
]);
export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);

View File

@ -1,5 +1,19 @@
import * as types from './actionsTypes';
export function subscribeRoom(rid) {
return {
type: types.ROOM.SUBSCRIBE,
rid
};
}
export function unsubscribeRoom(rid) {
return {
type: types.ROOM.UNSUBSCRIBE,
rid
};
}
export function leaveRoom(rid, t) {
return {
type: types.ROOM.LEAVE,
@ -16,6 +30,21 @@ export function deleteRoom(rid, t) {
};
}
export function closeRoom(rid) {
return {
type: types.ROOM.CLOSE,
rid
};
}
export function forwardRoom(rid, transferData) {
return {
type: types.ROOM.FORWARD,
transferData,
rid
};
}
export function removedRoom() {
return {
type: types.ROOM.REMOVED

View File

@ -46,7 +46,14 @@ export const themes = {
messageboxBackground: '#ffffff',
searchboxBackground: '#E6E6E7',
buttonBackground: '#414852',
buttonText: '#ffffff'
buttonText: '#ffffff',
passcodeBackground: '#EEEFF1',
passcodeButtonActive: '#E4E7EA',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#2F343D',
passcodeSecondary: '#6C727A',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
},
dark: {
backgroundColor: '#030b1b',
@ -81,7 +88,14 @@ export const themes = {
messageboxBackground: '#0b182c',
searchboxBackground: '#192d4d',
buttonBackground: '#414852',
buttonText: '#ffffff'
buttonText: '#ffffff',
passcodeBackground: '#030C1B',
passcodeButtonActive: '#0B182C',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
},
black: {
backgroundColor: '#000000',
@ -100,7 +114,7 @@ export const themes = {
infoText: '#6d6d72',
tintColor: '#1e9bfe',
auxiliaryTintColor: '#cdcdcd',
actionTintColor: '#1ea1fe',
actionTintColor: '#1e9bfe',
separatorColor: '#272728',
navbarBackground: '#0d0d0d',
headerBorder: '#323232',
@ -116,6 +130,13 @@ export const themes = {
messageboxBackground: '#0d0d0d',
searchboxBackground: '#1f1f1f',
buttonBackground: '#414852',
buttonText: '#ffffff'
buttonText: '#ffffff',
passcodeBackground: '#000000',
passcodeButtonActive: '#0E0D0D',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1',
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A'
}
};

View File

@ -0,0 +1,12 @@
export const PASSCODE_KEY = 'kPasscode';
export const LOCKED_OUT_TIMER_KEY = 'kLockedOutTimer';
export const ATTEMPTS_KEY = 'kAttempts';
export const LOCAL_AUTHENTICATE_EMITTER = 'LOCAL_AUTHENTICATE';
export const CHANGE_PASSCODE_EMITTER = 'CHANGE_PASSCODE';
export const PASSCODE_LENGTH = 6;
export const MAX_ATTEMPTS = 6;
export const TIME_TO_LOCK = 30000;
export const DEFAULT_AUTO_LOCK = 1800;

View File

@ -68,6 +68,9 @@ export default {
LDAP_Enable: {
type: 'valueAsBoolean'
},
Livechat_request_comment_when_closing_conversation: {
type: 'valueAsBoolean'
},
Jitsi_Enabled: {
type: 'valueAsBoolean'
},
@ -125,6 +128,9 @@ export default {
uniqueID: {
type: 'valueAsString'
},
UI_Allow_room_names_with_special_chars: {
type: 'valueAsBoolean'
},
UI_Use_Real_Name: {
type: 'valueAsBoolean'
},
@ -157,5 +163,11 @@ export default {
},
CAS_login_url: {
type: 'valueAsString'
},
Force_Screen_Lock: {
type: 'valueAsBoolean'
},
Force_Screen_Lock_After: {
type: 'valueAsNumber'
}
};

View File

@ -2,12 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import Touch from '../utils/touch';
import { avatarURL } from '../utils/avatar';
import Emoji from './markdown/Emoji';
const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme, emoji, getCustomEmoji
}) => {
const avatarStyle = {
width: size,
@ -23,7 +25,15 @@ const Avatar = React.memo(({
type, text, size, userId, token, avatar, baseUrl
});
let image = (
let image = emoji ? (
<Emoji
theme={theme}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
/>
) : (
<FastImage
style={avatarStyle}
source={{
@ -36,9 +46,9 @@ const Avatar = React.memo(({
if (onPress) {
image = (
<Touch onPress={onPress} theme={theme}>
<Touchable onPress={onPress}>
{image}
</Touch>
</Touchable>
);
}
@ -55,6 +65,7 @@ Avatar.propTypes = {
style: PropTypes.any,
text: PropTypes.string,
avatar: PropTypes.string,
emoji: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
@ -62,7 +73,8 @@ Avatar.propTypes = {
userId: PropTypes.string,
token: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func
onPress: PropTypes.func,
getCustomEmoji: PropTypes.func
};
Avatar.defaultProps = {

View File

@ -23,7 +23,7 @@ export const FormContainerInner = ({ children }) => (
</View>
);
const FormContainer = ({ children, theme }) => (
const FormContainer = ({ children, theme, testID }) => (
<KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container}
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme }) => (
>
<StatusBar theme={theme} />
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }}>
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }} testID={testID}>
{children}
<AppVersion theme={theme} />
</SafeAreaView>
@ -41,6 +41,7 @@ const FormContainer = ({ children, theme }) => (
FormContainer.propTypes = {
theme: PropTypes.string,
testID: PropTypes.string,
children: PropTypes.element
};

View File

@ -0,0 +1,29 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
const styles = StyleSheet.create({
infoContainer: {
padding: 15
},
infoText: {
fontSize: 14,
...sharedStyles.textRegular
}
});
const ItemInfo = React.memo(({ info, theme }) => (
<View style={[styles.infoContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<Text style={[styles.infoText, { color: themes[theme].infoText }]}>{info}</Text>
</View>
));
ItemInfo.propTypes = {
info: PropTypes.string,
theme: PropTypes.string
};
export default ItemInfo;

View File

@ -12,7 +12,7 @@ import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
import { loginRequest as loginRequestAction } from '../actions/login';
import Button from './Button';
import OnboardingSeparator from './OnboardingSeparator';
import OrSeparator from './OrSeparator';
import Touch from '../utils/touch';
import I18n from '../i18n';
import random from '../utils/random';
@ -252,12 +252,12 @@ class LoginServices extends React.PureComponent {
style={styles.options}
color={themes[theme].actionTintColor}
/>
<OnboardingSeparator theme={theme} />
<OrSeparator theme={theme} />
</>
);
}
if (length > 0 && separator) {
return <OnboardingSeparator theme={theme} />;
return <OrSeparator theme={theme} />;
}
return null;
}

View File

@ -64,7 +64,7 @@ const MentionItem = ({
content = (
<>
<Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.command}</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
</>
);
}

View File

@ -5,6 +5,7 @@ import {
} from 'react-native';
import { AudioRecorder, AudioUtils } from 'react-native-audio';
import { BorderlessButton } from 'react-native-gesture-handler';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import RNFetchBlob from 'rn-fetch-blob';
import styles from './styles';
@ -59,7 +60,8 @@ export default class extends React.PureComponent {
SampleRate: 22050,
Channels: 1,
AudioQuality: 'Low',
AudioEncoding: 'aac'
AudioEncoding: 'aac',
OutputFormat: 'aac_adts'
});
AudioRecorder.onProgress = (data) => {
@ -74,12 +76,16 @@ export default class extends React.PureComponent {
}
};
AudioRecorder.startRecording();
activateKeepAwake();
}
componentWillUnmount() {
if (this.recording) {
this.cancelAudioMessage();
}
deactivateKeepAwake();
}
finishRecording = (didSucceed, filePath, size) => {

View File

@ -8,7 +8,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
onPress={onPress}
testID='messagebox-send-message'
accessibilityLabel='Send_message'
icon='send1'
icon='Send-active'
theme={theme}
/>
));

View File

@ -190,7 +190,7 @@ class MessageBox extends Component {
});
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
const { isFocused, editing, replying } = this.props;
if (!isFocused()) {
return;
@ -306,9 +306,9 @@ class MessageBox extends Component {
if (!isTextEmpty) {
try {
const { start, end } = this.component._lastNativeSelection;
const { start, end } = this.component?.lastNativeSelection;
const cursor = Math.max(start, end);
const lastNativeText = this.component._lastNativeText || '';
const lastNativeText = this.component?.lastNativeText || '';
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp);
@ -339,7 +339,7 @@ class MessageBox extends Component {
}
const { trackingType } = this.state;
const msg = this.text;
const { start, end } = this.component._lastNativeSelection;
const { start, end } = this.component?.lastNativeSelection;
const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im;
const result = msg.substr(0, cursor).replace(regexp, '');
@ -383,8 +383,8 @@ class MessageBox extends Component {
let newText = '';
// if messagebox has an active cursor
if (this.component && this.component._lastNativeSelection) {
const { start, end } = this.component._lastNativeSelection;
if (this.component?.lastNativeSelection) {
const { start, end } = this.component.lastNativeSelection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else {

View File

@ -24,7 +24,7 @@ const styles = StyleSheet.create({
}
});
const DateSeparator = React.memo(({ theme }) => {
const OrSeparator = React.memo(({ theme }) => {
const line = { backgroundColor: themes[theme].borderColor };
const text = { color: themes[theme].auxiliaryText };
return (
@ -36,8 +36,8 @@ const DateSeparator = React.memo(({ theme }) => {
);
});
DateSeparator.propTypes = {
OrSeparator.propTypes = {
theme: PropTypes.string
};
export default DateSeparator;
export default OrSeparator;

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons';
const Button = React.memo(({
text, disabled, theme, onPress, icon
}) => {
const press = () => onPress && onPress(text);
return (
<Touch
style={[styles.buttonView, { backgroundColor: 'transparent' }]}
underlayColor={themes[theme].passcodeButtonActive}
rippleColor={themes[theme].passcodeButtonActive}
enabled={!disabled}
theme={theme}
onPress={press}
>
{
icon
? (
<CustomIcon name={icon} size={36} color={themes[theme].passcodePrimary} />
)
: (
<Text style={[styles.buttonText, { color: themes[theme].passcodePrimary }]}>
{text}
</Text>
)
}
</Touch>
);
});
Button.propTypes = {
text: PropTypes.string,
icon: PropTypes.string,
theme: PropTypes.string,
disabled: PropTypes.bool,
onPress: PropTypes.func
};
export default Button;

View File

@ -0,0 +1,51 @@
import React from 'react';
import { View } from 'react-native';
import _ from 'lodash';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
const SIZE_EMPTY = 12;
const SIZE_FULL = 16;
const Dots = React.memo(({ passcode, theme, length }) => (
<View style={styles.dotsContainer}>
{_.range(length).map((val) => {
const lengthSup = (passcode.length >= val + 1);
const height = lengthSup ? SIZE_FULL : SIZE_EMPTY;
const width = lengthSup ? SIZE_FULL : SIZE_EMPTY;
let backgroundColor = '';
if (lengthSup && passcode.length > 0) {
backgroundColor = themes[theme].passcodeDotFull;
} else {
backgroundColor = themes[theme].passcodeDotEmpty;
}
const borderRadius = lengthSup ? SIZE_FULL / 2 : SIZE_EMPTY / 2;
const marginRight = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
const marginLeft = lengthSup ? 10 - (SIZE_FULL - SIZE_EMPTY) / 2 : 10;
return (
<View style={styles.dotsView}>
<View
style={{
height,
width,
borderRadius,
backgroundColor,
marginRight,
marginLeft
}}
/>
</View>
);
})}
</View>
));
Dots.propTypes = {
passcode: PropTypes.string,
theme: PropTypes.string,
length: PropTypes.string
};
export default Dots;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { View } from 'react-native';
import { Row } from 'react-native-easy-grid';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
const LockIcon = React.memo(({ theme }) => (
<Row style={styles.row}>
<View style={styles.iconView}>
<CustomIcon name='lock' size={40} color={themes[theme].passcodeLockIcon} />
</View>
</Row>
));
LockIcon.propTypes = {
theme: PropTypes.string
};
export default LockIcon;

View File

@ -0,0 +1,74 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Grid } from 'react-native-easy-grid';
import { themes } from '../../../constants/colors';
import { resetAttempts } from '../../../utils/localAuthentication';
import { TYPE } from '../constants';
import { getLockedUntil, getDiff } from '../utils';
import I18n from '../../../i18n';
import styles from './styles';
import Title from './Title';
import Subtitle from './Subtitle';
import LockIcon from './LockIcon';
const Timer = React.memo(({ time, theme, setStatus }) => {
const calcTimeLeft = () => {
const diff = getDiff(time);
if (diff > 0) {
return Math.floor((diff / 1000) % 60);
}
};
const [timeLeft, setTimeLeft] = useState(calcTimeLeft());
useEffect(() => {
setTimeout(() => {
setTimeLeft(calcTimeLeft());
if (timeLeft <= 1) {
resetAttempts();
setStatus(TYPE.ENTER);
}
}, 1000);
});
if (!timeLeft) {
return null;
}
return <Subtitle text={I18n.t('Passcode_app_locked_subtitle', { timeLeft })} theme={theme} />;
});
const Locked = React.memo(({ theme, setStatus }) => {
const [lockedUntil, setLockedUntil] = useState(null);
const readItemFromStorage = async() => {
const l = await getLockedUntil();
setLockedUntil(l);
};
useEffect(() => {
readItemFromStorage();
}, []);
return (
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]} r>
<LockIcon theme={theme} />
<Title text={I18n.t('Passcode_app_locked_title')} theme={theme} />
<Timer theme={theme} time={lockedUntil} setStatus={setStatus} />
</Grid>
);
});
Locked.propTypes = {
theme: PropTypes.string,
setStatus: PropTypes.func
};
Timer.propTypes = {
time: PropTypes.string,
theme: PropTypes.string,
setStatus: PropTypes.func
};
export default Locked;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { View, Text } from 'react-native';
import { Row } from 'react-native-easy-grid';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
const Subtitle = React.memo(({ text, theme }) => (
<Row style={styles.row}>
<View style={styles.subtitleView}>
<Text style={[styles.textSubtitle, { color: themes[theme].passcodeSecondary }]}>{text}</Text>
</View>
</Row>
));
Subtitle.propTypes = {
text: PropTypes.string,
theme: PropTypes.string
};
export default Subtitle;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { View, Text } from 'react-native';
import { Row } from 'react-native-easy-grid';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../../constants/colors';
const Title = React.memo(({ text, theme }) => (
<Row style={styles.row}>
<View style={styles.titleView}>
<Text style={[styles.textTitle, { color: themes[theme].passcodePrimary }]}>{text}</Text>
</View>
</Row>
));
Title.propTypes = {
text: PropTypes.string,
theme: PropTypes.string
};
export default Title;

View File

@ -0,0 +1,139 @@
import React, {
useState, forwardRef, useImperativeHandle, useRef
} from 'react';
import { Col, Row, Grid } from 'react-native-easy-grid';
import _ from 'lodash';
import PropTypes from 'prop-types';
import * as Animatable from 'react-native-animatable';
import * as Haptics from 'expo-haptics';
import styles from './styles';
import Button from './Button';
import Dots from './Dots';
import { TYPE } from '../constants';
import { themes } from '../../../constants/colors';
import { PASSCODE_LENGTH } from '../../../constants/localAuthentication';
import LockIcon from './LockIcon';
import Title from './Title';
import Subtitle from './Subtitle';
const Base = forwardRef(({
theme, type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress
}, ref) => {
const rootRef = useRef();
const dotsRef = useRef();
const [passcode, setPasscode] = useState('');
const clearPasscode = () => setPasscode('');
const wrongPasscode = () => {
clearPasscode();
dotsRef?.current?.shake(500);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
};
const animate = (animation, duration = 500) => {
rootRef?.current?.[animation](duration);
};
const onPressNumber = text => setPasscode((p) => {
const currentPasscode = p + text;
if (currentPasscode?.length === PASSCODE_LENGTH) {
switch (type) {
case TYPE.CHOOSE:
onEndProcess(currentPasscode);
break;
case TYPE.CONFIRM:
if (currentPasscode !== previousPasscode) {
onError();
} else {
onEndProcess(currentPasscode);
}
break;
case TYPE.ENTER:
onEndProcess(currentPasscode);
break;
default:
break;
}
}
return currentPasscode;
});
const onPressDelete = () => setPasscode((p) => {
if (p?.length > 0) {
const newPasscode = p.slice(0, -1);
return newPasscode;
}
return '';
});
useImperativeHandle(ref, () => ({
wrongPasscode, animate, clearPasscode
}));
return (
<Animatable.View ref={rootRef} style={styles.container}>
<Grid style={[styles.grid, { backgroundColor: themes[theme].passcodeBackground }]}>
<LockIcon theme={theme} />
<Title text={title} theme={theme} />
<Subtitle text={subtitle} theme={theme} />
<Row style={styles.row}>
<Animatable.View ref={dotsRef}>
<Dots passcode={passcode} theme={theme} length={PASSCODE_LENGTH} />
</Animatable.View>
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{_.range(1, 4).map(i => (
<Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} />
</Col>
))}
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{_.range(4, 7).map(i => (
<Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} />
</Col>
))}
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{_.range(7, 10).map(i => (
<Col key={i} style={styles.colButton}>
<Button text={i} theme={theme} onPress={onPressNumber} />
</Col>
))}
</Row>
<Row style={[styles.row, styles.buttonRow]}>
{showBiometry
? (
<Col style={styles.colButton}>
<Button icon='fingerprint' theme={theme} onPress={onBiometryPress} />
</Col>
)
: <Col style={styles.colButton} />}
<Col style={styles.colButton}>
<Button text='0' theme={theme} onPress={onPressNumber} />
</Col>
<Col style={styles.colButton}>
<Button icon='backspace' theme={theme} onPress={onPressDelete} />
</Col>
</Row>
</Grid>
</Animatable.View>
);
});
Base.propTypes = {
theme: PropTypes.string,
type: PropTypes.string,
previousPasscode: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
showBiometry: PropTypes.string,
onEndProcess: PropTypes.func,
onError: PropTypes.func,
onBiometryPress: PropTypes.func
};
export default Base;

View File

@ -0,0 +1,70 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../../views/Styles';
export default StyleSheet.create({
container: {
flex: 1
},
titleView: {
justifyContent: 'center'
},
subtitleView: {
justifyContent: 'center',
height: 32
},
row: {
flex: 0,
alignItems: 'center',
justifyContent: 'center'
},
buttonRow: {
height: 102
},
colButton: {
flex: 0,
marginLeft: 12,
marginRight: 12,
alignItems: 'center',
width: 78,
height: 78
},
buttonText: {
fontSize: 28,
...sharedStyles.textRegular
},
buttonView: {
alignItems: 'center',
justifyContent: 'center',
width: 78,
height: 78,
borderRadius: 4
},
textTitle: {
fontSize: 22,
...sharedStyles.textRegular
},
textSubtitle: {
fontSize: 16,
...sharedStyles.textMedium
},
dotsContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginTop: 24,
marginBottom: 40
},
dotsView: {
justifyContent: 'center',
alignItems: 'center',
height: 16
},
grid: {
justifyContent: 'center',
flexDirection: 'column'
},
iconView: {
marginVertical: 16
}
});

View File

@ -0,0 +1,69 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import * as Haptics from 'expo-haptics';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import Base from './Base';
import { TYPE } from './constants';
import I18n from '../../i18n';
const PasscodeChoose = ({ theme, finishProcess, force = false }) => {
const chooseRef = useRef(null);
const confirmRef = useRef(null);
const [subtitle, setSubtitle] = useState(null);
const [status, setStatus] = useState(TYPE.CHOOSE);
const [previousPasscode, setPreviouPasscode] = useState(null);
const firstStep = (p) => {
setTimeout(() => {
setStatus(TYPE.CONFIRM);
setPreviouPasscode(p);
confirmRef?.current?.clearPasscode();
}, 200);
};
const changePasscode = p => finishProcess && finishProcess(p);
const onError = () => {
setTimeout(() => {
setStatus(TYPE.CHOOSE);
setSubtitle(I18n.t('Passcode_choose_error'));
chooseRef?.current?.animate('shake');
chooseRef?.current?.clearPasscode();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}, 200);
};
if (status === TYPE.CONFIRM) {
return (
<Base
ref={confirmRef}
theme={theme}
type={TYPE.CONFIRM}
onEndProcess={changePasscode}
previousPasscode={previousPasscode}
title={I18n.t('Passcode_choose_confirm_title')}
onError={onError}
/>
);
}
return (
<Base
ref={chooseRef}
theme={theme}
type={TYPE.CHOOSE}
onEndProcess={firstStep}
title={I18n.t('Passcode_choose_title')}
subtitle={subtitle || (force ? I18n.t('Passcode_choose_force_set') : null)}
/>
);
};
PasscodeChoose.propTypes = {
theme: PropTypes.string,
force: PropTypes.bool,
finishProcess: PropTypes.func
};
export default gestureHandlerRootHOC(PasscodeChoose);

View File

@ -0,0 +1,106 @@
import React, { useEffect, useRef, useState } from 'react';
import { useAsyncStorage } from '@react-native-community/async-storage';
import RNUserDefaults from 'rn-user-defaults';
import PropTypes from 'prop-types';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import * as Haptics from 'expo-haptics';
import { sha256 } from 'js-sha256';
import Base from './Base';
import Locked from './Base/Locked';
import { TYPE } from './constants';
import {
ATTEMPTS_KEY, LOCKED_OUT_TIMER_KEY, PASSCODE_KEY, MAX_ATTEMPTS
} from '../../constants/localAuthentication';
import { resetAttempts, biometryAuth } from '../../utils/localAuthentication';
import { getLockedUntil, getDiff } from './utils';
import I18n from '../../i18n';
const PasscodeEnter = ({ theme, hasBiometry, finishProcess }) => {
const ref = useRef(null);
let attempts = 0;
let lockedUntil = false;
const [passcode, setPasscode] = useState(null);
const [status, setStatus] = useState(null);
const { getItem: getAttempts, setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY);
const { setItem: setLockedUntil } = useAsyncStorage(LOCKED_OUT_TIMER_KEY);
const fetchPasscode = async() => {
const p = await RNUserDefaults.get(PASSCODE_KEY);
setPasscode(p);
};
const biometry = async() => {
if (hasBiometry && status === TYPE.ENTER) {
const result = await biometryAuth();
if (result?.success) {
finishProcess();
}
}
};
const readStorage = async() => {
lockedUntil = await getLockedUntil();
if (lockedUntil) {
const diff = getDiff(lockedUntil);
if (diff <= 1) {
await resetAttempts();
setStatus(TYPE.ENTER);
} else {
attempts = await getAttempts();
setStatus(TYPE.LOCKED);
}
} else {
setStatus(TYPE.ENTER);
}
await fetchPasscode();
biometry();
};
useEffect(() => {
readStorage();
}, [status]);
const onEndProcess = (p) => {
setTimeout(() => {
if (sha256(p) === passcode) {
finishProcess();
} else {
attempts += 1;
if (attempts >= MAX_ATTEMPTS) {
setStatus(TYPE.LOCKED);
setLockedUntil(new Date().toISOString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
} else {
ref.current.wrongPasscode();
setAttempts(attempts?.toString());
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
}
}
}, 200);
};
if (status === TYPE.LOCKED) {
return <Locked theme={theme} setStatus={setStatus} />;
}
return (
<Base
ref={ref}
theme={theme}
type={TYPE.ENTER}
title={I18n.t('Passcode_enter_title')}
showBiometry={hasBiometry}
onEndProcess={onEndProcess}
onBiometryPress={biometry}
/>
);
};
PasscodeEnter.propTypes = {
theme: PropTypes.string,
hasBiometry: PropTypes.string,
finishProcess: PropTypes.func
};
export default gestureHandlerRootHOC(PasscodeEnter);

View File

@ -0,0 +1,6 @@
export const TYPE = {
CHOOSE: 'choose',
CONFIRM: 'confirm',
ENTER: 'enter',
LOCKED: 'locked'
};

View File

@ -0,0 +1,4 @@
import PasscodeEnter from './PasscodeEnter';
import PasscodeChoose from './PasscodeChoose';
export { PasscodeEnter, PasscodeChoose };

View File

@ -0,0 +1,14 @@
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment';
import { LOCKED_OUT_TIMER_KEY, TIME_TO_LOCK } from '../../constants/localAuthentication';
export const getLockedUntil = async() => {
const t = await AsyncStorage.getItem(LOCKED_OUT_TIMER_KEY);
if (t) {
return moment(t).add(TIME_TO_LOCK);
}
return null;
};
export const getDiff = t => new Date(t) - new Date();

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Image, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors';
import { STATUS_COLORS, themes } from '../constants/colors';
const styles = StyleSheet.create({
style: {
@ -15,7 +15,7 @@ const styles = StyleSheet.create({
});
const RoomTypeIcon = React.memo(({
type, size, isGroupChat, style, theme
type, size, isGroupChat, status, style, theme
}) => {
if (!type) {
return null;
@ -36,7 +36,7 @@ const RoomTypeIcon = React.memo(({
}
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
} if (type === 'l') {
return <CustomIcon name='livechat' size={13} style={[styles.style, styles.discussion, { color }]} />;
return <CustomIcon name='omnichannel' size={13} style={[styles.style, styles.discussion, { color: STATUS_COLORS[status] }]} />;
}
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
});
@ -45,6 +45,7 @@ RoomTypeIcon.propTypes = {
theme: PropTypes.string,
type: PropTypes.string,
isGroupChat: PropTypes.bool,
status: PropTypes.string,
size: PropTypes.number,
style: PropTypes.object
};

View File

@ -64,8 +64,10 @@ export default class RCTextInput extends React.PureComponent {
inputRef: PropTypes.func,
testID: PropTypes.string,
iconLeft: PropTypes.string,
iconRight: PropTypes.string,
placeholder: PropTypes.string,
left: PropTypes.element,
onIconRightPress: PropTypes.func,
theme: PropTypes.string
}
@ -90,6 +92,19 @@ export default class RCTextInput extends React.PureComponent {
);
}
get iconRight() {
const { iconRight, onIconRightPress, theme } = this.props;
return (
<BorderlessButton onPress={onIconRightPress} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon
name={iconRight}
style={{ color: themes[theme].bodyText }}
size={20}
/>
</BorderlessButton>
);
}
get iconPassword() {
const { showPassword } = this.state;
const { testID, theme } = this.props;
@ -117,7 +132,7 @@ export default class RCTextInput extends React.PureComponent {
render() {
const { showPassword } = this.state;
const {
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, iconRight, inputStyle, testID, placeholder, theme, ...inputProps
} = this.props;
const { dangerColor } = themes[theme];
return (
@ -140,7 +155,7 @@ export default class RCTextInput extends React.PureComponent {
style={[
styles.input,
iconLeft && styles.inputIconLeft,
secureTextEntry && styles.inputIconRight,
(secureTextEntry || iconRight) && styles.inputIconRight,
{
backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor,
@ -165,6 +180,7 @@ export default class RCTextInput extends React.PureComponent {
{...inputProps}
/>
{iconLeft ? this.iconLeft : null}
{iconRight ? this.iconRight : null}
{secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null}
{left}

View File

@ -92,7 +92,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
isVisible={visible}
hideModalContentWhileAnimating
>
<View style={styles.container}>
<View style={styles.container} testID='two-factor'>
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
@ -106,6 +106,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
keyboardType={method?.keyboardType}
secureTextEntry={method?.secureTextEntry}
error={data.invalid && { error: 'totp-invalid', reason: I18n.t('Code_or_password_invalid') }}
testID='two-factor-input'
/>
{isEmail && <Text style={[styles.sendEmail, { color }]} onPress={sendEmail}>{I18n.t('Send_me_the_code_again')}</Text>}
<View style={styles.buttonContainer}>
@ -123,6 +124,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
style={styles.button}
onPress={onSubmit}
theme={theme}
testID='two-factor-send'
/>
</View>
</View>

View File

@ -12,11 +12,13 @@ import styles from './styles';
const keyExtractor = item => item.value.toString();
const Chip = ({ item, onSelect, theme }) => (
const Chip = ({
item, onSelect, style, theme
}) => (
<Touchable
key={item.value}
onPress={() => onSelect(item)}
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }]}
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<>
@ -29,17 +31,21 @@ const Chip = ({ item, onSelect, theme }) => (
Chip.propTypes = {
item: PropTypes.object,
onSelect: PropTypes.func,
style: PropTypes.object,
theme: PropTypes.string
};
const Chips = ({ items, onSelect, theme }) => (
const Chips = ({
items, onSelect, style, theme
}) => (
<View style={styles.chips}>
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} theme={theme} />)}
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} theme={theme} />)}
</View>
);
Chips.propTypes = {
items: PropTypes.array,
onSelect: PropTypes.func,
style: PropTypes.object,
theme: PropTypes.string
};

View File

@ -1,5 +1,5 @@
import React from 'react';
import { View } from 'react-native';
import { View, Text } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
@ -9,16 +9,16 @@ import ActivityIndicator from '../../ActivityIndicator';
import styles from './styles';
const Input = ({
children, open, theme, loading, inputStyle, disabled
children, onPress, theme, loading, inputStyle, placeholder, disabled
}) => (
<Touchable
onPress={() => open(true)}
onPress={onPress}
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={disabled}
>
<View style={[styles.input, { borderColor: themes[theme].separatorColor }]}>
{children}
{placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children}
{
loading
? <ActivityIndicator style={[styles.loading, styles.icon]} />
@ -29,10 +29,11 @@ const Input = ({
);
Input.propTypes = {
children: PropTypes.node,
open: PropTypes.func,
onPress: PropTypes.func,
theme: PropTypes.string,
inputStyle: PropTypes.object,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
loading: PropTypes.bool
};

View File

@ -43,14 +43,14 @@ export const MultiSelect = React.memo(({
inputStyle,
theme
}) => {
const [selected, select] = useState(values || []);
const [selected, select] = useState(Array.isArray(values) ? values : []);
const [open, setOpen] = useState(false);
const [search, onSearchChange] = useState('');
const [currentValue, setCurrentValue] = useState('');
const [showContent, setShowContent] = useState(false);
useEffect(() => {
if (values) {
if (Array.isArray(values)) {
select(values);
}
}, [values]);
@ -136,7 +136,7 @@ export const MultiSelect = React.memo(({
/>
) : (
<Input
open={onShow}
onPress={onShow}
theme={theme}
loading={loading}
disabled={disabled}
@ -150,7 +150,7 @@ export const MultiSelect = React.memo(({
const items = options.filter(option => selected.includes(option.value));
button = (
<Input
open={onShow}
onPress={onShow}
theme={theme}
loading={loading}
disabled={disabled}

View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
const styles = StyleSheet.create({
content: {
@ -18,11 +17,7 @@ const styles = StyleSheet.create({
},
text: {
flex: 1,
padding: 4,
fontSize: 16,
lineHeight: 22,
textAlignVertical: 'center',
...sharedStyles.textRegular
padding: 4
},
field: {
marginVertical: 6
@ -54,7 +49,7 @@ export const Section = ({
accessory && accessoriesRight.includes(accessory.type) ? styles.row : styles.column
]}
>
{text ? <Text style={[styles.text, { color: themes[theme].bodyText }]}>{parser.text(text)}</Text> : null}
{text ? <View style={styles.text}>{parser.text(text)}</View> : null}
{fields ? <Fields fields={fields} theme={theme} parser={parser} /> : null}
{accessory ? <Accessory element={{ blockId, appId, ...accessory }} parser={parser} /> : null}
</View>

View File

@ -1,6 +1,6 @@
/* eslint-disable class-methods-use-this */
import React, { useContext } from 'react';
import { StyleSheet } from 'react-native';
import { StyleSheet, Text } from 'react-native';
import {
uiKitMessage,
UiKitParserMessage,
@ -13,8 +13,9 @@ import Markdown from '../markdown';
import Button from '../Button';
import TextInput from '../TextInput';
import { useBlockContext } from './utils';
import { useBlockContext, textParser } from './utils';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import { Divider } from './Divider';
import { Section } from './Section';
@ -37,6 +38,12 @@ const styles = StyleSheet.create({
},
button: {
marginBottom: 16
},
text: {
fontSize: 16,
lineHeight: 22,
textAlignVertical: 'center',
...sharedStyles.textRegular
}
});
@ -46,7 +53,7 @@ class MessageParser extends UiKitParserMessage {
text({ text, type } = { text: '' }, context) {
const { theme } = useContext(ThemeContext);
if (type !== 'mrkdwn') {
return text;
return <Text style={[styles.text, { color: themes[theme].bodyText }]}>{text}</Text>;
}
const isContext = context === BLOCK_CONTEXT.CONTEXT;
@ -70,7 +77,7 @@ class MessageParser extends UiKitParserMessage {
<Button
key={actionId}
type={style}
title={this.text(text)}
title={textParser([text])}
loading={loading}
onPress={() => action({ value })}
style={styles.button}

View File

@ -11,11 +11,10 @@ const AtMention = React.memo(({
}) => {
let mentionStyle = { ...styles.mention, color: themes[theme].buttonText };
if (mention === 'all' || mention === 'here') {
mentionStyle = {
...mentionStyle,
...styles.mentionAll
};
} else if (mention === username) {
return <Text style={[mentionStyle, styles.mentionAll, ...style]}>{mention}</Text>;
}
if (mention === username) {
mentionStyle = {
...mentionStyle,
backgroundColor: themes[theme].actionTintColor

View File

@ -9,10 +9,10 @@ import { themes } from '../../constants/colors';
import styles from './styles';
const Emoji = React.memo(({
emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis, style = [], theme
literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = [], theme
}) => {
const emojiUnicode = shortnameToUnicode(literal);
const emoji = getCustomEmoji && getCustomEmoji(emojiName);
const emoji = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
if (emoji && customEmojis) {
return (
<CustomEmoji
@ -36,7 +36,6 @@ const Emoji = React.memo(({
});
Emoji.propTypes = {
emojiName: PropTypes.string,
literal: PropTypes.string,
isMessageContainsOnlyEmoji: PropTypes.bool,
getCustomEmoji: PropTypes.func,

View File

@ -261,13 +261,12 @@ class Markdown extends PureComponent {
);
}
renderEmoji = ({ emojiName, literal }) => {
renderEmoji = ({ literal }) => {
const {
getCustomEmoji, baseUrl, customEmojis = true, style, theme
getCustomEmoji, baseUrl, customEmojis, style, theme
} = this.props;
return (
<MarkdownEmoji
emojiName={emojiName}
literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji}

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply';
const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme
attachments, timeFormat, showAttachment, getCustomEmoji, theme
}) => {
if (!attachments || attachments.length === 0) {
return null;
@ -16,25 +16,23 @@ const Attachments = React.memo(({
return attachments.map((file, index) => {
if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Image key={file.image_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Video key={file.video_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
}
// eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />;
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} theme={theme} />;
});
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
Attachments.propTypes = {
attachments: PropTypes.array,
timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string

View File

@ -3,18 +3,31 @@ import PropTypes from 'prop-types';
import {
View, StyleSheet, Text, Easing, Dimensions
} from 'react-native';
import Video from 'react-native-video';
import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider';
import moment from 'moment';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import Touchable from './Touchable';
import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split';
import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator';
const mode = {
allowsRecordingIOS: false,
playsInSilentModeIOS: true,
staysActiveInBackground: false,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
};
const styles = StyleSheet.create({
audioContainer: {
@ -31,6 +44,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: 'transparent'
},
audioLoading: {
marginHorizontal: 8
},
slider: {
flex: 1
},
@ -51,29 +67,36 @@ const sliderAnimationConfig = {
delay: 0
};
const Button = React.memo(({ paused, onPress, theme }) => (
const Button = React.memo(({
loading, paused, onPress, theme
}) => (
<Touchable
style={styles.playPauseButton}
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
{
loading
? <ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} theme={theme} />
: <CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
}
</Touchable>
));
Button.propTypes = {
loading: PropTypes.bool,
paused: PropTypes.bool,
theme: PropTypes.string,
onPress: PropTypes.func
};
Button.displayName = 'MessageAudioButton';
class Audio extends React.Component {
class MessageAudio extends React.Component {
static contextType = MessageContext;
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
theme: PropTypes.string,
split: PropTypes.bool,
getCustomEmoji: PropTypes.func
@ -81,18 +104,39 @@ class Audio extends React.Component {
constructor(props) {
super(props);
const { baseUrl, file, user } = props;
this.state = {
loading: false,
currentTime: 0,
duration: 0,
paused: true,
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
paused: true
};
this.sound = new Audio.Sound();
this.sound.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
}
async componentDidMount() {
const { file } = this.props;
const { baseUrl, user } = this.context;
let url = file.audio_url;
if (!url.startsWith('http')) {
url = `${ baseUrl }${ file.audio_url }`;
}
this.setState({ loading: true });
try {
await Audio.setAudioModeAsync(mode);
await this.sound.loadAsync({ uri: `${ url }?rc_uid=${ user.id }&rc_token=${ user.token }` });
} catch {
// Do nothing
}
this.setState({ loading: false });
}
shouldComponentUpdate(nextProps, nextState) {
const {
currentTime, duration, paused, uri
currentTime, duration, paused, loading
} = this.state;
const { file, split, theme } = this.props;
if (nextProps.theme !== theme) {
@ -107,58 +151,108 @@ class Audio extends React.Component {
if (nextState.paused !== paused) {
return true;
}
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
if (nextProps.split !== split) {
return true;
}
if (nextState.loading !== loading) {
return true;
}
return false;
}
componentDidUpdate() {
const { paused } = this.state;
if (paused) {
deactivateKeepAwake();
} else {
activateKeepAwake();
}
}
async componentWillUnmount() {
try {
await this.sound.stopAsync();
} catch {
// Do nothing
}
}
onPlaybackStatusUpdate = (status) => {
if (status) {
this.onLoad(status);
this.onProgress(status);
this.onEnd(status);
}
}
onLoad = (data) => {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
const duration = data.durationMillis / 1000;
this.setState({ duration: duration > 0 ? duration : 0 });
}
onProgress = (data) => {
const { duration } = this.state;
if (data.currentTime <= duration) {
this.setState({ currentTime: data.currentTime });
const currentTime = data.positionMillis / 1000;
if (currentTime <= duration) {
this.setState({ currentTime });
}
}
onEnd = () => {
this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => {
this.player.seek(0);
});
onEnd = async(data) => {
if (data.didJustFinish) {
try {
await this.sound.stopAsync();
this.setState({ paused: true, currentTime: 0 });
} catch {
// do nothing
}
}
}
get duration() {
const { duration } = this.state;
return formatTime(duration);
const { currentTime, duration } = this.state;
return formatTime(currentTime || duration);
}
setRef = ref => this.player = ref;
togglePlayPause = () => {
const { paused } = this.state;
this.setState({ paused: !paused });
this.setState({ paused: !paused }, this.playPause);
}
onValueChange = value => this.setState({ currentTime: value });
playPause = async() => {
const { paused } = this.state;
try {
if (paused) {
await this.sound.pauseAsync();
} else {
await this.sound.playAsync();
}
} catch {
// Do nothing
}
}
onValueChange = async(value) => {
try {
this.setState({ currentTime: value });
await this.sound.setPositionAsync(value * 1000);
} catch {
// Do nothing
}
}
render() {
const {
uri, paused, currentTime, duration
loading, paused, currentTime, duration
} = this.state;
const {
user, baseUrl, file, getCustomEmoji, split, theme
file, getCustomEmoji, split, theme
} = this.props;
const { description } = file;
const { baseUrl, user } = this.context;
if (!baseUrl) {
return null;
@ -173,17 +267,7 @@ class Audio extends React.Component {
split && sharedStyles.tabletContent
]}
>
<Video
ref={this.setRef}
source={{ uri }}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
paused={paused}
repeat={false}
ignoreSilentSwitch='ignore'
/>
<Button paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Slider
style={styles.slider}
value={currentTime}
@ -205,4 +289,4 @@ class Audio extends React.Component {
}
}
export default withSplit(Audio);
export default withSplit(MessageAudio);

View File

@ -1,17 +1,19 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils';
import I18n from '../../i18n';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Broadcast = React.memo(({
author, user, broadcast, replyBroadcast, theme
author, broadcast, theme
}) => {
const { user, replyBroadcast } = useContext(MessageContext);
const isOwn = author._id === user.id;
if (broadcast && !isOwn) {
return (
@ -36,10 +38,8 @@ const Broadcast = React.memo(({
Broadcast.propTypes = {
author: PropTypes.object,
user: PropTypes.object,
broadcast: PropTypes.bool,
theme: PropTypes.string,
replyBroadcast: PropTypes.func
theme: PropTypes.string
};
Broadcast.displayName = 'MessageBroadcast';

View File

@ -1,8 +1,8 @@
import React from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
import styles from './styles';
import I18n from '../../i18n';

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import equal from 'deep-equal';
@ -8,6 +8,7 @@ import styles from './styles';
import Markdown from '../markdown';
import { getInfoMessage } from './utils';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Content = React.memo((props) => {
if (props.isInfo) {
@ -26,12 +27,13 @@ const Content = React.memo((props) => {
if (props.tmid && !props.msg) {
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else {
const { baseUrl, user } = useContext(MessageContext);
content = (
<Markdown
msg={props.msg}
baseUrl={props.baseUrl}
baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji}
username={props.user.username}
username={user.username}
isEdited={props.isEdited}
numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
preview={props.tmid && !props.isThreadRoom}
@ -77,8 +79,6 @@ Content.propTypes = {
msg: PropTypes.string,
theme: PropTypes.string,
isEdited: PropTypes.bool,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),

View File

@ -0,0 +1,4 @@
import React from 'react';
const MessageContext = React.createContext();
export default MessageContext;

View File

@ -1,20 +1,22 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
import styles from './styles';
import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons';
import { DISCUSSION } from './constants';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Discussion = React.memo(({
msg, dcount, dlm, onDiscussionPress, theme
msg, dcount, dlm, theme
}) => {
const time = formatLastMessage(dlm);
const buttonText = formatMessageCount(dcount, DISCUSSION);
const { onDiscussionPress } = useContext(MessageContext);
return (
<>
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
@ -55,8 +57,7 @@ Discussion.propTypes = {
msg: PropTypes.string,
dcount: PropTypes.number,
dlm: PropTypes.string,
theme: PropTypes.string,
onDiscussionPress: PropTypes.func
theme: PropTypes.string
};
Discussion.displayName = 'MessageDiscussion';

View File

@ -6,7 +6,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
const Emoji = React.memo(({
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji
}) => {
const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent);
@ -18,9 +18,9 @@ const Emoji = React.memo(({
Emoji.propTypes = {
content: PropTypes.string,
baseUrl: PropTypes.string,
standardEmojiStyle: PropTypes.object,
customEmojiStyle: PropTypes.object,
baseUrl: PropTypes.string,
getCustomEmoji: PropTypes.func
};
Emoji.displayName = 'MessageEmoji';

View File

@ -1,18 +1,19 @@
import React from 'react';
import React, { useContext } from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress';
import Touchable from './Touchable';
import Markdown from '../markdown';
import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils';
import { withSplit } from '../../split';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const ImageProgress = createImageProgress(FastImage);
@ -41,8 +42,9 @@ export const MessageImage = React.memo(({ img, theme }) => (
));
const ImageContainer = React.memo(({
file, imageUrl, baseUrl, user, showAttachment, getCustomEmoji, split, theme
file, imageUrl, showAttachment, getCustomEmoji, split, theme
}) => {
const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) {
return null;
@ -71,8 +73,6 @@ const ImageContainer = React.memo(({
ImageContainer.propTypes = {
file: PropTypes.object,
imageUrl: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,

View File

@ -1,8 +1,10 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import MessageContext from './Context';
import User from './User';
import styles from './styles';
import RepliedThread from './RepliedThread';
@ -111,10 +113,11 @@ const MessageTouchable = React.memo((props) => {
</View>
);
}
const { onPress, onLongPress } = useContext(MessageContext);
return (
<Touchable
onLongPress={props.onLongPress}
onPress={props.onPress}
onLongPress={onLongPress}
onPress={onPress}
disabled={props.isInfo || props.archived || props.isTemp}
>
<View>
@ -129,9 +132,7 @@ MessageTouchable.propTypes = {
hasError: PropTypes.bool,
isInfo: PropTypes.bool,
isTemp: PropTypes.bool,
archived: PropTypes.bool,
onLongPress: PropTypes.func,
onPress: PropTypes.func
archived: PropTypes.bool
};
Message.propTypes = {
@ -143,7 +144,6 @@ Message.propTypes = {
hasError: PropTypes.bool,
style: PropTypes.any,
onLongPress: PropTypes.func,
onPress: PropTypes.func,
isReadReceiptEnabled: PropTypes.bool,
unread: PropTypes.bool,
theme: PropTypes.string

View File

@ -1,34 +1,34 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import Avatar from '../Avatar';
import styles from './styles';
import MessageContext from './Context';
const MessageAvatar = React.memo(({
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo
isHeader, avatar, author, small, navToRoomInfo, emoji, getCustomEmoji, theme
}) => {
const { baseUrl, user } = useContext(MessageContext);
if (isHeader && author) {
const navParam = {
t: 'd',
rid: author._id
};
return (
<TouchableOpacity
onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id}
>
<Avatar
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={small ? 20 : 36}
borderRadius={small ? 2 : 4}
avatar={avatar}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
</TouchableOpacity>
<Avatar
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={small ? 20 : 36}
borderRadius={small ? 2 : 4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
getCustomEmoji={getCustomEmoji}
avatar={avatar}
emoji={emoji}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
theme={theme}
/>
);
}
return null;
@ -37,11 +37,12 @@ const MessageAvatar = React.memo(({
MessageAvatar.propTypes = {
isHeader: PropTypes.bool,
avatar: PropTypes.string,
emoji: PropTypes.string,
author: PropTypes.obj,
baseUrl: PropTypes.string,
user: PropTypes.obj,
small: PropTypes.bool,
navToRoomInfo: PropTypes.func
navToRoomInfo: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};
MessageAvatar.displayName = 'MessageAvatar';

View File

@ -1,16 +1,18 @@
import React from 'react';
import Touchable from 'react-native-platform-touchable';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
const MessageError = React.memo(({ hasError, theme }) => {
if (!hasError) {
return null;
}
const { onErrorPress } = useContext(MessageContext);
return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
@ -20,7 +22,6 @@ const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
MessageError.propTypes = {
hasError: PropTypes.bool,
onErrorPress: PropTypes.func,
theme: PropTypes.string
};
MessageError.displayName = 'MessageError';

View File

@ -1,33 +1,40 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import MessageContext from './Context';
const AddReaction = React.memo(({ reactionInit, theme }) => (
<Touchable
onPress={reactionInit}
key='message-add-reaction'
testID='message-add-reaction'
style={[styles.reactionButton, { backgroundColor: themes[theme].backgroundColor }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
hitSlop={BUTTON_HIT_SLOP}
>
<View style={[styles.reactionContainer, { borderColor: themes[theme].borderColor }]}>
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
</View>
</Touchable>
));
const AddReaction = React.memo(({ theme }) => {
const { reactionInit } = useContext(MessageContext);
return (
<Touchable
onPress={reactionInit}
key='message-add-reaction'
testID='message-add-reaction'
style={[styles.reactionButton, { backgroundColor: themes[theme].backgroundColor }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
hitSlop={BUTTON_HIT_SLOP}
>
<View style={[styles.reactionContainer, { borderColor: themes[theme].borderColor }]}>
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
</View>
</Touchable>
);
});
const Reaction = React.memo(({
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji, theme
reaction, getCustomEmoji, theme
}) => {
const {
onReactionPress, onReactionLongPress, baseUrl, user
} = useContext(MessageContext);
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
return (
<Touchable
@ -54,7 +61,7 @@ const Reaction = React.memo(({
});
const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme
reactions, getCustomEmoji, theme
}) => {
if (!Array.isArray(reactions) || reactions.length === 0) {
return null;
@ -65,25 +72,17 @@ const Reactions = React.memo(({
<Reaction
key={reaction.emoji}
reaction={reaction}
user={user}
baseUrl={baseUrl}
onReactionLongPress={onReactionLongPress}
onReactionPress={onReactionPress}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
))}
<AddReaction reactionInit={reactionInit} theme={theme} />
<AddReaction theme={theme} />
</View>
);
});
Reaction.propTypes = {
reaction: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};
@ -91,18 +90,12 @@ Reaction.displayName = 'MessageReaction';
Reactions.propTypes = {
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
reactionInit: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};
Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = {
reactionInit: PropTypes.func,
theme: PropTypes.string
};
AddReaction.displayName = 'MessageAddReaction';

View File

@ -1,15 +1,16 @@
import React from 'react';
import React, { useContext } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { withSplit } from '../../split';
import MessageContext from './Context';
const styles = StyleSheet.create({
button: {
@ -79,12 +80,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
});
const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, theme
attachment, getCustomEmoji, theme
}) => {
const text = attachment.text || attachment.title;
if (!text) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
return (
<Markdown
msg={text}
@ -124,11 +126,12 @@ const Fields = React.memo(({ attachment, theme }) => {
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme
attachment, timeFormat, index, getCustomEmoji, split, theme
}) => {
if (!attachment) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
const onPress = () => {
let url = attachment.title_link || attachment.author_link;
@ -136,7 +139,10 @@ const Reply = React.memo(({
return;
}
if (attachment.type === 'file') {
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
if (!url.startsWith('http')) {
url = `${ baseUrl }${ url }`;
}
url = `${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
}
openLink(url, theme);
};
@ -160,8 +166,6 @@ const Reply = React.memo(({
<Description
attachment={attachment}
timeFormat={timeFormat}
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
@ -174,8 +178,6 @@ const Reply = React.memo(({
Reply.propTypes = {
attachment: PropTypes.object,
timeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number,
theme: PropTypes.string,
getCustomEmoji: PropTypes.func,
@ -192,8 +194,6 @@ Title.displayName = 'MessageReplyTitle';
Description.propTypes = {
attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string
};

View File

@ -0,0 +1,25 @@
import React, { useContext } from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import MessageContext from './Context';
const RCTouchable = React.memo(({ children, ...props }) => {
const { onLongPress } = useContext(MessageContext);
return (
<Touchable
onLongPress={onLongPress}
{...props}
>
{children}
</Touchable>
);
});
RCTouchable.propTypes = {
children: PropTypes.node
};
RCTouchable.Ripple = (...args) => Touchable.Ripple(...args);
RCTouchable.SelectableBackgroundBorderless = () => Touchable.SelectableBackgroundBorderless();
export default RCTouchable;

View File

@ -1,12 +1,12 @@
import React from 'react';
import React, { useContext } from 'react';
import {
View, Text, StyleSheet, Clipboard
} from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'lodash/isEqual';
import Touchable from './Touchable';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
@ -15,6 +15,7 @@ import { withSplit } from '../../split';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
import MessageContext from './Context';
const styles = StyleSheet.create({
button: {
@ -52,10 +53,11 @@ const styles = StyleSheet.create({
}
});
const UrlImage = React.memo(({ image, user, baseUrl }) => {
const UrlImage = React.memo(({ image }) => {
if (!image) {
return null;
}
const { baseUrl, user } = useContext(MessageContext);
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
@ -79,7 +81,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
});
const Url = React.memo(({
url, index, user, baseUrl, split, theme
url, index, split, theme
}) => {
if (!url) {
return null;
@ -109,7 +111,7 @@ const Url = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<>
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
<UrlImage image={url.image} />
<UrlContent title={url.title} description={url.description} theme={theme} />
</>
</Touchable>
@ -117,21 +119,19 @@ const Url = React.memo(({
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
const Urls = React.memo(({
urls, user, baseUrl, split, theme
urls, split, theme
}) => {
if (!urls || urls.length === 0) {
return null;
}
return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} split={split} theme={theme} />
<Url url={url} key={url.url} index={index} split={split} theme={theme} />
));
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
UrlImage.propTypes = {
image: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string
image: PropTypes.string
};
UrlImage.displayName = 'MessageUrlImage';
@ -145,8 +145,6 @@ UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = {
url: PropTypes.object.isRequired,
index: PropTypes.number,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
split: PropTypes.bool
};
@ -154,8 +152,6 @@ Url.displayName = 'MessageUrl';
Urls.propTypes = {
urls: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
split: PropTypes.bool
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
View, Text, StyleSheet, TouchableOpacity
@ -11,6 +11,7 @@ import { withTheme } from '../../theme';
import MessageError from './MessageError';
import sharedStyles from '../../views/Styles';
import messageStyles from './styles';
import MessageContext from './Context';
const styles = StyleSheet.create({
container: {
@ -35,13 +36,14 @@ const styles = StyleSheet.create({
});
const User = React.memo(({
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, user, ...props
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, ...props
}) => {
if (isHeader || hasError) {
const navParam = {
t: 'd',
rid: author._id
};
const { user } = useContext(MessageContext);
const username = (useRealName && author.name) || author.username;
const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null;
const time = moment(ts).format(timeFormat);
@ -49,15 +51,14 @@ const User = React.memo(({
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.titleContainer}
onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id}
>
<View style={styles.titleContainer}>
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
{alias || username}
{aliasUsername}
</Text>
</View>
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
{alias || username}
{aliasUsername}
</Text>
</TouchableOpacity>
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
{ hasError && <MessageError hasError={hasError} theme={theme} {...props} /> }
@ -76,7 +77,6 @@ User.propTypes = {
ts: PropTypes.instanceOf(Date),
timeFormat: PropTypes.string,
theme: PropTypes.string,
user: PropTypes.obj,
navToRoomInfo: PropTypes.func
};
User.displayName = 'MessageUser';

View File

@ -1,9 +1,9 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import { isIOS, isTablet } from '../../utils/deviceInfo';
@ -11,6 +11,7 @@ import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -27,12 +28,12 @@ const styles = StyleSheet.create({
});
const Video = React.memo(({
file, baseUrl, user, showAttachment, getCustomEmoji, theme
file, showAttachment, getCustomEmoji, theme
}) => {
const { baseUrl, user } = useContext(MessageContext);
if (!baseUrl) {
return null;
}
const onPress = () => {
if (isTypeSupported(file.video_type)) {
return showAttachment(file);
@ -61,8 +62,6 @@ const Video = React.memo(({
Video.propTypes = {
file: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message';
import MessageContext from './Context';
import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus';
@ -229,7 +230,7 @@ class MessageContainer extends React.Component {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
} = this.props;
const {
id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
} = item;
let message = msg;
@ -240,63 +241,69 @@ class MessageContainer extends React.Component {
}
return (
<Message
id={id}
msg={message}
rid={rid}
author={u}
ts={ts}
type={t}
attachments={attachments}
blocks={blocks}
urls={urls}
reactions={reactions}
alias={alias}
avatar={avatar}
user={user}
timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style}
archived={archived}
broadcast={broadcast}
baseUrl={baseUrl}
useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread}
role={role}
drid={drid}
dcount={dcount}
dlm={dlm}
tmid={tmid}
tcount={tcount}
tlm={tlm}
tmsg={tmsg}
fetchThreadName={fetchThreadName}
mentions={mentions}
channels={channels}
isEdited={editedBy && !!editedBy.username}
isHeader={this.isHeader}
isThreadReply={this.isThreadReply}
isThreadSequential={this.isThreadSequential}
isThreadRoom={isThreadRoom}
isInfo={this.isInfo}
isTemp={this.isTemp}
hasError={this.hasError}
onErrorPress={this.onErrorPress}
onPress={this.onPress}
onLongPress={this.onLongPress}
onReactionLongPress={this.onReactionLongPress}
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
reactionInit={this.reactionInit}
onDiscussionPress={this.onDiscussionPress}
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
callJitsi={callJitsi}
blockAction={blockAction}
theme={theme}
/>
<MessageContext.Provider
value={{
user,
baseUrl,
onPress: this.onPress,
onLongPress: this.onLongPress,
reactionInit: this.reactionInit,
onErrorPress: this.onErrorPress,
replyBroadcast: this.replyBroadcast,
onReactionPress: this.onReactionPress,
onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress
}}
>
<Message
id={id}
msg={message}
rid={rid}
author={u}
ts={ts}
type={t}
attachments={attachments}
blocks={blocks}
urls={urls}
reactions={reactions}
alias={alias}
avatar={avatar}
emoji={emoji}
timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style}
archived={archived}
broadcast={broadcast}
useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread}
role={role}
drid={drid}
dcount={dcount}
dlm={dlm}
tmid={tmid}
tcount={tcount}
tlm={tlm}
tmsg={tmsg}
fetchThreadName={fetchThreadName}
mentions={mentions}
channels={channels}
isEdited={editedBy && !!editedBy.username}
isHeader={this.isHeader}
isThreadReply={this.isThreadReply}
isThreadSequential={this.isThreadSequential}
isThreadRoom={isThreadRoom}
isInfo={this.isInfo}
isTemp={this.isTemp}
hasError={this.hasError}
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
callJitsi={callJitsi}
blockAction={blockAction}
theme={theme}
/>
</MessageContext.Provider>
);
}
}

View File

@ -10,6 +10,7 @@ export default {
'error-could-not-change-email': 'E-Mail konnte nicht geändert werden',
'error-could-not-change-name': 'Name konnte nicht geändert werden',
'error-could-not-change-username': 'Benutzername konnte nicht geändert werden',
'error-could-not-change-status': 'Status konnte nicht geändert werden',
'error-delete-protected-role': 'Eine geschützte Rolle kann nicht gelöscht werden',
'error-department-not-found': 'Abteilung nicht gefunden',
'error-direct-message-file-upload-not-allowed': 'Dateifreigabe in direkten Nachrichten nicht zulässig',
@ -17,6 +18,7 @@ export default {
'error-email-domain-blacklisted': 'Die E-Mail-Domain wird auf die schwarze Liste gesetzt',
'error-email-send-failed': 'Fehler beim Versuch, eine E-Mail zu senden: {{message}}',
'error-save-image': 'Fehler beim Speichern des Bildes',
'error-save-video': 'Fehler beim Speichern des Videos',
'error-field-unavailable': '{{field}} wird bereits verwendet :(',
'error-file-too-large': 'Datei ist zu groß',
'error-importer-not-defined': 'Der Import wurde nicht korrekt definiert, es fehlt die Importklasse.',
@ -81,12 +83,14 @@ export default {
Activity: 'Aktivität',
Add_Reaction: 'Reaktion hinzufügen',
Add_Server: 'Server hinzufügen',
Add_users: 'Nutzer hinzufügen',
Add_users: 'Benutzer hinzufügen',
Admin_Panel: 'Admin-Panel',
Agent: 'Agent',
Alert: 'Benachrichtigung',
alert: 'Benachrichtigung',
alerts: 'Benachrichtigungen',
All_users_in_the_channel_can_write_new_messages: 'Alle Benutzer im Kanal können neue Nachrichten schreiben',
A_meaningful_name_for_the_discussion_room: 'Ein aussagekräftiger Name für den Diskussionsraum',
All: 'Alles',
All_Messages: 'Alle Nachrichten',
Allow_Reactions: 'Reaktionen zulassen',
@ -130,12 +134,15 @@ export default {
Click_to_join: 'Klicken um teilzunehmen!',
Close: 'Schließen',
Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl',
Closing_chat: 'Chat schließen',
Change_language_loading: 'Ändere Sprache.',
Chat_closed_by_agent: 'Chat durch den Agenten geschlossen',
Choose: 'Wählen',
Choose_from_library: 'Aus der Bibliothek auswählen',
Choose_file: 'Datei auswählen',
Choose_where_you_want_links_be_opened: 'Entscheide, wie Links geöffnet werden sollen',
Code: 'Code',
Code_or_password_invalid: 'Code oder Passwort sind falsch',
Collaborative: 'Kollaborativ',
Confirm: 'Bestätigen',
Connect: 'Verbinden',
@ -147,6 +154,7 @@ export default {
Continue_with: 'Weitermachen mit',
Copied_to_clipboard: 'In die Zwischenablage kopiert!',
Copy: 'Kopieren',
Conversation: 'Konversationen',
Permalink: 'Permalink',
Certificate_password: 'Zertifikats-Passwort',
Clear_cache: 'Lokalen Server-Cache leeren',
@ -154,14 +162,18 @@ export default {
Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?',
Create_account: 'Ein Konto erstellen',
Create_Channel: 'Kanal erstellen',
Create_Direct_Messages: 'Direkt-Nachricht erstellen',
Create_Discussion: 'Diskussion erstellen',
Created_snippet: 'Erstellt ein Snippet',
Create_a_new_workspace: 'Erstellen Sie einen neuen Arbeitsbereich',
Create: 'Erstellen',
Custom_Status: 'eigener Status',
Dark: 'Dunkel',
Dark_level: 'Dunkelstufe',
Default: 'Standard',
Default_browser: 'Standard-Browser',
Delete_Room_Warning: 'Durch das Löschen eines Raums werden alle Nachrichten gelöscht, die im Raum gepostet wurden. Das kann nicht rückgängig gemacht werden.',
Department: 'Abteilung',
delete: 'löschen',
Delete: 'Löschen',
DELETE: 'LÖSCHEN',
@ -173,17 +185,23 @@ export default {
Direct_Messages: 'Direkte Nachrichten',
Disable_notifications: 'Benachrichtigungen deaktiveren',
Discussions: 'Diskussionen',
Dont_Have_An_Account: 'Sie haben noch kein Konto?',
Discussion_Desc: 'Hilft dir die Übersicht zu behalten! Durch das Erstellen einer Diskussion wird ein Unter-Kanal im ausgewählten Raum erzeugt und beide verknüpft.',
Discussion_name: 'Diskussions-Name',
Done: 'Erledigt',
Dont_Have_An_Account: 'Du hast noch kein Konto?',
Do_you_have_an_account: 'Du hast schon ein Konto?',
Do_you_have_a_certificate: 'Haben Sie ein Zertifikat?',
Do_you_really_want_to_key_this_room_question_mark: 'Möchten Sie diesen Raum wirklich {{key}}?',
edit: 'bearbeiten',
edited: 'bearbeitet',
Edit: 'Bearbeiten',
Edit_Status: 'Status ändern',
Edit_Invite: 'Einladung bearbeiten',
Email_or_password_field_is_empty: 'Das E-Mail- oder Passwortfeld ist leer',
Email: 'Email',
EMAIL: 'EMAIL',
email: 'Email',
Empty_title: 'leerer Titel',
Enable_Auto_Translate: 'Automatische Übersetzung aktivieren',
Enable_notifications: 'Benachrichtigungen aktivieren',
Everyone_can_access_this_channel: 'Jeder kann auf diesen Kanal zugreifen',
@ -200,6 +218,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'Wenn diese E-Mail registriert ist, senden wir Anweisungen zum Zurücksetzen Ihres Passworts. Wenn Sie in Kürze keine E-Mail erhalten, kommen Sie bitte zurück und versuchen Sie es erneut.',
Forgot_password: 'Passwort vergessen',
Forgot_Password: 'Passwort vergessen',
Forward: 'Weiterleiten',
Forward_Chat: 'Chat weiterleiten',
Forward_to_department: 'Weiterleiten an Abteilung',
Forward_to_user: 'Weiterleiten an Benutzer',
Full_table: 'Klicken um die ganze Tabelle anzuzeigen',
Generate_New_Link: 'Neuen Link erstellen',
Group_by_favorites: 'Nach Favoriten gruppieren',
@ -210,19 +232,20 @@ export default {
Has_left_the_channel: 'Hat den Kanal verlassen',
Hide_System_Messages: 'Systemnachrichten verstecken',
Hide_type_messages: 'Verstecke "{{type}}"-Nachrichten',
Message_HideType_uj: 'Nutzer beigetreten',
Message_HideType_ul: 'Nutzer verlassen',
Message_HideType_ru: 'Nutzer entfernt',
Message_HideType_au: 'Nutzer hinzugefügt',
Message_HideType_mute_unmute: 'Nutzer stummgeschaltet / freigegeben',
Message_HideType_uj: 'Benutzer beigetreten',
Message_HideType_ul: 'Benutzer verlassen',
Message_HideType_ru: 'Benutzer entfernt',
Message_HideType_au: 'Benutzer hinzugefügt',
Message_HideType_mute_unmute: 'Benutzer stummgeschaltet / freigegeben',
Message_HideType_r: 'Raumname geändert',
Message_HideType_ut: 'Nutzer ist der Unterhaltung beigetreten',
Message_HideType_ut: 'Benutzer ist der Unterhaltung beigetreten',
Message_HideType_wm: 'Willkommen',
Message_HideType_rm: 'Nachricht entfernt',
Message_HideType_subscription_role_added: 'Rolle wurde gesetzt',
Message_HideType_subscription_role_removed: 'Rolle nicht länger definiert',
Message_HideType_room_archived: 'Raum archiviert',
Message_HideType_room_unarchived: 'Raum nicht mehr archiviert',
IP: 'IP',
In_app: 'In-App-Browser',
IN_APP_AND_DESKTOP: 'IN-APP UND DESKTOP',
In_App_and_Desktop_Alert_info: 'Zeigt ein Banner oben am Bildschirm, wenn die App geöffnet ist und eine Benachrichtigung auf dem Desktop.',
@ -230,12 +253,14 @@ export default {
Invite: 'Einladen',
is_a_valid_RocketChat_instance: 'ist eine gültige Rocket.Chat-Instanz',
is_not_a_valid_RocketChat_instance: 'ist keine gültige Rocket.Chat-Instanz',
is_typing: 'tippt',
is_typing: 'schreibt',
Invalid_or_expired_invite_token: 'Ungültiger oder abgelaufener Einladungscode',
Invalid_server_version: 'Der Server, zu dem Sie eine Verbindung herstellen möchten, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{MinVersion}}.',
Invite_Link: 'Einladungs-Link',
Invite_users: 'Benutzer einladen',
Join: 'Beitreten',
Join_our_open_workspace: 'Tritt unserem offenen Arbeitsbereich bei',
Join_your_workspace: 'Tritt deinem Arbeitsbereich bei',
Just_invited_people_can_access_this_channel: 'Nur eingeladene Personen können auf diesen Kanal zugreifen',
Language: 'Sprache',
last_message: 'letzte Nachricht',
@ -246,12 +271,14 @@ export default {
Light: 'Hell',
License: 'Lizenz',
Livechat: 'Live-Chat',
Livechat_edit: 'Livechat bearbeiten',
Login: 'Anmeldung',
Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.',
Login_with: 'Einloggen mit',
Logging_out: 'Abmelden.',
Logout: 'Abmelden',
Max_number_of_uses: 'Maximale Anzahl der Benutzungen',
Max_number_of_users_allowed_is_number: 'Maximale Anzahl von erlaubten Benutzern ist {{maxUsers}}',
members: 'Mitglieder',
Members: 'Mitglieder',
Mentioned_Messages: 'Erwähnte Nachrichten',
@ -277,6 +304,7 @@ export default {
N_users: '{{n}} Benutzer',
name: 'Name',
Name: 'Name',
Navigation_history: 'Navigations-Verlauf',
Never: 'Niemals',
New_Message: 'Neue Nachricht',
New_Password: 'Neues Kennwort',
@ -303,20 +331,34 @@ export default {
Notifications: 'Benachrichtigungen',
Notification_Duration: 'Benachrichtigungsdauer',
Notification_Preferences: 'Benachrichtigungseinstellungen',
No_available_agents_to_transfer: 'Keine Agenten für den Transfer verfügbar',
Offline: 'Offline',
Oops: 'Hoppla!',
Onboarding_description: 'Ein Arbeitsbereich ist der Ort für die Zusammenarbeit deines Teams oder Organisation. Bitte den Admin des Arbeitsbereichs um eine Adresse, um ihm beizutreten, oder erstelle einen Arbeitsbereich für dein Team.',
Onboarding_join_workspace: 'Tritt einem Arbeitsbereich bei',
Onboarding_subtitle: 'Mehr als Team-Zusammenarbeit',
Onboarding_title: 'Willkommen bei Rocket.Chat',
Onboarding_join_open_description: 'Tritt unserem Arbeitsbereich bei um mit dem Rocket.Chat-Team oder der Gemeinschaft zu chatten.',
Onboarding_agree_terms: 'Durch fortfahren stimmst du Rocket.Chats Bedingungen zu',
Onboarding_less_options: 'Weniger Optionen',
Onboarding_more_options: 'Mehr Optionen',
Online: 'Online',
Only_authorized_users_can_write_new_messages: 'Nur autorisierte Benutzer können neue Nachrichten schreiben',
Open_emoji_selector: 'Öffne die Emoji-Auswahl',
Open_Source_Communication: 'Open-Source-Kommunikation',
Open_your_authentication_app_and_enter_the_code: 'Öffne deine Authentifizierungsanwendung und gib den Code ein.',
OR: 'ODER',
OS: 'OS',
Overwrites_the_server_configuration_and_use_room_config: 'Übergeht die Servereinstellungen und nutzt Einstellung für den Raum',
Password: 'Passwort',
Parent_channel_or_group: 'Übergeordneter Kanal oder Gruppe',
Permalink_copied_to_clipboard: 'Permalink in die Zwischenablage kopiert!',
Phone: 'Telefon',
Pin: 'Anheften',
Pinned_Messages: 'Angeheftete Nachrichten',
pinned: 'angeheftet',
Pinned: 'Angeheftet',
Please_add_a_comment: 'Bitte Kommentar hinzufügen',
Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein',
Please_wait: 'Bitte warten.',
Preferences: 'Einstellungen',
@ -355,6 +397,7 @@ export default {
Reset_password: 'Passwort zurücksetzen',
resetting_password: 'Passwort zurücksetzen',
RESET: 'ZURÜCKSETZEN',
Return: 'Zurück',
Review_app_title: 'Gefällt dir diese App?',
Review_app_desc: 'Gib uns 5 Sterne im {{store}}',
Review_app_yes: 'Sicher!',
@ -375,7 +418,8 @@ export default {
Room_name_changed: 'Raumname geändert in {{name}} von {{userBy}}',
SAVE: 'SPEICHERN',
Save_Changes: 'Änderungen speichern',
Save: 'sparen',
Save: 'speichern',
Saved: 'gespeichert',
saving_preferences: 'Präferenzen speichern',
saving_profile: 'Profil speichern',
saving_settings: 'Einstellungen speichern',
@ -388,17 +432,25 @@ export default {
Seconds: '{{second}} Sekunden',
Select_Avatar: 'Wählen Sie einen Avatar aus',
Select_Server: 'Server auswählen',
Select_Users: 'Wählen Sie einen Benutzer aus',
Select_Users: 'Benutzer auswählen',
Select_a_Channel: 'Kanal auswählen',
Select_a_Department: 'Abteilung auswählen',
Select_an_option: 'Option auswählen',
Select_a_User: 'Benutzer auswählen',
Send: 'Senden',
Send_audio_message: 'Audio-Nachricht senden',
Send_crash_report: 'Absturzbericht senden',
Send_message: 'Nachricht senden',
Send_me_the_code_again: 'Den Code neu versenden',
Send_to: 'Senden an …',
Sent_an_attachment: 'Sende einen Anhang',
Server: 'Server',
Servers: 'Server',
Server_version: 'Server version: {{version}}',
Set_username_subtitle: 'Der Benutzername wird verwendet, damit andere Personen Sie in Nachrichten erwähnen können',
Set_custom_status: 'Individuellen Status setzen',
Set_status: 'Status setzen',
Status_saved_successfully: 'Status erfolgreich gesetzt!',
Settings: 'Einstellungen',
Settings_succesfully_changed: 'Einstellungen erfolgreich geändert!',
Share: 'Teilen',
@ -407,7 +459,7 @@ export default {
Show_more: 'Mehr anzeigen …',
Show_Unread_Counter: 'Zähler anzeigen',
Show_Unread_Counter_Info: 'Anzahl der ungelesenen Nachrichten anzeigen',
Sign_in_your_server: 'Melden Sie sich bei Ihrem Server an',
Sign_in_your_server: 'Melde dich bei deinem Server an',
Sign_Up: 'Anmelden',
Some_field_is_invalid_or_empty: 'Ein Feld ist ungültig oder leer',
Sorting_by: 'Sortierung nach {{key}}',
@ -422,6 +474,7 @@ export default {
Started_call: 'Anruf gestartet von {{userBy}}',
Submit: 'einreichen',
Table: 'Tabelle',
Tags: 'Tags',
Take_a_photo: 'Foto aufnehmen',
Take_a_video: 'Video aufnehmen',
tap_to_change_status: 'Tippen um den Status zu ändern',
@ -441,10 +494,10 @@ export default {
Translate: 'Übersetzen',
Try_again: 'Versuchen Sie es nochmal',
Two_Factor_Authentication: 'Zwei-Faktor-Authentifizierung',
Type_the_channel_name_here: 'Geben Sie hier den Kanalnamen ein',
Type_the_channel_name_here: 'Gib hier den Kanalnamen ein',
unarchive: 'wiederherstellen',
UNARCHIVE: 'WIEDERHERSTELLEN',
Unblock_user: 'Nutzer entsperren',
Unblock_user: 'Benutzer entsperren',
Unfavorite: 'Nicht mehr favorisieren',
Unfollowed_thread: 'Thread nicht mehr folgen',
Unmute: 'Stummschaltung aufheben',
@ -457,9 +510,10 @@ export default {
Updating: 'Aktualisierung …',
Uploading: 'Hochladen',
Upload_file_question_mark: 'Datei hochladen?',
User: 'Benutzer',
Users: 'Benutzer',
User_added_by: 'Benutzer {{userAdded}} hinzugefügt von {{userBy}}',
User_Info: 'Nutzerinfo',
User_Info: 'Benutzerinfo',
User_has_been_key: 'Benutzer wurde {{key}}!',
User_is_no_longer_role_by_: '{{user}} ist nicht länger {{role}} von {{userBy}}',
User_muted_by: 'Benutzer {{userMuted}} von {{userBy}} stummgeschaltet',
@ -471,18 +525,26 @@ export default {
Username: 'Benutzername',
Username_or_email: 'Benutzername oder E-Mail-Adresse',
Uses_server_configuration: 'Nutzt Servereinstellungen',
Usually_a_discussion_starts_with_a_question_like_How_do_I_upload_a_picture: 'Üblicherweise beginnt eine Diskussion mit einer Frage, beispielsweise: "Wie lade ich ein Bild hoch?"',
Validating: 'Validierung',
Registration_Succeeded: 'Registrierung erfolgreich!',
Verify: 'Überprüfen',
Verify_email_title: 'Registrierung erfolgreich!',
Verify_email_desc: 'Wir haben dir eine Email geschickt um deine Anmeldung zu bestätigen. Wenn du keine Email erhältst, komme bitte wieder und versuche es noch einmal.',
Verify_your_email_for_the_code_we_sent: 'Prüfe deine Mails für den Code, den wir dir eben geschickt haben.',
Video_call: 'Videoanruf',
View_Original: 'Original anzeigen',
Voice_call: 'Sprachanruf',
Websocket_disabled: 'Websockets sind auf diesem Server nicht aktiviert.\n{{contact}}',
Welcome: 'Herzlich willkommen',
What_are_you_doing_right_now: 'Was machst du gerade?',
Whats_your_2fa: 'Wie lautet Ihr 2FA-Code?',
Without_Servers: 'Ohne Server',
Workspaces: 'Arbeitsbereiche',
Would_you_like_to_return_the_inquiry: 'Willst du zur Anfrage zurück?',
Write_External_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihre Galerie um Bilder speichern zu können.',
Write_External_Permission: 'Galerie-Zugriff',
Yes: 'Ja',
Yes_action_it: 'Ja, {{action}}!',
Yesterday: 'Gestern',
You_are_in_preview_mode: 'Sie befinden sich im Vorschaumodus',
@ -495,11 +557,13 @@ export default {
You: 'Sie',
Logged_out_by_server: 'Du bist vom Server abgemeldet worden. Bitte melde dich wieder an.',
You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Sie benötigen Zugang zu mindestens einem Rocket.Chat-Server um etwas zu teilen.',
Your_certificate: 'Ihr Zertifikat',
Your_certificate: 'Dein Zertifikat',
Your_message: 'Deine Nachricht',
Your_invite_link_will_expire_after__usesLeft__uses: 'Dein Einladungs-Link wird nach {{usesLeft}} Benutzungen ablaufen.',
Your_invite_link_will_expire_on__date__or_after__usesLeft__uses: 'Dein Einladungs-Link wird am {{date}} oder nach {{usesLeft}} Benutzungen ablaufen.',
Your_invite_link_will_expire_on__date__: 'Dein Einladungs-Link wird am {{date}} ablaufen.',
Your_invite_link_will_never_expire: 'Dein Einladungs-Link wird niemals ablaufen.',
Your_workspace: 'Dein Arbeitsbereich',
Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!',
Change_Language: 'Sprache ändern',
@ -521,5 +585,30 @@ export default {
You_will_be_logged_out_of_this_application: 'Du wirst in dieser Anwendung vom Server abgemeldet.',
Clear: 'Löschen',
This_will_clear_all_your_offline_data: 'Dies wird deine Offline-Daten löschen.',
Mark_unread: 'Als ungelesen markieren'
This_will_remove_all_data_from_this_server: 'Dies wird alle Daten von diesem Server löschen.',
Mark_unread: 'Als ungelesen markieren',
Wait_activation_warning: 'Bevor du dich anmelden kannst, muss dein Konto durch einen Administrator freigeschaltet werden.',
Screen_lock: 'Zugriffs-Sperre',
Local_authentication_biometry_title: 'Authentifizieren',
Local_authentication_biometry_fallback: 'Sicherheitscode benutzen',
Local_authentication_unlock_option: 'Entsperren mit Sicherheitscode',
Local_authentication_change_passcode: 'Ändere Sicherheitscode',
Local_authentication_info: 'Anmerkung: Wenn du den Sicherheitscode vergisst, musst du diese App löschen und neu installieren.',
Local_authentication_facial_recognition: 'Gesichtserkennung',
Local_authentication_fingerprint: 'Fingerabdruck',
Local_authentication_unlock_with_label: 'Entsperren mit {{label}}',
Local_authentication_auto_lock_60: 'Nach 1 Minute',
Local_authentication_auto_lock_300: 'Nach 5 Minuten',
Local_authentication_auto_lock_900: 'Nach 15 Minuten',
Local_authentication_auto_lock_1800: 'Nach 30 Minuten',
Local_authentication_auto_lock_3600: 'Nach 1 Stunde',
Passcode_enter_title: 'Gib deinen Sicherheitscode ein',
Passcode_choose_title: 'Setze deinen neuen Sicherheitscode',
Passcode_choose_confirm_title: 'Bestätige deinen neuen Sicherheitscode',
Passcode_choose_error: 'Sicherheitscodes stimmen nicht überein. Probiere es noch einmal.',
Passcode_choose_force_set: 'Sicherheitscode wird vom Admin verlangt',
Passcode_app_locked_title: 'App gesperrt',
Passcode_app_locked_subtitle: 'Versuche es in {{timeLeft}} Sekunden noch einmal.',
After_seconds_set_by_admin: 'Nach {{seconds}} Sekunden (durch den Admin gesetzt)',
Dont_activate: 'Jetzt nicht aktivieren'
};

View File

@ -18,6 +18,7 @@ export default {
'error-email-domain-blacklisted': 'The email domain is blacklisted',
'error-email-send-failed': 'Error trying to send email: {{message}}',
'error-save-image': 'Error while saving image',
'error-save-video': 'Error while saving video',
'error-field-unavailable': '{{field}} is already in use :(',
'error-file-too-large': 'File is too large',
'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.',
@ -84,6 +85,7 @@ export default {
Add_Server: 'Add Server',
Add_users: 'Add users',
Admin_Panel: 'Admin Panel',
Agent: 'Agent',
Alert: 'Alert',
alert: 'alert',
alerts: 'alerts',
@ -132,7 +134,9 @@ export default {
Click_to_join: 'Click to Join!',
Close: 'Close',
Close_emoji_selector: 'Close emoji selector',
Closing_chat: 'Closing chat',
Change_language_loading: 'Changing language.',
Chat_closed_by_agent: 'Chat closed by agent',
Choose: 'Choose',
Choose_from_library: 'Choose from library',
Choose_file: 'Choose file',
@ -150,6 +154,7 @@ export default {
Continue_with: 'Continue with',
Copied_to_clipboard: 'Copied to clipboard!',
Copy: 'Copy',
Conversation: 'Conversation',
Permalink: 'Permalink',
Certificate_password: 'Certificate Password',
Clear_cache: 'Clear local server cache',
@ -168,6 +173,7 @@ export default {
Default: 'Default',
Default_browser: 'Default browser',
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
Department: 'Department',
delete: 'delete',
Delete: 'Delete',
DELETE: 'DELETE',
@ -195,6 +201,7 @@ export default {
Email: 'Email',
EMAIL: 'EMAIL',
email: 'e-mail',
Empty_title: 'Empty title',
Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel',
@ -211,6 +218,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
Forgot_password: 'Forgot your password?',
Forgot_Password: 'Forgot Password',
Forward: 'Forward',
Forward_Chat: 'Forward Chat',
Forward_to_department: 'Forward to department',
Forward_to_user: 'Forward to user',
Full_table: 'Click to see full table',
Generate_New_Link: 'Generate New Link',
Group_by_favorites: 'Group favorites',
@ -234,6 +245,7 @@ export default {
Message_HideType_subscription_role_removed: 'Role No Longer Defined',
Message_HideType_room_archived: 'Room Archived',
Message_HideType_room_unarchived: 'Room Unarchived',
IP: 'IP',
In_app: 'In-app',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop',
@ -259,6 +271,7 @@ export default {
Light: 'Light',
License: 'License',
Livechat: 'Livechat',
Livechat_edit: 'Livechat edit',
Login: 'Login',
Login_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with',
@ -291,6 +304,7 @@ export default {
N_users: '{{n}} users',
name: 'name',
Name: 'Name',
Navigation_history: 'Navigation history',
Never: 'Never',
New_Message: 'New Message',
New_Password: 'New Password',
@ -317,6 +331,7 @@ export default {
Notifications: 'Notifications',
Notification_Duration: 'Notification Duration',
Notification_Preferences: 'Notification Preferences',
No_available_agents_to_transfer: 'No available agents to transfer',
Offline: 'Offline',
Oops: 'Oops!',
Onboarding_description: 'A workspace is your team or organizations space to collaborate. Ask the workspace admin for address to join or create one for your team.',
@ -333,14 +348,17 @@ export default {
Open_Source_Communication: 'Open Source Communication',
Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
OR: 'OR',
OS: 'OS',
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
Password: 'Password',
Parent_channel_or_group: 'Parent channel or group',
Permalink_copied_to_clipboard: 'Permalink copied to clipboard!',
Phone: 'Phone',
Pin: 'Pin',
Pinned_Messages: 'Pinned Messages',
pinned: 'pinned',
Pinned: 'Pinned',
Please_add_a_comment: 'Please add a comment',
Please_enter_your_password: 'Please enter your password',
Please_wait: 'Please wait.',
Preferences: 'Preferences',
@ -379,6 +397,7 @@ export default {
Reset_password: 'Reset password',
resetting_password: 'resetting password',
RESET: 'RESET',
Return: 'Return',
Review_app_title: 'Are you enjoying this app?',
Review_app_desc: 'Give us 5 stars on {{store}}',
Review_app_yes: 'Sure!',
@ -400,6 +419,7 @@ export default {
SAVE: 'SAVE',
Save_Changes: 'Save Changes',
Save: 'Save',
Saved: 'Saved',
saving_preferences: 'saving preferences',
saving_profile: 'saving profile',
saving_settings: 'saving settings',
@ -414,7 +434,9 @@ export default {
Select_Server: 'Select Server',
Select_Users: 'Select Users',
Select_a_Channel: 'Select a Channel',
Select_a_Department: 'Select a Department',
Select_an_option: 'Select an option',
Select_a_User: 'Select a User',
Send: 'Send',
Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report',
@ -452,6 +474,7 @@ export default {
Started_call: 'Call started by {{userBy}}',
Submit: 'Submit',
Table: 'Table',
Tags: 'Tags',
Take_a_photo: 'Take a photo',
Take_a_video: 'Take a video',
tap_to_change_status: 'tap to change status',
@ -487,6 +510,7 @@ export default {
Updating: 'Updating...',
Uploading: 'Uploading',
Upload_file_question_mark: 'Upload file?',
User: 'User',
Users: 'Users',
User_added_by: 'User {{userAdded}} added by {{userBy}}',
User_Info: 'User Info',
@ -517,8 +541,10 @@ export default {
Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers',
Workspaces: 'Workspaces',
Would_you_like_to_return_the_inquiry: 'Would you like to return the inquiry?',
Write_External_Permission_Message: 'Rocket Chat needs access to your gallery so you can save images.',
Write_External_Permission: 'Gallery Permission',
Yes: 'Yes',
Yes_action_it: 'Yes, {{action}} it!',
Yesterday: 'Yesterday',
You_are_in_preview_mode: 'You are in preview mode',
@ -559,6 +585,30 @@ export default {
You_will_be_logged_out_of_this_application: 'You will be logged out of this application.',
Clear: 'Clear',
This_will_clear_all_your_offline_data: 'This will clear all your offline data.',
This_will_remove_all_data_from_this_server: 'This will remove all data from this server.',
Mark_unread: 'Mark Unread',
Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.'
Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.',
Screen_lock: 'Screen lock',
Local_authentication_biometry_title: 'Authenticate',
Local_authentication_biometry_fallback: 'Use passcode',
Local_authentication_unlock_option: 'Unlock with Passcode',
Local_authentication_change_passcode: 'Change Passcode',
Local_authentication_info: 'Note: if you forget the Passcode, you\'ll need to delete and reinstall the app.',
Local_authentication_facial_recognition: 'facial recognition',
Local_authentication_fingerprint: 'fingerprint',
Local_authentication_unlock_with_label: 'Unlock with {{label}}',
Local_authentication_auto_lock_60: 'After 1 minute',
Local_authentication_auto_lock_300: 'After 5 minutes',
Local_authentication_auto_lock_900: 'After 15 minutes',
Local_authentication_auto_lock_1800: 'After 30 minutes',
Local_authentication_auto_lock_3600: 'After 1 hour',
Passcode_enter_title: 'Enter your passcode',
Passcode_choose_title: 'Choose your new passcode',
Passcode_choose_confirm_title: 'Confirm your new passcode',
Passcode_choose_error: 'Passcodes don\'t match. Try again.',
Passcode_choose_force_set: 'Passcode required by admin',
Passcode_app_locked_title: 'App locked',
Passcode_app_locked_subtitle: 'Try again in {{timeLeft}} seconds',
After_seconds_set_by_admin: 'After {{seconds}} seconds (set by admin)',
Dont_activate: 'Don\'t activate now'
};

View File

@ -89,6 +89,7 @@ export default {
Add_Reaction: 'Reagir',
Add_Server: 'Adicionar servidor',
Add_users: 'Adicionar usuário',
Agent: 'Agente',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',
@ -135,7 +136,9 @@ export default {
Click_to_join: 'Clique para participar!',
Close: 'Fechar',
Close_emoji_selector: 'Fechar seletor de emojis',
Closing_chat: 'Fechando conversa',
Choose: 'Escolher',
Chat_closed_by_agent: 'Conversa fechada por agente',
Choose_from_library: 'Escolha da biblioteca',
Choose_file: 'Enviar arquivo',
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
@ -145,6 +148,7 @@ export default {
Confirm: 'Confirmar',
Connect: 'Conectar',
Connected: 'Conectado',
Conversation: 'Conversação',
connecting_server: 'conectando no servidor',
Connecting: 'Conectando...',
Continue_with: 'Entrar com',
@ -187,6 +191,7 @@ export default {
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
Empty_title: 'Título vazio',
Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo',
@ -201,6 +206,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.',
Forgot_password: 'Esqueceu sua senha?',
Forgot_Password: 'Esqueci minha senha',
Forward: 'Encaminhar',
Forward_Chat: 'Encaminhar Conversa',
Forward_to_department: 'Encaminhar para departamento',
Forward_to_user: 'Encaminhar para usuário',
Full_table: 'Clique para ver a tabela completa',
Generate_New_Link: 'Gerar novo convite',
Group_by_favorites: 'Agrupar favoritos',
@ -223,6 +232,7 @@ export default {
Message_HideType_subscription_role_removed: 'Papel removido',
Message_HideType_room_archived: 'Sala arquivada',
Message_HideType_room_unarchived: 'Sala desarquivada',
IP: 'IP',
In_app: 'No app',
Invisible: 'Invisível',
Invite: 'Convidar',
@ -269,6 +279,7 @@ export default {
N_users: '{{n}} usuários',
name: 'nome',
Name: 'Nome',
Navigation_history: 'Histórico de navegação',
Never: 'Nunca',
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
New_Message: 'Nova Mensagem',
@ -289,6 +300,7 @@ export default {
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
Notify_all_in_this_room: 'Notificar todos nesta sala',
Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}',
No_available_agents_to_transfer: 'Nenhum agente disponível para transferência',
Offline: 'Offline',
Oops: 'Ops!',
Onboarding_description: 'Workspace é o espaço de colaboração do seu time ou organização. Peça um convite ou o endereço ao seu administrador ou crie uma workspace para o seu time.',
@ -305,6 +317,7 @@ export default {
Open_Source_Communication: 'Comunicação Open Source',
Open_your_authentication_app_and_enter_the_code: 'Abra seu aplicativo de autenticação e digite o código.',
OR: 'OU',
OS: 'SO',
Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
Password: 'Senha',
Parent_channel_or_group: 'Canal ou grupo pai',
@ -315,6 +328,7 @@ export default {
Pinned: 'Mensagens Fixadas',
Please_wait: 'Por favor, aguarde.',
Please_enter_your_password: 'Por favor, digite sua senha',
Please_add_a_comment: 'Por favor, adicione um comentário',
Preferences: 'Preferências',
Preferences_saved: 'Preferências salvas!',
Privacy_Policy: ' Política de Privacidade',
@ -343,6 +357,7 @@ export default {
Reset_password: 'Resetar senha',
resetting_password: 'redefinindo senha',
RESET: 'RESETAR',
Return: 'Retornar',
Review_app_title: 'Você está gostando do app?',
Review_app_desc: 'Nos dê 5 estrelas na {{store}}',
Review_app_yes: 'Claro!',
@ -377,7 +392,9 @@ export default {
Select_Server: 'Selecionar Servidor',
Select_Users: 'Selecionar Usuários',
Select_a_Channel: 'Selecione um canal',
Select_a_Department: 'Selecione um Departamento',
Select_an_option: 'Selecione uma opção',
Select_a_User: 'Selecione um Usuário',
Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem',
@ -436,6 +453,7 @@ export default {
Updating: 'Atualizando...',
Uploading: 'Subindo arquivo',
Upload_file_question_mark: 'Enviar arquivo?',
User: 'Usuário',
Users: 'Usuários',
User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}',
User_has_been_key: 'Usuário foi {{key}}!',
@ -479,8 +497,10 @@ export default {
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
Your_workspace: 'Sua workspace',
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
Would_you_like_to_return_the_inquiry: 'Deseja retornar a consulta?',
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
Write_External_Permission: 'Acesso à Galeria',
Yes: 'Sim',
Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.',
Type_message: 'Digitar mensagem',
Room_search: 'Busca de sala',
@ -499,6 +519,30 @@ export default {
You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.',
Clear: 'Limpar',
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.',
This_will_remove_all_data_from_this_server: 'Isto removerá todos os dados desse servidor.',
Mark_unread: 'Marcar como não Lida',
Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.'
Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.',
Screen_lock: 'Bloqueio de Tela',
Local_authentication_biometry_title: 'Autenticar',
Local_authentication_biometry_fallback: 'Usar senha',
Local_authentication_unlock_option: 'Desbloquear com senha',
Local_authentication_change_passcode: 'Alterar senha',
Local_authentication_info: 'Nota: se você esquecer sua senha, terá de apagar e reinstalar o app.',
Local_authentication_facial_recognition: 'reconhecimento facial',
Local_authentication_fingerprint: 'impressão digital',
Local_authentication_unlock_with_label: 'Desbloquear com {{label}}',
Local_authentication_auto_lock_60: 'Após 1 minuto',
Local_authentication_auto_lock_300: 'Após 5 minutos',
Local_authentication_auto_lock_900: 'Após 15 minutos',
Local_authentication_auto_lock_1800: 'Após 30 minutos',
Local_authentication_auto_lock_3600: 'Após 1 hora',
Passcode_enter_title: 'Digite sua senha',
Passcode_choose_title: 'Insira sua nova senha',
Passcode_choose_confirm_title: 'Confirme sua nova senha',
Passcode_choose_error: 'As senhas não coincidem. Tente novamente.',
Passcode_choose_force_set: 'Senha foi exigida pelo admin',
Passcode_app_locked_title: 'Aplicativo bloqueado',
Passcode_app_locked_subtitle: 'Tente novamente em {{timeLeft}} segundos',
After_seconds_set_by_admin: 'Após {{seconds}} segundos (Configurado pelo adm)',
Dont_activate: 'Não ativar agora'
};

View File

@ -46,6 +46,8 @@ import TwoFactor from './containers/TwoFactor';
import RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView';
import ScreenLockedView from './views/ScreenLockedView';
import ChangePasscodeView from './views/ChangePasscodeView';
if (isIOS) {
const RNScreens = require('react-native-screens');
@ -166,6 +168,15 @@ const ChatsStack = createStackNavigator({
NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default
},
VisitorNavigationView: {
getScreen: () => require('./views/VisitorNavigationView').default
},
ForwardLivechatView: {
getScreen: () => require('./views/ForwardLivechatView').default
},
LivechatEditView: {
getScreen: () => require('./views/LivechatEditView').default
},
PickerView: {
getScreen: () => require('./views/PickerView').default
},
@ -224,6 +235,9 @@ const SettingsStack = createStackNavigator({
},
DefaultBrowserView: {
getScreen: () => require('./views/DefaultBrowserView').default
},
ScreenLockConfigView: {
getScreen: () => require('./views/ScreenLockConfigView').default
}
}, {
defaultNavigationOptions: defaultHeader,
@ -514,7 +528,7 @@ class CustomModalStack extends React.Component {
const pageSheetViews = ['AttachmentView'];
const pageSheet = pageSheetViews.includes(getActiveRouteName(navigation.state));
const androidProps = isAndroid && {
const androidProps = isAndroid && !pageSheet && {
style: { marginBottom: 0 }
};
@ -524,7 +538,7 @@ class CustomModalStack extends React.Component {
</View>
);
if (isAndroid) {
if (isAndroid && !pageSheet) {
content = (
<ScrollView overScrollMode='never'>
{content}
@ -729,6 +743,8 @@ export default class Root extends React.Component {
>
{content}
<TwoFactor />
<ScreenLockedView />
<ChangePasscodeView />
</ThemeContext.Provider>
</Provider>
</AppearanceProvider>

View File

@ -0,0 +1,35 @@
// https://github.com/bamlab/redux-enhancer-react-native-appstate
import { AppState } from 'react-native';
import { APP_STATE } from '../actions/actionsTypes';
export default () => createStore => (...args) => {
const store = createStore(...args);
let currentState = '';
const handleAppStateChange = (nextAppState) => {
if (nextAppState !== 'inactive') {
if (currentState !== nextAppState) {
let type;
if (nextAppState === 'active') {
type = APP_STATE.FOREGROUND;
} else if (nextAppState === 'background') {
type = APP_STATE.BACKGROUND;
}
if (type) {
store.dispatch({
type
});
}
}
currentState = nextAppState;
}
};
AppState.addEventListener('change', handleAppStateChange);
// setTimeout to allow redux-saga to catch the initial state fired by redux-enhancer-react-native-appstate library
setTimeout(() => handleAppStateChange(AppState.currentState));
return store;
};

View File

@ -1,9 +1,9 @@
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import applyAppStateListener from 'redux-enhancer-react-native-appstate';
import reducers from '../reducers';
import sagas from '../sagas';
import applyAppStateMiddleware from './appStateMiddleware';
let sagaMiddleware;
let enhancers;
@ -16,7 +16,7 @@ if (__DEV__) {
});
enhancers = compose(
applyAppStateListener(),
applyAppStateMiddleware(),
applyMiddleware(reduxImmutableStateInvariant),
applyMiddleware(sagaMiddleware),
Reactotron.createEnhancer()
@ -24,7 +24,7 @@ if (__DEV__) {
} else {
sagaMiddleware = createSagaMiddleware();
enhancers = compose(
applyAppStateListener(),
applyAppStateMiddleware(),
applyMiddleware(sagaMiddleware)
);
}

View File

@ -33,6 +33,36 @@ if (__DEV__ && isIOS) {
console.log(appGroupPath);
}
export const getDatabase = (database = '') => {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({
dbName,
schema: appSchema,
migrations
});
return new Database({
adapter,
modelClasses: [
Subscription,
Room,
Message,
Thread,
ThreadMessage,
CustomEmoji,
FrequentlyUsedEmoji,
Upload,
Setting,
Role,
Permission,
SlashCommand
],
actionsEnabled: true
});
};
class DB {
databases = {
serversDB: new Database({
@ -86,34 +116,8 @@ class DB {
});
}
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({
dbName,
schema: appSchema,
migrations
});
this.databases.activeDB = new Database({
adapter,
modelClasses: [
Subscription,
Room,
Message,
Thread,
ThreadMessage,
CustomEmoji,
FrequentlyUsedEmoji,
Upload,
Setting,
Role,
Permission,
SlashCommand
],
actionsEnabled: true
});
setActiveDB(database) {
this.databases.activeDB = getDatabase(database);
}
}

View File

@ -30,6 +30,8 @@ export default class Message extends Model {
@field('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;

View File

@ -13,4 +13,14 @@ export default class Room extends Model {
@field('encrypted') encrypted;
@field('ro') ro;
@json('v', sanitizer) v;
@json('served_by', sanitizer) servedBy;
@field('department_id') departmentId;
@json('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags;
}

View File

@ -17,4 +17,12 @@ export default class Server extends Model {
@date('rooms_updated_at') roomsUpdatedAt;
@field('version') version;
@date('last_local_authenticated_session') lastLocalAuthenticatedSession;
@field('auto_lock') autoLock;
@field('auto_lock_time') autoLockTime;
@field('biometry') biometry;
}

View File

@ -50,6 +50,8 @@ export default class Subscription extends Model {
@field('announcement') announcement;
@field('banner_closed') bannerClosed;
@field('topic') topic;
@field('blocked') blocked;
@ -95,4 +97,14 @@ export default class Subscription extends Model {
@json('uids', sanitizer) uids;
@json('usernames', sanitizer) usernames;
@json('visitor', sanitizer) visitor;
@field('department_id') departmentId;
@json('served_by', sanitizer) servedBy;
@json('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags;
}

View File

@ -30,6 +30,8 @@ export default class Thread extends Model {
@field('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;

View File

@ -32,6 +32,8 @@ export default class ThreadMessage extends Model {
@field('avatar') avatar;
@field('emoji') emoji;
@json('attachments', sanitizer) attachments;
@json('urls', sanitizer) urls;

View File

@ -74,6 +74,50 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 8,
steps: [
addColumns({
table: 'messages',
columns: [
{ name: 'emoji', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'thread_messages',
columns: [
{ name: 'emoji', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'threads',
columns: [
{ name: 'emoji', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'subscriptions',
columns: [
{ name: 'banner_closed', type: 'boolean', isOptional: true },
{ name: 'visitor', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'rooms',
columns: [
{ name: 'v', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
})
]
}
]
});

View File

@ -12,6 +12,20 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 4,
steps: [
addColumns({
table: 'servers',
columns: [
{ name: 'last_local_authenticated_session', type: 'number', isOptional: true },
{ name: 'auto_lock', type: 'boolean', isOptional: true },
{ name: 'auto_lock_time', type: 'number', isOptional: true },
{ name: 'biometry', type: 'boolean', isOptional: true }
]
})
]
}
]
});

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 7,
version: 8,
tables: [
tableSchema({
name: 'subscriptions',
@ -25,6 +25,7 @@ export default appSchema({
{ name: 'last_message', type: 'string', isOptional: true },
{ name: 'description', type: 'string', isOptional: true },
{ name: 'announcement', type: 'string', isOptional: true },
{ name: 'banner_closed', type: 'boolean', isOptional: true },
{ name: 'topic', type: 'string', isOptional: true },
{ name: 'blocked', type: 'boolean', isOptional: true },
{ name: 'blocker', type: 'boolean', isOptional: true },
@ -42,7 +43,12 @@ export default appSchema({
{ name: 'hide_unread_status', type: 'boolean', isOptional: true },
{ name: 'sys_mes', type: 'string', isOptional: true },
{ name: 'uids', type: 'string', isOptional: true },
{ name: 'usernames', type: 'string', isOptional: true }
{ name: 'usernames', type: 'string', isOptional: true },
{ name: 'visitor', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
}),
tableSchema({
@ -51,7 +57,12 @@ export default appSchema({
{ name: 'custom_fields', type: 'string' },
{ name: 'broadcast', type: 'boolean' },
{ name: 'encrypted', type: 'boolean' },
{ name: 'ro', type: 'boolean' }
{ name: 'ro', type: 'boolean' },
{ name: 'v', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
}),
tableSchema({
@ -66,6 +77,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string' },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: '_updated_at', type: 'number' },
@ -104,6 +116,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true },
@ -140,6 +153,7 @@ export default appSchema({
{ name: 'parse_urls', type: 'string', isOptional: true },
{ name: 'groupable', type: 'boolean', isOptional: true },
{ name: 'avatar', type: 'string', isOptional: true },
{ name: 'emoji', type: 'string', isOptional: true },
{ name: 'attachments', type: 'string', isOptional: true },
{ name: 'urls', type: 'string', isOptional: true },
{ name: 'status', type: 'number', isOptional: true },

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 3,
version: 4,
tables: [
tableSchema({
name: 'users',
@ -24,7 +24,11 @@ export default appSchema({
{ name: 'file_upload_media_type_white_list', type: 'string', isOptional: true },
{ name: 'file_upload_max_file_size', type: 'number', isOptional: true },
{ name: 'rooms_updated_at', type: 'number', isOptional: true },
{ name: 'version', type: 'string', isOptional: true }
{ name: 'version', type: 'string', isOptional: true },
{ name: 'last_local_authenticated_session', type: 'number', isOptional: true },
{ name: 'auto_lock', type: 'boolean', isOptional: true },
{ name: 'auto_lock_time', type: 'number', isOptional: true },
{ name: 'biometry', type: 'boolean', isOptional: true }
]
})
]

View File

@ -10,8 +10,9 @@ import log from '../../utils/log';
import database from '../database';
import protectedFunction from './helpers/protectedFunction';
import fetch from '../../utils/fetch';
import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication';
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize'];
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After'];
// these settings are used only on onboarding process
const loginSettings = [
@ -32,6 +33,8 @@ const loginSettings = [
const serverInfoUpdate = async(serverInfo, iconSetting) => {
const serversDB = database.servers;
const serverId = reduxStore.getState().server.server;
const serversCollection = serversDB.collections.get('servers');
const server = await serversCollection.find(serverId);
let info = serverInfo.reduce((allSettings, setting) => {
if (setting._id === 'Site_Name') {
@ -46,6 +49,23 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
if (setting._id === 'FileUpload_MaxFileSize') {
return { ...allSettings, FileUpload_MaxFileSize: setting.valueAsNumber };
}
if (setting._id === 'Force_Screen_Lock') {
// if this was disabled on server side we must keep this enabled on app
const autoLock = server.autoLock || setting.valueAsBoolean;
return { ...allSettings, autoLock };
}
if (setting._id === 'Force_Screen_Lock_After') {
const forceScreenLock = serverInfo.find(s => s._id === 'Force_Screen_Lock')?.valueAsBoolean;
// if Force_Screen_Lock_After === 0 and autoLockTime is null, set app's default value
if (setting.valueAsNumber === 0 && !server.autoLockTime) {
return { ...allSettings, autoLockTime: DEFAULT_AUTO_LOCK };
}
// if Force_Screen_Lock_After > 0 and forceScreenLock is enabled, use it
if (setting.valueAsNumber > 0 && forceScreenLock) {
return { ...allSettings, autoLockTime: setting.valueAsNumber };
}
}
return allSettings;
}, {});
@ -56,9 +76,6 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
await serversDB.action(async() => {
try {
const serversCollection = serversDB.collections.get('servers');
const server = await serversCollection.find(serverId);
await server.update((record) => {
Object.assign(record, info);
});

View File

@ -27,6 +27,7 @@ export default async(subscriptions = [], rooms = []) => {
lastOpen: s.lastOpen,
description: s.description,
announcement: s.announcement,
bannerClosed: s.bannerClosed,
topic: s.topic,
blocked: s.blocked,
blocker: s.blocker,
@ -43,7 +44,12 @@ export default async(subscriptions = [], rooms = []) => {
autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage,
usernames: s.usernames,
uids: s.uids
uids: s.uids,
visitor: s.visitor,
departmentId: s.departmentId,
servedBy: s.servedBy,
livechatData: s.livechatData,
tags: s.tags
}));
subscriptions = subscriptions.concat(existingSubs);
@ -64,7 +70,12 @@ export default async(subscriptions = [], rooms = []) => {
ro: r.ro,
broadcast: r.broadcast,
muted: r.muted,
sysMes: r.sysMes
sysMes: r.sysMes,
v: r.v,
departmentId: r.departmentId,
servedBy: r.servedBy,
livechatData: r.livechatData,
tags: r.tags
}));
rooms = rooms.concat(existingRooms);
} catch {

View File

@ -35,6 +35,21 @@ export const merge = (subscription, room) => {
} else {
subscription.muted = [];
}
if (room.v) {
subscription.visitor = room.v;
}
if (room.departmentId) {
subscription.departmentId = room.departmentId;
}
if (room.servedBy) {
subscription.servedBy = room.servedBy;
}
if (room.livechatData) {
subscription.livechatData = room.livechatData;
}
if (room.tags) {
subscription.tags = room.tags;
}
subscription.sysMes = room.sysMes;
}

127
app/lib/methods/logout.js Normal file
View File

@ -0,0 +1,127 @@
import RNUserDefaults from 'rn-user-defaults';
import * as FileSystem from 'expo-file-system';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import { SERVERS, SERVER_URL } from '../../constants/userDefaults';
import { getDeviceToken } from '../../notifications/push';
import { extractHostname } from '../../utils/server';
import { BASIC_AUTH_KEY } from '../../utils/fetch';
import database, { getDatabase } from '../database';
import RocketChat from '../rocketchat';
import { useSsl } from '../../utils/url';
async function removeServerKeys({ server, userId }) {
await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ server }`);
await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ userId }`);
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
}
async function removeSharedCredentials({ server }) {
try {
const servers = await RNUserDefaults.objectForKey(SERVERS);
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
// clear certificate for server - SSL Pinning
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
if (certificate && certificate.path) {
await RNUserDefaults.clear(extractHostname(server));
await FileSystem.deleteAsync(certificate.path);
}
} catch (e) {
console.log('removeSharedCredentials', e);
}
}
async function removeServerData({ server }) {
try {
const batch = [];
const serversDB = database.servers;
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
const usersCollection = serversDB.collections.get('users');
if (userId) {
const userRecord = await usersCollection.find(userId);
batch.push(userRecord.prepareDestroyPermanently());
}
const serverCollection = serversDB.collections.get('servers');
const serverRecord = await serverCollection.find(server);
batch.push(serverRecord.prepareDestroyPermanently());
await serversDB.action(() => serversDB.batch(...batch));
await removeSharedCredentials({ server });
await removeServerKeys({ server });
} catch (e) {
console.log('removeServerData', e);
}
}
async function removeCurrentServer() {
await RNUserDefaults.clear('currentServer');
await RNUserDefaults.clear(RocketChat.TOKEN_KEY);
}
async function removeServerDatabase({ server }) {
try {
const db = getDatabase(server);
await db.action(() => db.unsafeResetDatabase());
} catch (e) {
console.log(e);
}
}
export async function removeServer({ server }) {
try {
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
if (userId) {
const resume = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ userId }`);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
await sdk.login({ resume });
const token = getDeviceToken();
if (token) {
await sdk.del('push.token', { token });
}
await sdk.logout();
}
await removeServerData({ server });
await removeServerDatabase({ server });
} catch (e) {
console.log('removePush', e);
}
}
export default async function logout({ server }) {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
try {
await this.removePushToken();
} catch (e) {
console.log('removePushToken', e);
}
try {
// RC 0.60.0
await this.sdk.logout();
} catch (e) {
console.log('logout', e);
}
if (this.sdk) {
this.sdk = null;
}
await removeServerData({ server });
await removeCurrentServer();
await removeServerDatabase({ server });
}

View File

@ -62,7 +62,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) {
formData.append('file', {
uri: fileInfo.path,
type: fileInfo.type,
name: fileInfo.name || 'fileMessage'
name: encodeURI(fileInfo.name) || 'fileMessage'
});
if (fileInfo.description) {

View File

@ -10,6 +10,7 @@ import reduxStore from '../../createStore';
import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping';
import debounce from '../../../utils/debounce';
import RocketChat from '../../rocketchat';
import { subscribeRoom, unsubscribeRoom } from '../../../actions/room';
const WINDOW_TIME = 1000;
@ -38,6 +39,8 @@ export default class RoomSubscription {
if (!this.isAlive) {
this.unsubscribe();
}
reduxStore.dispatch(subscribeRoom(this.rid));
}
unsubscribe = async() => {
@ -59,6 +62,8 @@ export default class RoomSubscription {
if (this.timer) {
clearTimeout(this.timer);
}
reduxStore.dispatch(unsubscribeRoom(this.rid));
}
removeListener = async(promise) => {
@ -155,22 +160,17 @@ export default class RoomSubscription {
const msgCollection = db.collections.get('messages');
const threadsCollection = db.collections.get('threads');
const threadMessagesCollection = db.collections.get('thread_messages');
let messageRecord;
let threadRecord;
let threadMessageRecord;
// Create or update message
try {
messageRecord = await msgCollection.find(message._id);
} catch (error) {
// Do nothing
}
if (messageRecord) {
const update = messageRecord.prepareUpdate((m) => {
Object.assign(m, message);
});
this._messagesBatch[message._id] = update;
} else {
const messageRecord = await msgCollection.find(message._id);
if (!messageRecord._hasPendingUpdate) {
const update = messageRecord.prepareUpdate(protectedFunction((m) => {
Object.assign(m, message);
}));
this._messagesBatch[message._id] = update;
}
} catch {
const create = msgCollection.prepareCreate(protectedFunction((m) => {
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
m.subscription.id = this.rid;
@ -182,17 +182,14 @@ export default class RoomSubscription {
// Create or update thread
if (message.tlm) {
try {
threadRecord = await threadsCollection.find(message._id);
} catch (error) {
// Do nothing
}
if (threadRecord) {
const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => {
Object.assign(t, message);
}));
this._threadsBatch[message._id] = updateThread;
} else {
const threadRecord = await threadsCollection.find(message._id);
if (!threadRecord._hasPendingUpdate) {
const updateThread = threadRecord.prepareUpdate(protectedFunction((t) => {
Object.assign(t, message);
}));
this._threadsBatch[message._id] = updateThread;
}
} catch {
const createThread = threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema);
t.subscription.id = this.rid;
@ -205,19 +202,16 @@ export default class RoomSubscription {
// Create or update thread message
if (message.tmid) {
try {
threadMessageRecord = await threadMessagesCollection.find(message._id);
} catch (error) {
// Do nothing
}
if (threadMessageRecord) {
const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, message);
tm.rid = message.tmid;
delete tm.tmid;
}));
this._threadMessagesBatch[message._id] = updateThreadMessage;
} else {
const threadMessageRecord = await threadMessagesCollection.find(message._id);
if (!threadMessageRecord._hasPendingUpdate) {
const updateThreadMessage = threadMessageRecord.prepareUpdate(protectedFunction((tm) => {
Object.assign(tm, message);
tm.rid = message.tmid;
delete tm.tmid;
}));
this._threadMessagesBatch[message._id] = updateThreadMessage;
}
} catch {
const createThreadMessage = threadMessagesCollection.prepareCreate(protectedFunction((tm) => {
tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema);
Object.assign(tm, message);

View File

@ -57,6 +57,7 @@ const createOrUpdateSubscription = async(subscription, room) => {
lastOpen: s.lastOpen,
description: s.description,
announcement: s.announcement,
bannerClosed: s.bannerClosed,
topic: s.topic,
blocked: s.blocked,
blocker: s.blocker,
@ -74,7 +75,12 @@ const createOrUpdateSubscription = async(subscription, room) => {
lastMessage: s.lastMessage,
roles: s.roles,
usernames: s.usernames,
uids: s.uids
uids: s.uids,
visitor: s.visitor,
departmentId: s.departmentId,
servedBy: s.servedBy,
livechatData: s.livechatData,
tags: s.tags
};
} catch (error) {
try {
@ -97,10 +103,15 @@ const createOrUpdateSubscription = async(subscription, room) => {
// We have to create a plain obj so we can manipulate it on `merge`
// Can we do it in a better way?
room = {
customFields: r.customFields,
broadcast: r.broadcast,
v: r.v,
ro: r.ro,
tags: r.tags,
servedBy: r.servedBy,
encrypted: r.encrypted,
ro: r.ro
broadcast: r.broadcast,
customFields: r.customFields,
departmentId: r.departmentId,
livechatData: r.livechatData
};
} catch (error) {
// Do nothing
@ -121,6 +132,11 @@ const createOrUpdateSubscription = async(subscription, room) => {
try {
const update = sub.prepareUpdate((s) => {
Object.assign(s, tmp);
if (subscription.announcement) {
if (subscription.announcement !== sub.announcement) {
s.bannerClosed = false;
}
}
});
batch.push(update);
} catch (e) {
@ -141,7 +157,8 @@ const createOrUpdateSubscription = async(subscription, room) => {
}
}
if (tmp.lastMessage) {
const { rooms } = store.getState().room;
if (tmp.lastMessage && !rooms.includes(tmp.rid)) {
const lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.collections.get('messages');
let messageRecord;

View File

@ -1,9 +1,9 @@
import { AsyncStorage, InteractionManager } from 'react-native';
import { InteractionManager } from 'react-native';
import semver from 'semver';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import RNUserDefaults from 'rn-user-defaults';
import { Q } from '@nozbe/watermelondb';
import * as FileSystem from 'expo-file-system';
import AsyncStorage from '@react-native-community/async-storage';
import reduxStore from './createStore';
import defaultSettings from '../constants/settings';
@ -11,8 +11,7 @@ import messagesStatus from '../constants/messagesStatus';
import database from './database';
import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo';
import { extractHostname } from '../utils/server';
import fetch, { BASIC_AUTH_KEY } from '../utils/fetch';
import fetch from '../utils/fetch';
import { setUser, setLoginServices, loginRequest } from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
@ -43,12 +42,14 @@ import sendMessage, { sendMessageCall } from './methods/sendMessage';
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
import callJitsi from './methods/callJitsi';
import logout, { removeServer } from './methods/logout';
import { getDeviceToken } from '../notifications/push';
import { SERVERS, SERVER_URL } from '../constants/userDefaults';
import { setActiveUsers } from '../actions/activeUsers';
import I18n from '../i18n';
import { twoFactor } from '../utils/twoFactor';
import { selectServerFailure } from '../actions/server';
import { useSsl } from '../utils/url';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@ -86,10 +87,7 @@ const RocketChat = {
}
},
async getWebsocketInfo({ server }) {
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
try {
await sdk.connect();
@ -146,6 +144,10 @@ const RocketChat = {
}
return result;
} catch (e) {
if (e.message === 'Aborted') {
reduxStore.dispatch(selectServerFailure());
throw e;
}
log(e);
}
return {
@ -159,6 +161,16 @@ const RocketChat = {
stopListener(listener) {
return listener && listener.stop();
},
// Abort all requests and create a new AbortController
abort() {
if (this.controller) {
this.controller.abort();
if (this.sdk) {
this.sdk.abort();
}
}
this.controller = new AbortController();
},
connect({ server, user, logoutOnError = false }) {
return new Promise((resolve) => {
if (!this.sdk || this.sdk.client.host !== server) {
@ -200,15 +212,13 @@ const RocketChat = {
this.code = null;
}
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
this.getSettings();
const sdkConnect = () => this.sdk.connect()
.then(() => {
if (user && user.token) {
const { server: currentServer } = reduxStore.getState().server;
if (user && user.token && server === currentServer) {
reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError));
}
})
@ -217,7 +227,9 @@ const RocketChat = {
// when `connect` raises an error, we try again in 10 seconds
this.connectTimeout = setTimeout(() => {
sdkConnect();
if (this.sdk?.client?.host === server) {
sdkConnect();
}
}, 10000);
});
@ -270,10 +282,7 @@ const RocketChat = {
this.shareSDK = null;
}
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
// set Server
const serversDB = database.servers;
@ -306,7 +315,7 @@ const RocketChat = {
}
database.share = null;
reduxStore.dispatch(shareSetUser(null));
reduxStore.dispatch(shareSetUser({}));
},
updateJitsiTimeout(rid) {
@ -369,112 +378,35 @@ const RocketChat = {
};
}
try {
return this.loginTOTP(params);
} catch (error) {
throw error;
}
return this.loginTOTP(params);
},
async loginOAuthOrSso(params) {
try {
const result = await this.login(params);
reduxStore.dispatch(loginRequest({ resume: result.token }));
} catch (error) {
throw error;
}
const result = await this.login(params);
reduxStore.dispatch(loginRequest({ resume: result.token }));
},
async login(params) {
try {
const sdk = this.shareSDK || this.sdk;
// RC 0.64.0
await sdk.login(params);
const { result } = sdk.currentLogin;
const user = {
id: result.userId,
token: result.authToken,
username: result.me.username,
name: result.me.name,
language: result.me.language,
status: result.me.status,
statusText: result.me.statusText,
customFields: result.me.customFields,
emails: result.me.emails,
roles: result.me.roles
};
return user;
} catch (e) {
throw e;
}
},
async logout({ server }) {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
try {
await this.removePushToken();
} catch (error) {
console.log('logout -> removePushToken -> catch -> error', error);
}
try {
// RC 0.60.0
await this.sdk.logout();
} catch (error) {
console.log('logout -> api logout -> catch -> error', error);
}
this.sdk = null;
try {
const servers = await RNUserDefaults.objectForKey(SERVERS);
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
// clear certificate for server - SSL Pinning
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
if (certificate && certificate.path) {
await RNUserDefaults.clear(extractHostname(server));
await FileSystem.deleteAsync(certificate.path);
}
} catch (error) {
console.log('logout_rn_user_defaults', error);
}
const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`);
try {
const serversDB = database.servers;
await serversDB.action(async() => {
const usersCollection = serversDB.collections.get('users');
const userRecord = await usersCollection.find(userId);
const serverCollection = serversDB.collections.get('servers');
const serverRecord = await serverCollection.find(server);
await serversDB.batch(
userRecord.prepareDestroyPermanently(),
serverRecord.prepareDestroyPermanently()
);
});
} catch (error) {
// Do nothing
}
await RNUserDefaults.clear('currentServer');
await RNUserDefaults.clear(TOKEN_KEY);
await RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`);
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
try {
const db = database.active;
await db.action(() => db.unsafeResetDatabase());
} catch (error) {
console.log(error);
}
const sdk = this.shareSDK || this.sdk;
// RC 0.64.0
await sdk.login(params);
const { result } = sdk.currentLogin;
const user = {
id: result.userId,
token: result.authToken,
username: result.me.username,
name: result.me.name,
language: result.me.language,
status: result.me.status,
statusText: result.me.statusText,
customFields: result.me.customFields,
emails: result.me.emails,
roles: result.me.roles
};
return user;
},
logout,
removeServer,
async clearCache({ server }) {
try {
const serversDB = database.servers;
@ -573,9 +505,9 @@ const RocketChat = {
).fetch();
if (filterUsers && !filterRooms) {
data = data.filter(item => item.t === 'd');
data = data.filter(item => item.t === 'd' && !RocketChat.isGroupChat(item));
} else if (!filterUsers && filterRooms) {
data = data.filter(item => item.t !== 'd');
data = data.filter(item => item.t !== 'd' || RocketChat.isGroupChat(item));
}
data = data.slice(0, 7);
@ -824,6 +756,59 @@ const RocketChat = {
return this.sdk.get('rooms.info', { roomId });
},
getVisitorInfo(visitorId) {
// RC 2.3.0
return this.sdk.get('livechat/visitors.info', { visitorId });
},
closeLivechat(rid, comment) {
// RC 0.29.0
return this.methodCall('livechat:closeRoom', rid, comment, { clientAction: true });
},
editLivechat(userData, roomData) {
// RC 0.55.0
return this.methodCall('livechat:saveInfo', userData, roomData);
},
returnLivechat(rid) {
// RC 0.72.0
return this.methodCall('livechat:returnAsInquiry', rid);
},
forwardLivechat(transferData) {
// RC 0.36.0
return this.methodCall('livechat:transfer', transferData);
},
getPagesLivechat(rid, offset) {
// RC 2.3.0
return this.sdk.get(`livechat/visitors.pagesVisited/${ rid }?count=50&offset=${ offset }`);
},
getDepartmentInfo(departmentId) {
// RC 2.2.0
return this.sdk.get(`livechat/department/${ departmentId }?includeAgents=false`);
},
getDepartments() {
// RC 2.2.0
return this.sdk.get('livechat/department');
},
usersAutoComplete(selector) {
// RC 2.4.0
return this.sdk.get('users.autocomplete', { selector });
},
getRoutingConfig() {
// RC 2.0.0
return this.methodCall('livechat:getRoutingConfig');
},
getTagsList() {
// RC 2.0.0
return this.methodCall('livechat:getTagsList');
},
getAgentDepartments(uid) {
// RC 2.4.0
return this.sdk.get(`livechat/agents/${ uid }/departments`);
},
getCustomFields() {
// RC 2.2.0
return this.sdk.get('livechat/custom-fields');
},
getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user;
@ -909,7 +894,7 @@ const RocketChat = {
methodCall(...args) {
return new Promise(async(resolve, reject) => {
try {
const result = await this.sdk.methodCall(...args, this.code);
const result = await this.sdk.methodCall(...args, this.code || '');
return resolve(result);
} catch (e) {
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
@ -977,7 +962,7 @@ const RocketChat = {
const shareUser = reduxStore.getState().share.user;
const loginUser = reduxStore.getState().login.user;
// get user roles on the server from redux
const userRoles = (shareUser.roles || loginUser.roles) || [];
const userRoles = (shareUser?.roles || loginUser?.roles) || [];
// merge both roles
const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
@ -1226,16 +1211,19 @@ const RocketChat = {
return this.methodCall('autoTranslate.translateMessage', message, targetLanguage);
},
getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings;
const { UI_Use_Real_Name: useRealName, UI_Allow_room_names_with_special_chars: allowSpecialChars } = reduxStore.getState().settings;
const { username } = reduxStore.getState().login.user;
if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) {
return room.usernames.filter(u => u !== username).sort((u1, u2) => u1.localeCompare(u2)).join(', ');
}
if (allowSpecialChars && room.t !== 'd') {
return room.fname || room.name;
}
return ((room.prid || useRealName) && room.fname) || room.name;
},
getRoomAvatar(room) {
if (RocketChat.isGroupChat(room)) {
return room.uids.length + room.usernames.join();
return room.uids?.length + room.usernames?.join();
}
return room.prid ? room.fname : room.name;
},

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