Merge 1.13.0 into Master (#936)

* fix last messages (#239)

* fix last messages

* Room actions (#231)

* Layout


* Empty starred list


* Favorite room

* Pinned messages

* fix last messages

* fix date on pinned messages

* fix package

* [NEW] OAuth (#241)

* Layout

* tmp

* test iscordova

* Webview redirecting

* Open and Close login actions

* Login services saved on redux

* OAuth Github

* Server regex fix

* OAuth modal style

* - Twitter login
- Remove services from redux
- Open login saga fix

* - Facebook login
- Fixed user agent
- Reactions fix
- Message url unique key fix

* Google login

* Email keyboard removed from messagebox

* - Login buttons refactored
- RoomList header

* Layout improvements

* Meteor login redirect_uri changed

* fix

* Random credentialToken state

* [NEW] Room actions: Mentioned messages and Room Members (#242)

* Mentioned messages

* Starred and pinned actions debounce

* Room members

* Open room on member touch

* [WIP] Improves (#245)

*  hotfix for ios

*  hotfix for ios

* Update config.yml

* Workaround for RN 0.54 on iOS (#246)

* Update iOS to RN 0.54 (#248)

* Update iOS to RN 0.54

* [WIP] Audio message functionality (#247)

* [NEW] Add module react-native-audio

* [WIP] Audio message basic UI

* [NEW] Record audio message

* Use cordova repository to get certificates

* Icon 1024

* [NEW] Room actions: block user, snippet messages, room files and leave room (#250)

* - Block user
- Load room members async
- fixed reactive change of room's read only flag

* Snippet messages

* - Room files
- Dismiss Video component on back button press
- Improvements on Image component

* Improvement on Video component

* Leave room

* Missing message types

* lint

* Reactotron working (#249)

* [NEW] Room info and Room info edit (#254)

* - Block user
- Load room members async
- fixed reactive change of room's read only flag

* Snippet messages

* - Room files
- Dismiss Video component on back button press
- Improvements on Image component

* Improvement on Video component

* Leave room

* Missing message types

* lint

* - Room info (read only)
- Missing message types

* Room info scroll

* - Tap on room header opens room info
- Layout tweaks

* - Room info edit
- iOS Toast fixed

* - Style not implemented actions as disabled

* Edit room permission

* - Save all room settings in a single call
- Implemented roomType and readOnly

* - Allow reacting when room is read only

* Message type added: room_changed_privacy

* Erase room

* Created TextInput and SwitchContainer components for reuse and readability

* - hasPermission method

* - Archive/Unarchive room
- Set Join Code

* Twitter keyboard type on iOS

* Archived room

* reactWhenReadOnly permission on message

* Active users refactored

* User roles

* - Subscribe to roles (in order to get role description info: e.g. 'core-team' to 'Rocket.Chat Team')
- Save roles to realm (for offline access)
- Save roles to redux (and get data from realm on app init)

* Lint

* code style

* password show/hide feature

* fix show/hide password

* password show/hide

* Crashlytics (#258)

* Fabric iOS

* Fabric configured on iOS and Android

* login tracked

* more logs

* fix reaction

* CI fix

* Bug fixes (#261)

* Layout fixes

* RoomsListView's SafeAreaView

* Unhandled promise rejection fix

* Prevent navigation from opening scenes twice

* Create channel fixes

* Create LICENSE

* Beta (#265)

* Fabric iOS

* Fabric configured on iOS and Android

* - react-native-fabric configured

- login tracked

* README updated

* Run scripts from README updated

* README scripts

* get rooms and messages by rest

* user status

* more improves

* more improves

* send pong on timeout

* fix some methods

* more tests

* rest messages

* Room actions (#266)

* Toggle notifications

* Search messages

* Invite users

* Mute/Unmute users in room

* rocket.cat messages

* Room topic layout fixed

* Starred messages loading onEndReached

* Room actions onEndReached

* Unnecessary login request

* Login loading

* Login services fixed

* User presence layout

* ïmproves on room actions view

* Removed unnecessary data from SelectedUsersView

* load few messages on open room, search message improve

* fix loading messages forever

* Removed state from search

* Custom message time format

* secureTextEntry layout

* Reduce android app size

* Roles subscription fix

* Public routes navigation

* fix reconnect

* - New login/register, login, register

* proguard

* Login flux

* App init/restore

* Android layout fixes

* Multiple meteor connection requests fixed

* Nested attachments

* Nested attachments

* fix check status

* New login layout (#269)

* Public routes navigation

* New login/register, login, register

* Multiple meteor connection requests fixed

* Nested attachments

* Button component

* TextInput android layout fixed

* Register fixed

* Thinner close modal button

* Requests /me after login only one time

* Static images moved

* fix reconnect

* fix ddp

* fix custom emoji

* New message layout (#273)

* Grouping messages

* Message layout

* Users typing animation

* Image  attachment layout

* Fabric and image fix (#284)

* Fixed images not showing

* Keyboard libs updated

* Fabric fix and location removed (#286)


* Proguard disabled

* message with list + links fixed (#288)

* Better image cache component (#292)

* react-native-img-cache removed

* Improve list render

* Support <http://link/Text> inside markdown

* Deep linking (#291)

* deep linking

* Basic deep link working

* Deep link routing

* Multiple servers working

* Send user to the room

* Avatar initials and room type icon (#298)

* Deep linking fix and more (#294)

* Fix - Any https link was deep linking to RocketChat

* Keyboard dismiss after add new server

* Room info bug fix

* Opacity animation

* Navigation when adding server fixed

* Throttle for unnecessary render on receiving several messages

* Search inputs without autocorrect and autocapitalize

* Search messages fixed

* Messagebox unnecessary render and spotlight fixed

* react-native-keyboard-input updated

* Lint

* Tests updated

* Update all dependencies (#299)

* Update react-navigation to the latest version 🚀 (#293)

* fix(package): update react-navigation to version 2.0.0

* Code updated to support breaking changes of react-navigation

* Detox tests E2E (#283)

* RoomsListView re-render (#304)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
- [x] Removed unnecessary re-renders on RoomsListView

* [NEW] Broadcast channels (#301)

* Broadcast channels

* e2e tests

* New markdown (#306)

Our current markdown is causing a lot of issues on Android devices, since it wraps everything inside a Text component.
On Android, Text doesn't support View as a child.
This PR adds react-native-markdown-renderer, that uses View as wrapper and may be better.

* Fixed audio recording issues (#310)

* Fix for "java.lang.IllegalArgumentException: unexpected url" (#313)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
User was able to add an invalid instance of Rocket.Chat by pressing submit button instead of "Connect" button.

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* I18n (#312)

* Unread and date separator layout improved (#319)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
- [x] Unread and date separator layout
- [x] "Start of conversation"/"Loading messages" label

![screen shot 2018-05-30 at 18 10 43](https://user-images.githubusercontent.com/804994/40747867-0424964a-6435-11e8-9293-31cc43c110ab.png)
![screen shot 2018-05-30 at 18 09 05](https://user-images.githubusercontent.com/804994/40747868-04484784-6435-11e8-8c31-92e0776276f0.png)



<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* [FIX] iOS Universal links (#318)

* [NEW] Drawer (#322)

* [FIX] invalid user muted value

* Ddp fixes (#324)

* [NEW] User Profile (#323)

* Drawer layout

* Drawer changes

* Profile

* Profile avatar

* Set language

* Tests

* Custom fields

* Readme updated

* fix invalid user muted value

* Fix for "Cannot add a child that doesn't have a YogaNode to a parent without a measure function! (Trying to add a 'RCTVirtualText' to a 'RCTView')"

* Settings/Permissions improvements (#325)

* Changed the way we read RocketChat settings since setting.type won't be returned from server anymore

* Permissions

* Unnecessary action sheet render

* Update gradle and targetSdkVersion (#328)

* Changed the way we read RocketChat settings since setting.type won't be returned from server anymore

* Permissions

* Unnecessary action sheet render

* Update gradle

* Switched testServer to use blob

* RoomsListHeader search fixed

* Runs loadMessagesForRoom only if room has at least 20 rows

* - Logout if user's token expired
- Removed update avatar logic
- Profile dialog border on android

* - Animations disabled
- CircleCI set

* Tests updated

* "eventType argument is required" fix

* Switch push notification lib (#346)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #342 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* Allow x-instance-id and X-Instance-ID header (#354)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #137 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
Some server configurations may send x-instance-id header with different case.

* Image upload improvements (#368)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
- [x] Crop image
- [x] Type image description (like web)
- [x] Show upload progress
- [x] "Try again" in case of error
- [x] Cancel upload while in progress
- [x] [Android] Zoom on photos

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
![image](https://user-images.githubusercontent.com/804994/42526934-a12da304-844d-11e8-8668-f3d69369726a.png)
![image](https://user-images.githubusercontent.com/804994/42527829-297945fe-8450-11e8-9f0e-9e668dd33043.png)

* [NEW] Room Loading(#372)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* [FIX] Empty room name for livechat (#375)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #320 
Closes #209 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* [NEW] Reply preview (#374)

* Updated to React Native 0.56

* Reply Preview

* [FIX] Close websocket (#379)

* Fixed a bug when closing websocket

* removeListener fixed

* [I18N] Russian translation (#381)

[I18N] Russian translation file

* [NEW] Icon (#383)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
![image](https://user-images.githubusercontent.com/804994/43228416-d8af49d6-9037-11e8-8830-a1803932c7fd.png)

* [FIX] Android 8 notifications (#382)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #380 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* Added CocoaPods to manage react-native-image-crop-picker (#373)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
react-native-image-crop-picker raised an error when uploading to TestFlight.
The lib highly recommends CocoaPods for production builds.

* Added single-server to readme (#390)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #386 
Closes #295 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* Improve RoomsList render time (#384)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
- [x] Added FlatList.getItemLayout() to improve list render time
- [x] Some texts were breaking lines at sidebar
- [x] Removed onPress from links at RoomsListView
- [x] Added eslint rule to prevent unused styles
- [x] Fixed auto focus bug at CreateChannel and NewServer
- [x] Fix change server bug
- [x] Fixed a bug when resuming in ListServer
- [x] I18n fixed
- [x] Fixed a bug on actionsheet ref not being created
- [x] Reply wasn't showing on Android
- [x] Use Notification.Builder.setColor/getColor only after Android SDK 23
- [x] Listen to app state only when inside app
- [x] Switched register push token position in order to improve login performance
- [x] When deep link changes server, it doesn't refresh rooms list
- [x] Added SafeAreaView in all views to improve iPhone X experience
- [x] Subpath regex #388

* [NEW] Empty room background (#412)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #398 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
![aug-09-2018 11-35-32](https://user-images.githubusercontent.com/804994/43906080-cbfadf92-9bc8-11e8-9ac9-44f43d3af023.gif)
![aug-09-2018 11-35-16](https://user-images.githubusercontent.com/804994/43906082-cc19411c-9bc8-11e8-9892-c65c86951a91.gif)
![image](https://user-images.githubusercontent.com/804994/43911366-ad830cd0-9bd5-11e8-8913-6a7e87a2206c.png)

* Add roadmap (#406)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #45 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

* [NEW] Onboarding (#407)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #392 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
![aug-07-2018 17-03-50](https://user-images.githubusercontent.com/804994/43799447-f62074dc-9a63-11e8-8aac-bf2c4c5a8a2b.gif)
![aug-07-2018 17-03-35](https://user-images.githubusercontent.com/804994/43799446-f5f84a70-9a63-11e8-8947-265113ae9bf4.gif)
![aug-07-2018 17-03-13](https://user-images.githubusercontent.com/804994/43799445-f5d70ee6-9a63-11e8-94a9-f49c7d69fbba.gif)

* [NEW] Updated Logo on Splash screen (#409)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #399 

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
![aug-07-2018 17-39-44](https://user-images.githubusercontent.com/804994/43801415-739a0cca-9a69-11e8-8bec-d65f751e6a28.gif)
![aug-07-2018 17-31-12](https://user-images.githubusercontent.com/804994/43801416-73d19bd6-9a69-11e8-90ac-bbc7ddeed938.gif)

* [FIX] Only single attachment rendered (#417)

* [NEW] Rooms list layout (#413)

* RoomsListView layout

* Rooms list layout

* Sort component

* Header icons

* Default header colors

* Add server dropdown

* Close sort dropdown if server dropdown will open

* UserItem

* Room type icon

* Search working

* Tests updated

* Android layout

* Using realm queries instead of array iterates

* Animation duration

* Fixed render bug

* [NEW] Create channel layout (#420)

* RoomsListView layout

* Rooms list layout

* Sort component

* Header icons

* Default header colors

* Add server dropdown

* Close sort dropdown if server dropdown will open

* UserItem

* Room type icon

* Search working

* Tests updated

* Android layout

* Using realm queries instead of array iterates

* Animation duration

* Fixed render bug

* - NewMessageView
- backButtonTitle always empty
- SearchBox created

* New create channel layout

* Search refactored

* loginSuccess dismiss modal

* Tests working

* [FIX] Open unsupported videos on browser (#422)

* 1.1

* Sort/group rooms local only (#425)

* Update android api from ci

* Sort local only

* [FIX] Missing current server (#427)

* server.current removed

* Increased area of touch on header

* Hide search when sort dropdown is tapped

* default server icon url

* 1.1.1

* [NEW] Experimental Icon (#430)

* [NEW] Message layout (#426)

* message container/component

* Separator component

* Reply

* Url

* tests updated

* Minor changes

* Audio component

* Broadcast button

* Minor touches

* Reply preview

* Edited

* Minor bug fixes

* - Update roadmap
- Bump version to 1.2

* Onboarding styles fix

* [FIX] Drawer navigation won't refresh chats (#432)

* Avoid errors on Audio/Image/Video (#443)

* Bump version to 1.2.1 (#444)

* Stop supporting Android 4.4 and lower (#447)

* Several fixes for 1.2.1 (#448)

* Fix user.roles

* Better onLongPress handle on messages

* Indicator position

* Fix role undefined in system messages

* Add baseUrl in case of file attachments

* Join room fixed

* RoomView params

* Broadcast fixes

* Add server layout changes

* Use native images

* Subscribe to not joined channels

* Fix alerts without i18n

* Tests updated

* Bump version to 1.2.2 (#449)

* [NEW] Use community JSC for Android (#450)

* [NEW] Use community JSC for Android

* Quick fix on unread chats

* [NEW] Show app version (#454)

* [NEW] Portuguese translation (#452)

* [NEW] Portuguese translation

* Remove servers from sidebar

* Update dependencies (#431)

* Update dependencies

* Lint and test

* Added react-native fork

* rn 57

* Lint and tests updated

* Update xcode on circleci

* Use legacy build system

* Update tests

* Use inline requires (#459)

* Update dependencies

* Lint and test

* Added react-native fork

* rn 57

* Lint and tests updated

* Update xcode on circleci

* Use legacy build system

* Update tests

* Inline requires

* Fix eslint and remove temp gradle

* Unnecessary renders

* Update isNotch and Readme

* Tests updated

* Bump version to 1.3.0 (#461)

* Better touch handling on rooms list (#462)

* Use react-native-gesture-handler at RoomItem

* Fixed info message author

* Edit message render improvement

* Fix ws to http replace

* Bump version to 1.3.1 (#463)

* Composer layout tweaked (#464)

* Composer layout tweaked

* Fix localization error

* Bump version to 1.3.2

* [FIX] Handle deleted messages (#466)

* [FIX] Handle deleted messages

* Fix rest error

* Fix some connection issues

* [FIX] Search rooms (#468)

* Bump version to 1.3.3 (#469)

* Connecting to DDP badge (#471)

* Display custom fields on user info (#476)

* Render custom fields on user info

* renderCustomFields fix

* Display custom fields in user info

* Fix lint error

* [FIX] DDP badge wasn't hiding on fast connections (#477)

* Use Rocket.Chat JS SDK (#481)

* JS SDK

* API working

* Multiple servers

* Bump version to 1.4.0 (#482)

* [FIX] 2FA and LDAP (#488)

* [FIX] Unread rooms group order (#487)

* Use grouping setting on temp messages (#486)

* [FIX] Delete room error (#485)

* Rename to Rocket.Chat Experimental (#483)

* Update dependencies (#484)

* Bump version to 1.4.0 (#482)

* test

* one more test

* Fix build

* Regression: Wait for unmount to delete database after logout (#489)

* Bump version to 1.4.1 (#490)

* Regression: Crash on Android search (#492)

* Bump version to 1.4.2 (#493)

* Update Rocket.Chat.js.SDK (#494)

* Bump version to v1.4.3 (#495)

* [FIX] OAuth (#496)

* Smaller header icons inside the room (#499)

* [FIX] Logout (#497)

* [FIX] Logout

* Removed realm instances on rooms list

* Bump version to 1.4.4 (#498)

* Update navigation library (#501)

* v2

* Working on Android 0.57.3

* Drawer working

* Removing v1 navigator

* - Splash screen
- Icons changed

* Deeplink

* Remove EventEmitter from CreateChannelView

* Android search

* Android notifications

* OAuth

* Fix search props

* Lint and tests fixed

* Fix android build

* Improvements on iPhone X* usage

* Fix detox

* Fix android build

* Room.f added to RoomView.shouldComponentUpdate

* Animations on RoomsListView and RoomView

* Fix topbar buttons on Android

* Bump version to 1.5.0 (#503)

* Check $FABRIC_KEY availability in CircleCI (#506)

* Check $FABRIC_KEY in CircleCI

* Remove config scripts

* Check $FABRIC_KEY availability in CircleCI for iOS (#507)

* [I18n] Add Simplified Chinese(zh-CN) locale (#505)

* [FIX] iOS pop gesture not working properly (#509)

* Check if lastMessage has an attachment and show "User sent an attachment" at RoomsList (#510)

* [FIX] Messages not being loaded properly (#513)

* Fetch avatar initials from server (#512)

* Fix iOS pop gesture and open sidemenu gesture (#511)

* Bump version to 1.5.1 (#516)

* [NEW] Room header layout (#521)

* Clear iOS notification on resume/open (#520)

* [FIX] Flashing avatars on Android after #512 (#519)

* [FIX] App connects to previous server instead of the recent added (#518)

* [FIX] Room view header crashes when destructuring reducer (#523)

* [FIX] Dismiss keyboard on room close (#530)

* [FIX] Composer composer's send icon slowness (#528)

* [WIP] New Authentication layout (#536)

New Authentication layout

* Regression: Resend messages with error (#532)

* DDP Connection badge animation changed (#533)

* [FIX] Upload buttons on Android (#541)

* Bump version to 1.6.0 (#543)

* I18n: Add missing translation of simplified Chinese (#539)

* Update dependencies (#544)

* AndroidManifest changes

* Regression: Deep linking stopped working after react-native-navigation update (#549)

* [FIX] Android stuck on splash screen after hardware back button is pressed (#550)

* [FIX] Android stuck on splash screen after hardware button is pressed

* Fix empty user at asyncstorage

* Remove unused subscribe

* [FIX] x-instance-id header prop is case insensitive (#551)

* Bump version to 1.6.1 (#553)

* [FIX] x-instance-id header prop is case insensitive

* Use Rest API calls (#558)

* Chats: Don't show group header if none of the filters is selected (#560)

* [CHORE] Update Xcode image version on CircleCI (#561)

* Bump version to 1.7.0 (#562)

* [FIX] Load messages on notification tap (#564)

* Use Rest API pt 2 (#568)

* Room files

* Pinned messages

* Starred messages

* Mentioned messages

* Search messages

* Bug fixes

* Profile

* Livechat

* Block/unblock user

* Erase room

* Archive room

* Remove unused method

* Bug fix

* [CHORE] Add hold step on CircleCI before TestFlight (#572)

* [FIX] GET /info to check if it's a valid server instead of x-instance-id (#573)

* Bump version to 1.7.1 (#574)

* Unnecessary re-renders removed (#570)

* shouldComponentUpdate

* Rooms list shouldcomponentupdate

* RoomView shouldComponentUpdate

* Messagebox and Message shouldComponentUpdate

* EmojiPicker shouldComponentUpdate

* RoomActions shouldComponentUpdate

* Room info shouldComponentUpdate

* Update RNN

* Use only one Flatlist if none group filter is selected

* Update fix

* shouldComponentUpdate

* Bug fixes

* ListView changes

* Bug fix

* render list bug fix

* Changes on public channels

* - RoomView saga leak removed
- Join room e2e tests added

* Rest versions

* Method call versions

* Min RocketChat version alert

* Update dependencies (#587)

* [FIX] Better message actions (#567)

* [FIX] Back button press on message actions (#592)

* Bump version to 1.8.0 (#595)

* [FIX] LDAP login (#596)

* Create class to manage navigation (#594)

* Add Navigation class

* Place Drawer.js logic inside of Navigation

* Load less views at startup

* [FIX] v1.8.0 (#599)

* Downgrade react-native-fast-image

* Update iOS permission usage descriptions

* [FIX] Delete upload item

* Update JS SDK version (#602)

* Add Icons class (#611)

Creates Icons class to manage when to load icons from native side or react-native-vector-icons.
It also fixes `react-native run-android` #517

* Updating room indicator (#609)

Shows "Updating..." when requesting rooms from Rest API.

* [FIX] Load avatar on servers that prevent unauthenticated avatar access (#604)

App would show an empty space on servers that require authentication on avatar access

* [FIX] 2FA login in a server with LDAP enabled (#612)

* [FIX] Start loop searching for rooms updates only when connection goes down and SDK has userId (#613)

* Allow to create empty channel (#615)

* [FIX] Reply title should break text (#616)

* Bump version to 1.9.0 (#617)

* [FIX] SDK issues (#621)

* Remove listeners from room
* Properly close connections on change server
* Minor layout change on connecting badge

* [CHORE] Add TestFlight invite and update Readme (#623)

* [FIX] npm -> yarn dependencies migration (#622)

* I18n: Add French (#629)

* [FIX] Remove rooms listener (#630)

* [CHORE] Update issue template (#638)

* I18n: Add German (#641)

* Bump version to 1.10.0 (#644)

* [FIX] Prevent mass is typing dispatchs (#651)

* [FIX] Handle database errors properly (#650)

* [FIX] Change actions labels (#654)

* [FIX] Room members filter (#655)

* [FIX] uploadProgress is not a function (#656)

* [FIX] Slow messagebox (#658)

* Remove drawer (#653)

* Remove drawer (layout needs to be changed in future releases, though)
* Don't navigate outside on logout if there's other logged server
* Update react-native-navigation

* Message button (#660)

* Remove touchable opacity when scrolling messages
* Tap on disable messages closes keyboard
* Unify vibration
* Vibrate only on Android

* [FIX] Fetch rooms date (#662)

* [FIX] Select emoji error (#666)

* Update Realm to 2.24 (#667)

* Update React Native to 0.58.6 (#668)

* [FIX] Fix some language issues in German language (#664)

* New icons (#643)

* New Icons

* Remove unused assets

* Change send icon

* Layout tweaks

* Refactor Status

* Styles changed

* User layout fix

* Separator layout changes

* Sidebar status layout fix

* Fix Message.onLongPress issue

* Fix code markdown
Closes https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/625

* Status lint

* Fix tests

* Navigation debounce

* RoomActions icons

* Space between components

* Group text

* Update tests

* [CHORE] Remove .debug suffix on Android (#681)

* [FIX] Fix null native Messagebox component object (#680)

* Fix null native Messagebox component object

* [iOS] Fix header alignment

* Remove unused files

* Switch to react-navigation (#687)

* Update readme (#714)

* Bump to 1.10.1 (#731)

* [FIX] Deep linking between multiple logged servers (#730)

* Fix handle invisible status (#692)

* I18n: Add Portuguese (Portugal) (#722)

* [FIX] Show ActivityIndicator in RoomMembersView (#686)

* Bump version to 1.11.0 (#761)

* Migrate from GCM to FCM (#760)

* [NEW] Scrollable room name feature (#756)

* [NEW] Scroll down floating button (#735)

* [CHORE] Added Storybook documentation (#757)

* Use FlatList in RoomView (#762)

* [FIX] iOS requiring location permission (#768)

* Room item layout (#771)

* [NEW] Draft message per room (#772)

* [FIX] Add Realm.safeAddListener (#785)

* [CHORE] Remove tvOS target (#779)

* [NEW] Discussions (#696)

* Bump version to 1.12.0 (#804)

* [NEW] Threads (#798)

* RoomsListView improvements (#819)

* [FIX] Giphy not showing (#810)

* [FIX] Apply emojify on empty texts (#824)

* Lock drawer when stack is not on root screen (#825)

* Room item layout (#835)

* [FIX] Threads (#838)

Closes #826
Closes #827
Closes #828
Closes #829
Closes #830
Closes #831
Closes #832
Closes #833

* [FIX] Smaller thread title (#846)

* [FIX] Smaller thread title

* Remove markdown notation from thread title

* On message press debounce

* Align vertical thread title

* [Regression] Search stopped working on Android after LastMessage refactor (#851)

* Load legal pages from web (#849)

* Update fetch permissions api (#850)

* Update custom emojis endpoint (#852)

* Update emoji endpoint

* Use React.memo on Markdown

* Support RC versions lower than 0.75.0

* Realm migration

* Fetch roles from rest api (#853)

* Fetch roles from rest api

* Fix RoomInfoView role get

* Remove roles from redux

* Bump version to 1.13 (#857)

* Active users improvements (#855)

* Remove connection badge (#862)

* Connecting indicator on RoomsListView header

* Connecting indicator on RoomView header

* Remove ConnectionBadge

* Show updating on RoomView load messages

* Update dependencies (#863)

* Minor updates

* Update jsc-android

* Update react-native-modal

* Minor updates

* Update react-native-fast-image

* Minor dev updates

* Few major updates

* Update react-native-keyboard-aware-scroll-view

* Update pods

* Update android-support

* Update tests

* Remove duplicated getRoleDescription function (#866)

* [FIX] Load local URL image (#871)

* [FIX] Toggle/follow thread icon (#867)

* Tweaks on sequential threads messages layout (#858)

* Tweaks on sequential threads messages

* Update tests

* Fix quote

* Prevent from deleting thread start message when positioned inside the thread

* Remove thread listener from RightButtons

* Fix error on thread start parse

* Stop parsing threads on render

* Check replied thread only if necessary

* Fix messages don't displaying

* Fix threads e2e

* RoomsListView.updateState slice

* Stop fetching hidden messages on threads

* Set initialNumToRender to 5

* [FIX] Check if room is mounted before setting state (#864)

* Tweaks on sequential threads messages

* Update tests

* Fix quote

* Prevent from deleting thread start message when positioned inside the thread

* Remove thread listener from RightButtons

* Fix error on thread start parse

* Stop parsing threads on render

* Check replied thread only if necessary

* Fix messages don't displaying

* Fix threads e2e

* RoomsListView.updateState slice

* Stop fetching hidden messages on threads

* Check if RoomView is mounted before rendering

* Refactor navigation events on RoomsListView

* Fix lint

* Fix listener

* [FIX] Typing not getting cleared after popping a room (#873)

* [CHORE] Remove e2e tests from CI (#875)

* [FIX] Remove listeners on RoomView header unmount (#874)
This commit is contained in:
Diego Mello 2019-05-29 14:56:04 -03:00 committed by GitHub
parent 7303bddaef
commit fe46929238
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
181 changed files with 19431 additions and 9939 deletions

View File

@ -266,9 +266,9 @@ workflows:
build-and-test:
jobs:
- lint-testunit
- e2e-test:
requires:
- lint-testunit
# - e2e-test:
# requires:
# - lint-testunit
- ios-build:
requires:

View File

@ -1,5 +0,0 @@
{
"expoServerPort": null,
"packagerPort": null,
"packagerPid": null
}

View File

@ -1,9 +0,0 @@
{
"hostType": "tunnel",
"lanType": "ip",
"dev": true,
"strict": false,
"minify": false,
"urlType": "exp",
"urlRandomness": null
}

View File

@ -213,4 +213,25 @@ $ detox build --configuration ios.sim.release
```bash
$ detox test --configuration ios.sim.release
```
```
## Storybook
- Open index.js
- Uncomment following line
```bash
import './storybook';
```
- Comment out following lines
```bash
import './app/ReactotronConfig';
import { AppRegistry } from 'react-native';
import App from './app/index';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
```
- Start your application again

26
__mocks__/realm.js Normal file
View File

@ -0,0 +1,26 @@
export default class Realm {
schema = [];
data = [];
constructor(params) {
require('lodash').each(params.schema, (schema) => {
this.data[schema.name] = [];
this.data[schema.name].filtered = () => this.data[schema.name];
});
this.schema = params.schema;
}
objects(schemaName) {
return this.data[schemaName];
}
write = (fn) => {
fn();
}
create(schemaName, data) {
this.data[schemaName].push(data);
return data;
}
}

View File

@ -1,40 +0,0 @@
import {View} from 'react-native';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
const reducers = combineReducers({login:() => ({user: {}}), settings:() => ({}), meteor: () => ({ connected: true })});
const store = createStore(reducers);
import React from 'react';
import RoomItem from '../app/presentation/RoomItem';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
const date = '2017-10-10T10:00:00Z';
const onPress = () => {};
it('renders correctly', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" baseUrl="baseUrl" /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render unread', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1} /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render unread +999', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1000} /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render no icon', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="X" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render private group', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="g" _updatedAt={date} name="private-group" /> </View></Provider>).toJSON()).toMatchSnapshot();
});
it('render channel', () => {
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="c" _updatedAt={date} name="general" /></View></Provider>).toJSON()).toMatchSnapshot();
});

View File

@ -1,38 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render channel 1`] = `
<View>
View
</View>
`;
exports[`render no icon 1`] = `
<View>
View
</View>
`;
exports[`render private group 1`] = `
<View>
View
</View>
`;
exports[`render unread +999 1`] = `
<View>
View
</View>
`;
exports[`render unread 1`] = `
<View>
View
</View>
`;
exports[`renders correctly 1`] = `
<View>
View
</View>
`;

File diff suppressed because it is too large Load Diff

View File

@ -103,7 +103,7 @@ android {
minSdkVersion 21
targetSdkVersion 28
versionCode VERSIONCODE as Integer
versionName "1.10.0"
versionName "1.13.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
@ -180,7 +180,22 @@ repositories {
configurations.all {
resolutionStrategy {
force 'org.webkit:android-jsc:r225067'
force 'org.webkit:android-jsc:r241213'
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'play-services-base') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-stats') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-basement') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
}
}
}
@ -199,21 +214,23 @@ dependencies {
implementation project(':react-native-video')
implementation project(':react-native-vector-icons')
implementation project(':rn-fetch-blob')
implementation project(':@remobile/react-native-toast')
implementation project(':react-native-toast')
implementation project(':react-native-fast-image')
implementation project(':realm')
implementation project(':reactnativenotifications')
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:27.1.1"
implementation "com.android.support:support-v4:27.1.1"
implementation 'com.android.support:customtabs:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'org.webkit:android-jsc-cppruntime:+'
implementation "com.android.support:appcompat-v7:28.0.0"
implementation "com.android.support:support-v4:28.0.0"
implementation 'com.android.support:customtabs:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation "com.facebook.react:react-native:+" // From node_modules
implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
implementation 'com.google.android.gms:play-services-gcm:16.1.0'
implementation "com.google.firebase:firebase-core:16.0.1"
implementation "com.google.firebase:firebase-messaging:17.3.4"
implementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') {
transitive = true;
}
@ -225,3 +242,6 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'
com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true

View File

@ -0,0 +1,245 @@
{
"project_info": {
"project_number": "673693445664",
"firebase_url": "https://rocketchat-9e9be.firebaseio.com",
"project_id": "rocketchat-9e9be",
"storage_bucket": "rocketchat-9e9be.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:673693445664:android:6ef4638e500ec958",
"android_client_info": {
"package_name": "RocketChat"
}
},
"oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:673693445664:android:16da2e50aff9f0c9",
"android_client_info": {
"package_name": "chat.rocket.android"
}
},
"oauth_client": [
{
"client_id": "673693445664-hrjftksij02vqtd467ln2cubvu48ft5j.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "chat.rocket.android",
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
}
},
{
"client_id": "673693445664-k0mvosdjoe5dbvqce3b377ckabb5dgu8.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "chat.rocket.android",
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
}
},
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.ios",
"app_store_id": "1148741252"
}
}
]
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:673693445664:android:1551054db195f705",
"android_client_info": {
"package_name": "chat.rocket.android.dev"
}
},
"oauth_client": [
{
"client_id": "673693445664-t5aeku0oie010npd40a0tgn27c418vk7.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "chat.rocket.android.dev",
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
}
},
{
"client_id": "673693445664-iml14ln4vccuu7liclrpt2k671fkjs38.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "chat.rocket.android.dev",
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
}
},
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.ios",
"app_store_id": "1148741252"
}
}
]
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:673693445664:android:8be27b1f7c42a2ed",
"android_client_info": {
"package_name": "chat.rocket.reactnative"
}
},
"oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:673693445664:android:64932c99863e2838",
"android_client_info": {
"package_name": "com.konecty.rocket.chat"
}
},
"oauth_client": [
{
"client_id": "673693445664-3ajben08beuco6eout3kpod2gbbm8fij.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.konecty.rocket.chat",
"certificate_hash": "cd5806ba3f0141d0f2e47acfe64a485f575108ab"
}
},
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.ios",
"app_store_id": "1148741252"
}
}
]
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

View File

@ -9,15 +9,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="27" />
<application
android:name=".MainApplication"
android:allowBackup="true"
@ -45,8 +36,6 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="673693445664\0"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -4,12 +4,11 @@ buildscript {
repositories {
mavenLocal()
google()
// mavenCentral()
jcenter()
}
dependencies {
// classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.android.tools.build:gradle:3.1.0'
classpath 'com.google.gms:google-services:4.0.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -19,12 +18,8 @@ buildscript {
allprojects {
repositories {
mavenLocal()
// mavenCentral()
google()
jcenter()
// maven {
// url 'https://maven.google.com'
// }
maven { url "https://jitpack.io" }
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm

View File

@ -11,8 +11,8 @@ include ':react-native-device-info'
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':@remobile/react-native-toast'
project(':@remobile/react-native-toast').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-toast/android')
include ':react-native-toast'
project(':react-native-toast').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-toast/android')
include ':rn-fetch-blob'
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
include ':react-native-image-crop-picker'

View File

@ -26,18 +26,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER'
]);
export const ROOM = createRequestTypes('ROOM', [
'ADD_USER_TYPING',
'REMOVE_USER_TYPING',
'SOMEONE_TYPING',
'OPEN',
'CLOSE',
'LEAVE',
'ERASE',
'USER_TYPING',
'MESSAGE_RECEIVED',
'SET_LAST_OPEN'
]);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES', [
...defaultTypes,
@ -75,8 +64,6 @@ export const SERVER = createRequestTypes('SERVER', [
]);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
export const LOGOUT = 'LOGOUT'; // logout is always success
export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET']);
export const ROLES = createRequestTypes('ROLES', ['SET']);
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);

View File

@ -1,8 +0,0 @@
import * as types from './actionsTypes';
export function setActiveUser(data) {
return {
type: types.ACTIVE_USERS.SET,
data
};
}

View File

@ -1,25 +1,5 @@
import * as types from './actionsTypes';
export function messagesRequest(room) {
return {
type: types.MESSAGES.REQUEST,
room
};
}
export function messagesSuccess() {
return {
type: types.MESSAGES.SUCCESS
};
}
export function messagesFailure(err) {
return {
type: types.MESSAGES.FAILURE,
err
};
}
export function actionsShow(actionMessage) {
return {
type: types.MESSAGES.ACTIONS_SHOW,

View File

@ -1,8 +0,0 @@
import * as types from './actionsTypes';
export function setRoles(data) {
return {
type: types.ROLES.SET,
data
};
}

View File

@ -1,40 +1,5 @@
import * as types from './actionsTypes';
export function removeUserTyping(username) {
return {
type: types.ROOM.REMOVE_USER_TYPING,
username
};
}
export function someoneTyping(data) {
return {
type: types.ROOM.SOMEONE_TYPING,
...data
};
}
export function addUserTyping(username) {
return {
type: types.ROOM.ADD_USER_TYPING,
username
};
}
export function openRoom(room) {
return {
type: types.ROOM.OPEN,
room
};
}
export function closeRoom() {
return {
type: types.ROOM.CLOSE
};
}
export function leaveRoom(rid, t) {
return {
type: types.ROOM.LEAVE,
@ -51,23 +16,10 @@ export function eraseRoom(rid, t) {
};
}
export function userTyping(status = true) {
export function userTyping(rid, status = true) {
return {
type: types.ROOM.USER_TYPING,
rid,
status
};
}
export function roomMessageReceived(message) {
return {
type: types.ROOM.MESSAGE_RECEIVED,
message
};
}
export function setLastOpen(date = new Date()) {
return {
type: types.ROOM.SET_LAST_OPEN,
date
};
}

View File

@ -1,16 +1,19 @@
import { SERVER } from './actionsTypes';
export function selectServerRequest(server) {
export function selectServerRequest(server, version, fetchVersion = true) {
return {
type: SERVER.SELECT_REQUEST,
server
server,
version,
fetchVersion
};
}
export function selectServerSuccess(server) {
export function selectServerSuccess(server, version) {
return {
type: SERVER.SELECT_SUCCESS,
server
server,
version
};
}

View File

@ -1,9 +1,17 @@
import { isIOS } from '../utils/deviceInfo';
export const COLOR_DANGER = '#f5455c';
export const COLOR_BUTTON_PRIMARY = '#1d74f5';
export const COLOR_TEXT = '#292E35';
export const COLOR_SEPARATOR = '#CBCED1';
export const COLOR_SUCCESS = '#2de0a5';
export const COLOR_PRIMARY = '#1d74f5';
export const COLOR_WHITE = '#fff';
export const COLOR_BUTTON_PRIMARY = COLOR_PRIMARY;
export const COLOR_TITLE = '#0C0D0F';
export const COLOR_TEXT = '#2F343D';
export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
export const COLOR_SEPARATOR = '#A7A7AA';
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
export const COLOR_BORDER = '#e1e5e8';
export const COLOR_UNREAD = '#e1e5e8';
export const STATUS_COLORS = {
online: '#2de0a5',
busy: COLOR_DANGER,
@ -11,6 +19,6 @@ export const STATUS_COLORS = {
offline: '#cbced1'
};
export const HEADER_BACKGROUND = isIOS ? '#FFF' : '#2F343D';
export const HEADER_TITLE = isIOS ? '#0C0D0F' : '#FFF';
export const HEADER_BACK = isIOS ? '#1d74f5' : '#FFF';
export const HEADER_BACKGROUND = isIOS ? '#f8f8f8' : '#2F343D';
export const HEADER_TITLE = isIOS ? COLOR_TITLE : COLOR_WHITE;
export const HEADER_BACK = isIOS ? COLOR_PRIMARY : COLOR_WHITE;

View File

@ -1,17 +0,0 @@
export default [
'add-user-to-any-c-room',
'add-user-to-any-p-room',
'add-user-to-joined-room',
'archive-room',
'delete-c',
'delete-message',
'delete-p',
'edit-message',
'edit-room',
'force-delete-message',
'mute-user',
'set-react-when-readonly',
'set-readonly',
'unarchive-room',
'view-broadcast-member-list'
];

View File

@ -14,12 +14,6 @@ export default {
CROWD_Enable: {
type: 'valueAsBoolean'
},
Layout_Privacy_Policy: {
type: 'valueAsString'
},
Layout_Terms_of_Service: {
type: 'valueAsString'
},
LDAP_Enable: {
type: 'valueAsBoolean'
},
@ -61,5 +55,8 @@ export default {
},
Assets_favicon_512: {
type: null
},
Threads_enabled: {
type: null
}
};

View File

@ -1,73 +1,69 @@
import React, { PureComponent } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { View, ViewPropTypes } from 'react-native';
import FastImage from 'react-native-fast-image';
export default class Avatar extends PureComponent {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style,
text: PropTypes.string,
avatar: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
})
const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token
}) => {
const avatarStyle = {
width: size,
height: size,
borderRadius
};
if (!text && !avatar) {
return null;
}
static defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
const room = type === 'd' ? text : `@${ text }`;
// Avoid requesting several sizes by having only two sizes on cache
const uriSize = size === 100 ? 100 : 50;
let avatarAuthURLFragment = '';
if (userId && token) {
avatarAuthURLFragment = `&rc_token=${ token }&rc_uid=${ userId }`;
}
render() {
const {
text, size, baseUrl, borderRadius, style, avatar, type, children, user
} = this.props;
const uri = avatar || `${ baseUrl }/avatar/${ room }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`;
const avatarStyle = {
width: size,
height: size,
borderRadius
};
const image = (
<FastImage
style={avatarStyle}
source={{
uri,
priority: FastImage.priority.high
}}
/>
);
if (!text && !avatar) {
return null;
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
});
const room = type === 'd' ? text : `@${ text }`;
Avatar.propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style,
text: PropTypes.string,
avatar: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
userId: PropTypes.string,
token: PropTypes.string
};
// Avoid requesting several sizes by having only two sizes on cache
const uriSize = size === 100 ? 100 : 50;
Avatar.defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
};
let avatarAuthURLFragment = '';
if (user && user.id && user.token) {
avatarAuthURLFragment = `&rc_token=${ user.token }&rc_uid=${ user.id }`;
}
const uri = avatar || `${ baseUrl }/avatar/${ room }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`;
const image = (
<FastImage
style={avatarStyle}
source={{
uri,
priority: FastImage.priority.high
}}
/>
);
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
}
}
export default Avatar;

View File

@ -1,136 +0,0 @@
import React, { Component } from 'react';
import {
Text, StyleSheet, ActivityIndicator, Animated, Easing
} from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import I18n from '../i18n';
import debounce from '../utils/debounce';
const styles = StyleSheet.create({
container: {
width: '100%',
position: 'absolute',
top: 0,
height: 41,
backgroundColor: '#F7F8FA',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
elevation: 4
},
text: {
color: '#fff',
fontSize: 15,
fontWeight: 'normal'
},
textConnecting: {
color: '#9EA2A8'
},
containerConnected: {
backgroundColor: '#2de0a5'
},
containerOffline: {
backgroundColor: '#f5455c'
},
activityIndicator: {
marginRight: 15
}
});
const ANIMATION_DURATION = 300;
@connect(state => ({
connecting: state.meteor.connecting,
connected: state.meteor.connected,
disconnected: !state.meteor.connecting && !state.meteor.connected
}))
class ConnectionBadge extends Component {
static propTypes = {
connecting: PropTypes.bool,
connected: PropTypes.bool,
disconnected: PropTypes.bool
}
constructor(props) {
super(props);
this.animatedValue = new Animated.Value(0);
if (props.connecting) {
this.show();
}
}
componentDidUpdate() {
const { connected, disconnected } = this.props;
this.show(connected || disconnected);
}
componentWillUnmount() {
if (this.timeout) {
clearTimeout(this.timeout);
}
}
// eslint-disable-next-line react/sort-comp
animate = debounce((toValue, autoHide) => {
Animated.timing(
this.animatedValue,
{
toValue,
duration: ANIMATION_DURATION,
easing: Easing.ease,
useNativeDriver: true
},
).start(() => {
if (toValue === 1 && autoHide) {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.hide();
}, 1000);
}
});
}, 300);
show = (autoHide) => {
this.animate(1, autoHide);
}
hide = () => {
this.animate(0);
}
render() {
const { connecting, connected } = this.props;
const translateY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-42, 0]
});
if (connecting) {
return (
<Animated.View style={[styles.container, { transform: [{ translateY }] }]}>
<ActivityIndicator color='#9EA2A8' style={styles.activityIndicator} />
<Text style={[styles.text, styles.textConnecting]}>{I18n.t('Connecting')}</Text>
</Animated.View>
);
} else if (connected) {
return (
<Animated.View style={[styles.container, styles.containerConnected, { transform: [{ translateY }] }]}>
<Text style={styles.text}>{I18n.t('Connected')}</Text>
</Animated.View>
);
}
return (
<Animated.View style={[styles.container, styles.containerOffline, { transform: [{ translateY }] }]}>
<Text style={styles.text}>{I18n.t('Offline')}</Text>
</Animated.View>
);
}
}
export default ConnectionBadge;

View File

@ -10,7 +10,7 @@ import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
import styles from './styles';
import categories from './categories';
import database from '../../lib/realm';
import database, { safeAddListener } from '../../lib/realm';
import { emojisByCategory } from '../../emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
@ -45,8 +45,8 @@ export default class EmojiPicker extends Component {
this.updateFrequentlyUsed();
this.updateCustomEmojis();
requestAnimationFrame(() => this.setState({ show: true }));
this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
this.customEmojis.addListener(this.updateCustomEmojis);
safeAddListener(this.frequentlyUsed, this.updateFrequentlyUsed);
safeAddListener(this.customEmojis, this.updateCustomEmojis);
}
shouldComponentUpdate(nextProps, nextState) {

View File

@ -1,8 +1,9 @@
import { StyleSheet } from 'react-native';
import { COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors';
export default StyleSheet.create({
background: {
backgroundColor: '#fff'
backgroundColor: COLOR_WHITE
},
container: {
flex: 1
@ -27,7 +28,7 @@ export default StyleSheet.create({
left: 0,
right: 0,
height: 2,
backgroundColor: '#007aff',
backgroundColor: COLOR_PRIMARY,
bottom: 0
},
tabLine: {

View File

@ -1,15 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
import HeaderButtons, { HeaderButton, Item } from 'react-navigation-header-buttons';
import { CustomIcon } from '../lib/Icons';
import { isIOS } from '../utils/deviceInfo';
import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors';
const color = isIOS ? '#1D74F5' : '#FFF';
const color = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
export const headerIconSize = 23;
const CustomHeaderButton = React.memo(props => (
<HeaderButton {...props} IconComponent={CustomIcon} iconSize={23} color={color} />
<HeaderButton {...props} IconComponent={CustomIcon} iconSize={headerIconSize} color={color} />
));
export const CustomHeaderButtons = React.memo(props => (
@ -59,5 +60,3 @@ LegalButton.propTypes = {
};
export { Item };
export default () => <Text>a</Text>;

View File

@ -198,6 +198,11 @@ export default class MessageActions extends React.Component {
if (this.isRoomReadOnly()) {
return false;
}
// Prevent from deleting thread start message when positioned inside the thread
if (props.tmid && props.tmid === props.actionMessage._id) {
return false;
}
const deleteOwn = this.isOwn(props);
const { Message_AllowDeleting: isDeleteAllowed, Message_AllowDeleting_BlockDeleteInMinutes } = this.props;
if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) {

View File

@ -10,6 +10,7 @@ import styles from './styles';
import I18n from '../../i18n';
import { isIOS, isAndroid } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_SUCCESS, COLOR_DANGER } from '../../constants/colors';
export const _formatTime = function(seconds) {
let minutes = Math.floor(seconds / 60);
@ -130,7 +131,7 @@ export default class extends React.PureComponent {
>
<CustomIcon
size={22}
color='#f5455c'
color={COLOR_DANGER}
name='cross'
/>
</BorderlessButton>
@ -143,7 +144,7 @@ export default class extends React.PureComponent {
>
<CustomIcon
size={22}
color='#2de0a5'
color={COLOR_SUCCESS}
name='check'
/>
</BorderlessButton>

View File

@ -6,17 +6,21 @@ import { connect } from 'react-redux';
import Markdown from '../message/Markdown';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import {
COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_TEXT_DESCRIPTION, COLOR_WHITE
} from '../../constants/colors';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginTop: 10,
backgroundColor: '#fff'
backgroundColor: COLOR_WHITE
},
messageContainer: {
flex: 1,
marginHorizontal: 10,
backgroundColor: '#F3F4F5',
backgroundColor: COLOR_BACKGROUND_CONTAINER,
paddingHorizontal: 15,
paddingVertical: 10,
borderRadius: 4
@ -26,15 +30,17 @@ const styles = StyleSheet.create({
alignItems: 'center'
},
username: {
color: '#1D74F5',
color: COLOR_PRIMARY,
fontSize: 16,
fontWeight: '500'
...sharedStyles.textMedium
},
time: {
color: '#9EA2A8',
fontSize: 12,
lineHeight: 16,
marginLeft: 5
marginLeft: 6,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular,
fontWeight: '300'
},
close: {
marginRight: 10
@ -79,7 +85,7 @@ export default class ReplyPreview extends Component {
</View>
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
</View>
<CustomIcon name='cross' color='#9ea2a8' size={20} style={styles.close} onPress={this.close} />
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
</View>
);
}

View File

@ -12,8 +12,9 @@ import Button from '../Button';
import I18n from '../../i18n';
import sharedStyles from '../../views/Styles';
import { isIOS } from '../../utils/deviceInfo';
import { COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE } from '../../constants/colors';
const cancelButtonColor = '#f7f8fa';
const cancelButtonColor = COLOR_BACKGROUND_CONTAINER;
const styles = StyleSheet.create({
modal: {
@ -25,11 +26,13 @@ const styles = StyleSheet.create({
paddingTop: 16
},
title: {
fontSize: 14,
...sharedStyles.textColorTitle,
...sharedStyles.textBold
},
container: {
height: 430,
backgroundColor: '#ffffff',
backgroundColor: COLOR_WHITE,
flexDirection: 'column'
},
scrollView: {
@ -46,7 +49,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
padding: 16,
backgroundColor: '#f7f8fa'
backgroundColor: COLOR_BACKGROUND_CONTAINER
},
button: {
marginBottom: 0
@ -149,15 +152,15 @@ export default class UploadModal extends Component {
underlayColor={cancelButtonColor}
activeOpacity={0.5}
>
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: '#1d74f5' }]}>{I18n.t('Cancel')}</Text>
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: COLOR_PRIMARY }]}>{I18n.t('Cancel')}</Text>
</TouchableHighlight>
<TouchableHighlight
onPress={this.submit}
style={[styles.androidButton, { backgroundColor: '#1d74f5' }]}
underlayColor='#1d74f5'
style={[styles.androidButton, { backgroundColor: COLOR_PRIMARY }]}
underlayColor={COLOR_PRIMARY}
activeOpacity={0.5}
>
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: '#fff' }]}>{I18n.t('Send')}</Text>
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: COLOR_WHITE }]}>{I18n.t('Send')}</Text>
</TouchableHighlight>
</View>
);

View File

@ -31,12 +31,11 @@ import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview';
import { CustomIcon } from '../../lib/Icons';
import debounce from '../../utils/debounce';
import { COLOR_PRIMARY, COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
const MENTIONS_TRACKING_TYPE_USERS = '@';
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
const BLUE_COLOR = '#1D74F5';
const onlyUnique = function onlyUnique(value, index, self) {
return self.indexOf(({ _id }) => value._id === _id) === index;
};
@ -49,25 +48,7 @@ const imagePickerConfig = {
cropperCancelText: I18n.t('Cancel')
};
@connect(state => ({
roomType: state.room.t,
message: state.messages.message,
replyMessage: state.messages.replyMessage,
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}
}), dispatch => ({
editCancel: () => dispatch(editCancelAction()),
editRequest: message => dispatch(editRequestAction(message)),
typing: status => dispatch(userTypingAction(status)),
closeReply: () => dispatch(replyCancelAction())
}))
export default class MessageBox extends Component {
class MessageBox extends Component {
static propTypes = {
rid: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
@ -75,12 +56,15 @@ export default class MessageBox extends Component {
replyMessage: PropTypes.object,
replying: PropTypes.bool,
editing: PropTypes.bool,
threadsEnabled: PropTypes.bool,
isFocused: PropTypes.bool,
user: PropTypes.shape({
id: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string
}),
roomType: PropTypes.string,
tmid: PropTypes.string,
editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
@ -109,12 +93,38 @@ export default class MessageBox extends Component {
this.text = '';
}
componentDidMount() {
const { rid, tmid } = this.props;
let msg;
if (tmid) {
const thread = database.objectForPrimaryKey('threads', tmid);
if (thread) {
msg = thread.draftMessage;
}
} else {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room) {
msg = room.draftMessage;
}
}
if (msg) {
this.setInput(msg);
this.setShowSend(true);
}
}
componentWillReceiveProps(nextProps) {
const { message, replyMessage } = this.props;
if (message !== nextProps.message && nextProps.message.msg) {
const { message, replyMessage, isFocused } = this.props;
if (!isFocused) {
return;
}
if (!equal(message, nextProps.message) && nextProps.message.msg) {
this.setInput(nextProps.message.msg);
if (this.text) {
this.setShowSend(true);
}
this.focus();
} else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
} else if (!equal(replyMessage, nextProps.replyMessage)) {
this.focus();
} else if (!nextProps.message) {
this.clearInput();
@ -126,8 +136,11 @@ export default class MessageBox extends Component {
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
} = this.state;
const {
roomType, replying, editing
roomType, replying, editing, isFocused
} = this.props;
if (!isFocused) {
return false;
}
if (nextProps.roomType !== roomType) {
return true;
}
@ -241,7 +254,7 @@ export default class MessageBox extends Component {
>
<CustomIcon
size={22}
color={BLUE_COLOR}
color={COLOR_PRIMARY}
name='cross'
/>
</BorderlessButton>
@ -258,7 +271,7 @@ export default class MessageBox extends Component {
>
<CustomIcon
size={22}
color={BLUE_COLOR}
color={COLOR_PRIMARY}
name='emoji'
/>
</BorderlessButton>
@ -273,7 +286,7 @@ export default class MessageBox extends Component {
>
<CustomIcon
size={22}
color={BLUE_COLOR}
color={COLOR_PRIMARY}
name='keyboard'
/>
</BorderlessButton>
@ -294,7 +307,7 @@ export default class MessageBox extends Component {
accessibilityLabel={I18n.t('Send message')}
accessibilityTraits='button'
>
<CustomIcon name='send1' size={23} color={BLUE_COLOR} />
<CustomIcon name='send1' size={23} color={COLOR_PRIMARY} />
</BorderlessButton>
);
return icons;
@ -308,7 +321,7 @@ export default class MessageBox extends Component {
accessibilityLabel={I18n.t('Send audio message')}
accessibilityTraits='button'
>
<CustomIcon name='mic' size={23} color={BLUE_COLOR} />
<CustomIcon name='mic' size={23} color={COLOR_PRIMARY} />
</BorderlessButton>
);
icons.push(
@ -320,7 +333,7 @@ export default class MessageBox extends Component {
accessibilityLabel={I18n.t('Message actions')}
accessibilityTraits='button'
>
<CustomIcon name='plus' size={23} color={BLUE_COLOR} />
<CustomIcon name='plus' size={23} color={COLOR_PRIMARY} />
</BorderlessButton>
);
return icons;
@ -446,13 +459,13 @@ export default class MessageBox extends Component {
}
handleTyping = (isTyping) => {
const { typing } = this.props;
const { typing, rid } = this.props;
if (!isTyping) {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
this.typingTimeout = false;
}
typing(false);
typing(rid, false);
return;
}
@ -461,7 +474,7 @@ export default class MessageBox extends Component {
}
this.typingTimeout = setTimeout(() => {
typing(true);
typing(rid, true);
this.typingTimeout = false;
}, 1000);
}
@ -487,7 +500,7 @@ export default class MessageBox extends Component {
}
sendImageMessage = async(file) => {
const { rid } = this.props;
const { rid, tmid } = this.props;
this.setState({ file: { isVisible: false } });
const fileInfo = {
@ -499,7 +512,7 @@ export default class MessageBox extends Component {
path: file.path
};
try {
await RocketChat.sendFileMessage(rid, fileInfo);
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
} catch (e) {
log('sendImageMessage', e);
}
@ -545,14 +558,14 @@ export default class MessageBox extends Component {
}
finishAudioMessage = async(fileInfo) => {
const { rid } = this.props;
const { rid, tmid } = this.props;
this.setState({
recording: false
});
if (fileInfo) {
try {
await RocketChat.sendFileMessage(rid, fileInfo);
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
} catch (e) {
if (e && e.error === 'error-file-too-large') {
return Alert.alert(I18n.t(e.error));
@ -578,31 +591,42 @@ export default class MessageBox extends Component {
if (message.trim() === '') {
return;
}
// if is editing a message
const {
editing, replying
} = this.props;
// Edit
if (editing) {
const { _id, rid } = editingMessage;
editRequest({ _id, msg: message, rid });
// Reply
} else if (replying) {
const {
user, replyMessage, roomType, closeReply
} = this.props;
const permalink = await this.getPermalink(replyMessage);
let msg = `[ ](${ permalink }) `;
const { replyMessage, closeReply, threadsEnabled } = this.props;
// if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) {
msg += `@${ replyMessage.u.username } `;
// Thread
if (threadsEnabled && replyMessage.mention) {
onSubmit(message, replyMessage._id);
// Legacy reply or quote (quote is a reply without mention)
} else {
const { user, roomType } = this.props;
const permalink = await this.getPermalink(replyMessage);
let msg = `[ ](${ permalink }) `;
// if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) {
msg += `@${ replyMessage.u.username } `;
}
msg = `${ msg } ${ message }`;
onSubmit(msg);
}
msg = `${ msg } ${ message }`;
onSubmit(msg);
closeReply();
// Normal message
} else {
// if is submiting a new message
onSubmit(message);
}
this.clearInput();
@ -648,7 +672,7 @@ export default class MessageBox extends Component {
onPress={() => this.onPressMention(item)}
>
<Text style={styles.fixedMentionAvatar}>{item.username}</Text>
<Text>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
<Text style={styles.mentionText}>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
</TouchableOpacity>
)
@ -691,7 +715,7 @@ export default class MessageBox extends Component {
{trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
? [
this.renderMentionEmoji(item),
<Text key='mention-item-name'>:{ item.name || item }:</Text>
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
]
: [
<Avatar
@ -701,9 +725,10 @@ export default class MessageBox extends Component {
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
/>,
<Text key='mention-item-name'>{ item.username || item.name }</Text>
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
]
}
</TouchableOpacity>
@ -782,7 +807,7 @@ export default class MessageBox extends Component {
underlineColorAndroid='transparent'
defaultValue=''
multiline
placeholderTextColor='#9ea2a8'
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
testID='messagebox-input'
/>
{this.rightButtons}
@ -820,3 +845,26 @@ export default class MessageBox extends Component {
);
}
}
const mapStateToProps = state => ({
message: state.messages.message,
replyMessage: state.messages.replyMessage,
replying: state.messages.replying,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
threadsEnabled: state.settings.Threads_enabled,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}
});
const dispatchToProps = ({
editCancel: () => editCancelAction(),
editRequest: message => editRequestAction(message),
typing: (rid, status) => userTypingAction(rid, status),
closeReply: () => replyCancelAction()
});
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);

View File

@ -1,29 +1,33 @@
import { StyleSheet } from 'react-native';
import { isIOS } from '../../utils/deviceInfo';
import sharedStyles from '../../views/Styles';
import {
COLOR_BORDER, COLOR_SEPARATOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE
} from '../../constants/colors';
const MENTION_HEIGHT = 50;
export default StyleSheet.create({
textBox: {
backgroundColor: '#fff',
backgroundColor: COLOR_WHITE,
flex: 0,
alignItems: 'center',
borderTopWidth: 1,
borderTopColor: '#D8D8D8',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: COLOR_SEPARATOR,
zIndex: 2
},
composer: {
backgroundColor: '#fff',
backgroundColor: COLOR_WHITE,
flexDirection: 'column',
borderTopColor: '#e1e5e8',
borderTopWidth: 1
borderTopColor: COLOR_SEPARATOR,
borderTopWidth: StyleSheet.hairlineWidth
},
textArea: {
flexDirection: 'row',
alignItems: 'center',
flexGrow: 0,
backgroundColor: '#fff'
backgroundColor: COLOR_WHITE
},
textBoxInput: {
textAlignVertical: 'center',
@ -37,7 +41,8 @@ export default StyleSheet.create({
paddingRight: 0,
fontSize: 17,
letterSpacing: 0,
color: '#2f343d'
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
editing: {
backgroundColor: '#fff5df'
@ -53,9 +58,9 @@ export default StyleSheet.create({
},
mentionItem: {
height: MENTION_HEIGHT,
backgroundColor: '#F7F8FA',
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderTopWidth: 1,
borderTopColor: '#ECECEC',
borderTopColor: COLOR_BORDER,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 5
@ -72,18 +77,25 @@ export default StyleSheet.create({
textAlign: 'center'
},
fixedMentionAvatar: {
fontWeight: 'bold',
textAlign: 'center',
width: 46
width: 46,
fontSize: 14,
...sharedStyles.textBold,
...sharedStyles.textColorNormal
},
mentionText: {
fontSize: 14,
...sharedStyles.textRegular,
...sharedStyles.textColorNormal
},
emojiKeyboardContainer: {
flex: 1,
borderTopColor: '#ECECEC',
borderTopColor: COLOR_BORDER,
borderTopWidth: 1
},
iphoneXArea: {
height: 50,
backgroundColor: '#fff',
backgroundColor: COLOR_WHITE,
position: 'absolute',
bottom: 0,
left: 0,

View File

@ -1,12 +1,18 @@
import React from 'react';
import { Image, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons';
import { COLOR_TEXT_DESCRIPTION } from '../constants/colors';
const styles = StyleSheet.create({
style: {
marginRight: 7,
marginTop: 3,
tintColor: '#9EA2A8'
tintColor: COLOR_TEXT_DESCRIPTION,
color: COLOR_TEXT_DESCRIPTION
},
discussion: {
marginRight: 6
}
});
@ -15,6 +21,11 @@ const RoomTypeIcon = React.memo(({ type, size, style }) => {
return null;
}
if (type === 'discussion') {
// FIXME: These are temporary only. We should have all room icons on <Customicon />, but our design team is still working on this.
return <CustomIcon name='chat' size={13} style={[styles.style, styles.discussion]} />;
}
if (type === 'c') {
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size }]} />;
}

View File

@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo';
import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles';
const styles = StyleSheet.create({
container: {
@ -28,7 +29,8 @@ const styles = StyleSheet.create({
fontSize: 17,
marginLeft: 8,
paddingTop: 0,
paddingBottom: 0
paddingBottom: 0,
...sharedStyles.textRegular
}
});

View File

@ -4,28 +4,16 @@ import { connect } from 'react-redux';
import { ViewPropTypes } from 'react-native';
import Status from './Status';
import database, { safeAddListener } from '../../lib/realm';
@connect((state, ownProps) => {
if (state.login.user && ownProps.id === state.login.user.id) {
return {
status: state.login.user && state.login.user.status,
offline: !state.meteor.connected
};
}
const user = state.activeUsers[ownProps.id];
return {
status: (user && user.status) || 'offline'
};
})
@connect(state => ({
offline: !state.meteor.connected
}))
export default class StatusContainer extends React.PureComponent {
static propTypes = {
// id is a prop, but it's used only inside @connect to find for current status
id: PropTypes.string, // eslint-disable-line
id: PropTypes.string,
style: ViewPropTypes.style,
size: PropTypes.number,
status: PropTypes.string,
offline: PropTypes.bool
};
@ -33,12 +21,32 @@ export default class StatusContainer extends React.PureComponent {
size: 16
}
constructor(props) {
super(props);
this.user = database.memoryDatabase.objects('activeUsers').filtered('id == $0', props.id);
this.state = {
user: this.user[0] || {}
};
safeAddListener(this.user, this.updateState);
}
componentWillUnmount() {
this.user.removeAllListeners();
}
get status() {
const { offline, status } = this.props;
if (offline) {
const { user } = this.state;
const { offline } = this.props;
if (offline || !user) {
return 'offline';
}
return status;
return user.status || 'offline';
}
updateState = () => {
if (this.user.length) {
this.setState({ user: this.user[0] });
}
}
render() {

View File

@ -3,13 +3,13 @@ import { StatusBar as StatusBarRN } from 'react-native';
import PropTypes from 'prop-types';
import { isIOS } from '../utils/deviceInfo';
import { HEADER_BACKGROUND } from '../constants/colors';
import { HEADER_BACKGROUND, COLOR_WHITE } from '../constants/colors';
const HEADER_BAR_STYLE = isIOS ? 'dark-content' : 'light-content';
const StatusBar = React.memo(({ light }) => {
if (light) {
return <StatusBarRN backgroundColor='#FFF' barStyle='dark-content' animated />;
return <StatusBarRN backgroundColor={COLOR_WHITE} barStyle='dark-content' animated />;
}
return <StatusBarRN backgroundColor={HEADER_BACKGROUND} barStyle={HEADER_BAR_STYLE} animated />;
});

View File

@ -6,7 +6,9 @@ import PropTypes from 'prop-types';
import { BorderlessButton } from 'react-native-gesture-handler';
import sharedStyles from '../views/Styles';
import { COLOR_DANGER, COLOR_TEXT } from '../constants/colors';
import {
COLOR_DANGER, COLOR_TEXT_DESCRIPTION, COLOR_TEXT, COLOR_BORDER
} from '../constants/colors';
import { CustomIcon } from '../lib/Icons';
const styles = StyleSheet.create({
@ -15,22 +17,21 @@ const styles = StyleSheet.create({
},
label: {
marginBottom: 10,
color: COLOR_TEXT,
fontSize: 14,
fontWeight: '700'
...sharedStyles.textSemibold,
...sharedStyles.textColorNormal
},
input: {
...sharedStyles.textRegular,
...sharedStyles.textColorNormal,
height: 48,
fontSize: 17,
color: '#9EA2A8',
letterSpacing: 0,
fontSize: 16,
paddingLeft: 14,
paddingRight: 14,
borderWidth: 1.5,
borderWidth: 1,
borderRadius: 2,
backgroundColor: 'white',
borderColor: '#E7EBF2'
borderColor: COLOR_BORDER
},
inputIconLeft: {
paddingLeft: 45
@ -59,10 +60,10 @@ const styles = StyleSheet.create({
right: 15
},
icon: {
color: '#2F343D'
color: COLOR_TEXT
},
password: {
color: '#9ea2a8'
color: COLOR_TEXT_DESCRIPTION
}
});
@ -144,7 +145,7 @@ export default class RCTextInput extends React.PureComponent {
testID={testID}
accessibilityLabel={placeholder}
placeholder={placeholder}
placeholderTextColor='#9ea2a8'
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
contentDescription={placeholder}
{...inputProps}
/>

View File

@ -6,11 +6,13 @@ import {
import Video from 'react-native-video';
import Slider from 'react-native-slider';
import moment from 'moment';
import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';
const styles = StyleSheet.create({
audioContainer: {
@ -18,35 +20,42 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
height: 56,
backgroundColor: '#f7f8fa',
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4,
marginBottom: 10
marginBottom: 6
},
playPauseButton: {
width: 56,
marginHorizontal: 10,
alignItems: 'center',
backgroundColor: 'transparent'
},
playPauseImage: {
color: '#1D74F5'
color: COLOR_PRIMARY
},
slider: {
flex: 1,
marginRight: 10
flex: 1
},
duration: {
marginRight: 16,
marginHorizontal: 12,
fontSize: 14,
fontWeight: '500',
color: '#54585e'
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
thumbStyle: {
width: 12,
height: 12
},
trackStyle: {
height: 2
}
});
const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
const BUTTON_HIT_SLOP = {
top: 12, right: 12, bottom: 12, left: 12
};
export default class Audio extends React.Component {
static propTypes = {
@ -93,30 +102,30 @@ export default class Audio extends React.Component {
return false;
}
onLoad(data) {
onLoad = (data) => {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
}
onProgress(data) {
onProgress = (data) => {
const { duration } = this.state;
if (data.currentTime <= duration) {
this.setState({ currentTime: data.currentTime });
}
}
onEnd() {
onEnd = () => {
this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => {
this.player.seek(0);
});
}
getDuration() {
getDuration = () => {
const { duration } = this.state;
return formatTime(duration);
}
togglePlayPause() {
togglePlayPause = () => {
const { paused } = this.state;
this.setState({ paused: !paused });
}
@ -148,16 +157,18 @@ export default class Audio extends React.Component {
paused={paused}
repeat={false}
/>
<BorderlessButton
<Touchable
style={styles.playPauseButton}
onPress={() => this.togglePlayPause()}
onPress={this.togglePlayPause}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
{
paused
? <CustomIcon name='play' size={30} style={styles.playPauseImage} />
: <CustomIcon name='pause' size={30} style={styles.playPauseImage} />
? <CustomIcon name='play' size={36} style={styles.playPauseImage} />
: <CustomIcon name='pause' size={36} style={styles.playPauseImage} />
}
</BorderlessButton>
</Touchable>
<Slider
style={styles.slider}
value={currentTime}
@ -169,10 +180,11 @@ export default class Audio extends React.Component {
easing: Easing.linear,
delay: 0
}}
thumbTintColor='#1d74f5'
minimumTrackTintColor='#1d74f5'
thumbTintColor={COLOR_PRIMARY}
minimumTrackTintColor={COLOR_PRIMARY}
onValueChange={value => this.setState({ currentTime: value })}
thumbStyle={styles.thumbStyle}
trackStyle={styles.trackStyle}
/>
<Text style={styles.duration}>{this.getDuration()}</Text>
</View>,

View File

@ -1,8 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
@ -36,7 +36,7 @@ export default class extends Component {
return false;
}
onPressButton() {
onPressButton = () => {
this.setState({
modalVisible: true
});
@ -58,7 +58,7 @@ export default class extends Component {
render() {
const { modalVisible, isPressed } = this.state;
const { baseUrl, file, user } = this.props;
const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
const img = file.image_url.includes('http') ? file.image_url : `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
if (!img) {
return null;
@ -66,20 +66,21 @@ export default class extends Component {
return (
[
<RectButton
<Touchable
key='image'
onPress={() => this.onPressButton()}
onActiveStateChange={this.isPressed}
onPress={this.onPressButton}
style={styles.imageContainer}
underlayColor='#fff'
background={Touchable.Ripple('#fff')}
>
<FastImage
style={[styles.image, isPressed && { opacity: 0.5 }]}
source={{ uri: encodeURI(img) }}
resizeMode={FastImage.resizeMode.cover}
/>
{this.getDescription()}
</RectButton>,
<React.Fragment>
<FastImage
style={[styles.image, isPressed && { opacity: 0.5 }]}
source={{ uri: encodeURI(img) }}
resizeMode={FastImage.resizeMode.cover}
/>
{this.getDescription()}
</React.Fragment>
</Touchable>,
<PhotoModal
key='modal'
title={file.title}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Text, Image, Platform } from 'react-native';
import { Text, Image } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
@ -14,116 +14,90 @@ const formatText = text => text.replace(
(match, url, title) => `[${ title }](${ url })`
);
const codeFontFamily = Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
});
export default class Markdown extends React.Component {
shouldComponentUpdate(nextProps) {
const { msg } = this.props;
return nextProps.msg !== msg;
const Markdown = React.memo(({
msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
}) => {
if (!msg) {
return null;
}
render() {
const {
msg, customEmojis, style, rules, baseUrl, username, edited
} = this.props;
if (!msg) {
return null;
}
let m = formatText(msg);
let m = formatText(msg);
if (m) {
m = emojify(m, { output: 'unicode' });
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
return (
<MarkdownRenderer
rules={{
paragraph: (node, children) => (
// eslint-disable-next-line
<Text key={node.key} style={styles.paragraph}>
{children}
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
</Text>
),
mention: (node) => {
const { content, key } = node;
let mentionStyle = styles.mention;
if (content === 'all' || content === 'here') {
mentionStyle = {
...mentionStyle,
...styles.mentionAll
};
} else if (content === username) {
mentionStyle = {
...mentionStyle,
...styles.mentionLoggedUser
};
}
return (
<Text style={mentionStyle} key={key}>
&nbsp;{content}&nbsp;
</Text>
);
},
hashtag: node => (
<Text key={node.key} style={styles.mention}>
&nbsp;#{node.content}&nbsp;
</Text>
),
emoji: (node) => {
if (node.children && node.children.length && node.children[0].content) {
const { content } = node.children[0];
const emojiExtension = customEmojis[content];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content };
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
}
return <Text key={node.key}>:{content}:</Text>;
}
return null;
},
hardbreak: () => null,
blocklink: () => null,
image: node => (
<Image key={node.key} style={styles.inlineImage} source={{ uri: node.attributes.src }} />
),
...rules
}}
style={{
paragraph: styles.paragraph,
text: {
color: '#0C0D0F',
fontSize: 16,
letterSpacing: 0.1
},
codeInline: {
...codeFontFamily,
borderWidth: 1,
backgroundColor: '#f8f8f8',
borderRadius: 4
},
codeBlock: {
...codeFontFamily,
backgroundColor: '#f8f8f8',
borderColor: '#cccccc',
borderWidth: 1,
borderRadius: 4,
padding: 4
},
link: {
color: '#1D74F5'
},
...style
}}
plugins={[
new PluginContainer(MarkdownFlowdock),
new PluginContainer(MarkdownEmojiPlugin)
]}
>{m}
</MarkdownRenderer>
);
}
}
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
if (numberOfLines > 0) {
m = m.replace(/[\n]+/g, '\n').trim();
}
return (
<MarkdownRenderer
rules={{
paragraph: (node, children) => (
// eslint-disable-next-line
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
{children}
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
</Text>
),
mention: (node) => {
const { content, key } = node;
let mentionStyle = styles.mention;
if (content === 'all' || content === 'here') {
mentionStyle = {
...mentionStyle,
...styles.mentionAll
};
} else if (content === username) {
mentionStyle = {
...mentionStyle,
...styles.mentionLoggedUser
};
}
return (
<Text style={mentionStyle} key={key}>
&nbsp;{content}&nbsp;
</Text>
);
},
hashtag: node => (
<Text key={node.key} style={styles.mention}>
&nbsp;#{node.content}&nbsp;
</Text>
),
emoji: (node) => {
if (node.children && node.children.length && node.children[0].content) {
const { content } = node.children[0];
const emojiExtension = customEmojis[content];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content };
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
}
return <Text key={node.key}>:{content}:</Text>;
}
return null;
},
hardbreak: () => null,
blocklink: () => null,
image: node => (
<Image key={node.key} style={styles.inlineImage} source={{ uri: node.attributes.src }} />
),
...rules
}}
style={{
paragraph: styles.paragraph,
text: styles.text,
codeInline: styles.codeInline,
codeBlock: styles.codeBlock,
link: styles.link,
...style
}}
plugins={[
new PluginContainer(MarkdownFlowdock),
new PluginContainer(MarkdownEmojiPlugin)
]}
>{m}
</MarkdownRenderer>
);
}, (prevProps, nextProps) => prevProps.msg === nextProps.msg);
Markdown.propTypes = {
msg: PropTypes.string,
@ -132,5 +106,8 @@ Markdown.propTypes = {
customEmojis: PropTypes.object.isRequired,
style: PropTypes.any,
rules: PropTypes.object,
edited: PropTypes.bool
edited: PropTypes.bool,
numberOfLines: PropTypes.number
};
export default Markdown;

View File

@ -5,9 +5,9 @@ import {
} from 'react-native';
import moment from 'moment';
import { KeyboardUtils } from 'react-native-keyboard-input';
import {
State, RectButton, LongPressGestureHandler, BorderlessButton
} from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable';
import { emojify } from 'react-emojione';
import removeMarkdown from 'remove-markdown';
import Image from './Image';
import User from './User';
@ -23,6 +23,10 @@ import styles from './styles';
import I18n from '../../i18n';
import messagesStatus from '../../constants/messagesStatus';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_DANGER } from '../../constants/colors';
import debounce from '../../utils/debounce';
import DisclosureIndicator from '../DisclosureIndicator';
import sharedStyles from '../../views/Styles';
const SYSTEM_MESSAGES = [
'r',
@ -30,6 +34,7 @@ const SYSTEM_MESSAGES = [
'ru',
'ul',
'uj',
'ut',
'rm',
'user-muted',
'user-unmuted',
@ -40,7 +45,8 @@ const SYSTEM_MESSAGES = [
'room_changed_announcement',
'room_changed_topic',
'room_changed_privacy',
'message_snippeted'
'message_snippeted',
'thread-created'
];
const getInfoMessage = ({
@ -51,6 +57,8 @@ const getInfoMessage = ({
return I18n.t('Message_removed');
} else if (type === 'uj') {
return I18n.t('Has_joined_the_channel');
} else if (type === 'ut') {
return I18n.t('Has_joined_the_conversation');
} else if (type === 'r') {
return I18n.t('Room_name_changed', { name: msg, userBy: username });
} else if (type === 'message_pinned') {
@ -82,12 +90,16 @@ const getInfoMessage = ({
}
return '';
};
const BUTTON_HIT_SLOP = {
top: 4, right: 4, bottom: 4, left: 4
};
export default class Message extends PureComponent {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
timeFormat: PropTypes.string.isRequired,
customThreadTimeFormat: PropTypes.string,
msg: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
@ -108,6 +120,8 @@ export default class Message extends PureComponent {
reactionsModal: PropTypes.bool,
type: PropTypes.string,
header: PropTypes.bool,
isThreadReply: PropTypes.bool,
isThreadSequential: PropTypes.bool,
avatar: PropTypes.string,
alias: PropTypes.string,
ts: PropTypes.oneOfType([
@ -124,14 +138,23 @@ export default class Message extends PureComponent {
PropTypes.object
]),
useRealName: PropTypes.bool,
dcount: PropTypes.number,
dlm: PropTypes.instanceOf(Date),
tmid: PropTypes.string,
tcount: PropTypes.number,
tlm: PropTypes.instanceOf(Date),
tmsg: PropTypes.string,
// methods
closeReactions: PropTypes.func,
onErrorPress: PropTypes.func,
onLongPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
onThreadPress: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func
toggleReactionPicker: PropTypes.func,
fetchThreadName: PropTypes.func
}
static defaultProps = {
@ -143,9 +166,14 @@ export default class Message extends PureComponent {
onLongPress: () => {}
}
onPress = () => {
onPress = debounce(() => {
KeyboardUtils.dismiss();
}
const { onThreadPress, tlm, tmid } = this.props;
if ((tlm || tmid) && onThreadPress) {
onThreadPress();
}
}, 300, true)
onLongPress = () => {
const { archived, onLongPress } = this.props;
@ -155,6 +183,32 @@ export default class Message extends PureComponent {
onLongPress();
}
formatLastMessage = (lm) => {
const { customThreadTimeFormat } = this.props;
if (customThreadTimeFormat) {
return moment(lm).format(customThreadTimeFormat);
}
return lm ? moment(lm).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null;
}
formatMessageCount = (count, type) => {
const discussion = type === 'discussion';
let text = discussion ? I18n.t('No_messages_yet') : null;
if (count === 1) {
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
} else if (count > 1 && count < 1000) {
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
} else if (count > 999) {
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
}
return text;
}
isInfoMessage = () => {
const { type } = this.props;
return SYSTEM_MESSAGES.includes(type);
@ -180,20 +234,21 @@ export default class Message extends PureComponent {
return status === messagesStatus.ERROR;
}
renderAvatar = () => {
renderAvatar = (small = false) => {
const {
header, avatar, author, baseUrl, user
} = this.props;
if (header) {
return (
<Avatar
style={styles.avatar}
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={36}
borderRadius={4}
size={small ? 20 : 36}
borderRadius={small ? 2 : 4}
avatar={avatar}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
/>
);
}
@ -223,10 +278,25 @@ export default class Message extends PureComponent {
if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
}
const {
customEmojis, msg, baseUrl, user, edited
customEmojis, msg, baseUrl, user, edited, tmid
} = this.props;
return <Markdown msg={msg} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} edited={edited} />;
if (tmid && !msg) {
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
}
return (
<Markdown
msg={msg}
customEmojis={customEmojis}
baseUrl={baseUrl}
username={user.username}
edited={edited}
numberOfLines={tmid ? 1 : 0}
/>
);
}
renderAttachment() {
@ -254,13 +324,13 @@ export default class Message extends PureComponent {
}
renderUrl = () => {
const { urls } = this.props;
const { urls, user, baseUrl } = this.props;
if (urls.length === 0) {
return null;
}
return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} />
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
));
}
@ -270,9 +340,9 @@ export default class Message extends PureComponent {
}
const { onErrorPress } = this.props;
return (
<BorderlessButton onPress={onErrorPress} style={styles.errorButton}>
<CustomIcon name='circle-cross' color='red' size={20} />
</BorderlessButton>
<Touchable onPress={onErrorPress} style={styles.errorButton}>
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
</Touchable>
);
}
@ -281,31 +351,27 @@ export default class Message extends PureComponent {
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
} = this.props;
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
const underlayColor = reacted ? '#fff' : '#e1e5e8';
return (
<LongPressGestureHandler
<Touchable
onPress={() => onReactionPress(reaction.emoji)}
onLongPress={onReactionLongPress}
key={reaction.emoji}
onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && onReactionLongPress()}
testID={`message-reaction-${ reaction.emoji }`}
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
>
<RectButton
onPress={() => onReactionPress(reaction.emoji)}
testID={`message-reaction-${ reaction.emoji }`}
style={[styles.reactionButton, reacted && { backgroundColor: '#e8f2ff' }]}
activeOpacity={0.8}
underlayColor={underlayColor}
>
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
<Emoji
content={reaction.emoji}
customEmojis={customEmojis}
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={baseUrl}
/>
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
</View>
</RectButton>
</LongPressGestureHandler>
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
<Emoji
content={reaction.emoji}
customEmojis={customEmojis}
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={baseUrl}
/>
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
</View>
</Touchable>
);
}
@ -317,18 +383,18 @@ export default class Message extends PureComponent {
return (
<View style={styles.reactionsContainer}>
{reactions.map(this.renderReaction)}
<RectButton
<Touchable
onPress={toggleReactionPicker}
key='message-add-reaction'
testID='message-add-reaction'
style={styles.reactionButton}
activeOpacity={0.8}
underlayColor='#e1e5e8'
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
>
<View style={styles.reactionContainer}>
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
</View>
</RectButton>
</Touchable>
</View>
);
}
@ -337,23 +403,176 @@ export default class Message extends PureComponent {
const { broadcast, replyBroadcast } = this.props;
if (broadcast && !this.isOwn()) {
return (
<RectButton
onPress={replyBroadcast}
style={styles.broadcastButton}
activeOpacity={0.5}
underlayColor='#fff'
>
<CustomIcon name='back' size={20} style={styles.broadcastButtonIcon} />
<Text style={styles.broadcastButtonText}>{I18n.t('Reply')}</Text>
</RectButton>
<View style={styles.buttonContainer}>
<Touchable
onPress={replyBroadcast}
background={Touchable.Ripple('#fff')}
style={styles.button}
hitSlop={BUTTON_HIT_SLOP}
>
<React.Fragment>
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
</React.Fragment>
</Touchable>
</View>
);
}
return null;
}
renderDiscussion = () => {
const {
msg, dcount, dlm, onDiscussionPress
} = this.props;
const time = this.formatLastMessage(dlm);
const buttonText = this.formatMessageCount(dcount, 'discussion');
return (
<React.Fragment>
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
<Text style={styles.text}>{msg}</Text>
<View style={styles.buttonContainer}>
<Touchable
onPress={onDiscussionPress}
background={Touchable.Ripple('#fff')}
style={[styles.button, styles.smallButton]}
hitSlop={BUTTON_HIT_SLOP}
>
<React.Fragment>
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
<Text style={styles.buttonText}>{buttonText}</Text>
</React.Fragment>
</Touchable>
<Text style={styles.time}>{time}</Text>
</View>
</React.Fragment>
);
}
renderThread = () => {
const {
tcount, tlm, onThreadPress, msg
} = this.props;
if (!tlm) {
return null;
}
const time = this.formatLastMessage(tlm);
const buttonText = this.formatMessageCount(tcount, 'thread');
return (
<View style={styles.buttonContainer}>
<Touchable
onPress={onThreadPress}
background={Touchable.Ripple('#fff')}
style={[styles.button, styles.smallButton]}
hitSlop={BUTTON_HIT_SLOP}
testID={`message-thread-button-${ msg }`}
>
<React.Fragment>
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
<Text style={styles.buttonText}>{buttonText}</Text>
</React.Fragment>
</Touchable>
<Text style={styles.time}>{time}</Text>
</View>
);
}
renderRepliedThread = () => {
const {
tmid, tmsg, header, fetchThreadName
} = this.props;
if (!tmid || !header || this.isTemp()) {
return null;
}
if (!tmsg) {
fetchThreadName(tmid);
return null;
}
let msg = emojify(tmsg, { output: 'unicode' });
msg = removeMarkdown(msg);
return (
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} />
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
<DisclosureIndicator />
</View>
);
}
renderInner = () => {
const { type } = this.props;
if (type === 'discussion-created') {
return (
<React.Fragment>
{this.renderUsername()}
{this.renderDiscussion()}
</React.Fragment>
);
}
return (
<React.Fragment>
{this.renderUsername()}
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderThread()}
{this.renderReactions()}
{this.renderBroadcastReply()}
</React.Fragment>
);
}
renderMessage = () => {
const { header, isThreadReply, isThreadSequential } = this.props;
if (isThreadReply || isThreadSequential || this.isInfoMessage()) {
const thread = isThreadReply ? this.renderRepliedThread() : null;
return (
<React.Fragment>
{thread}
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
{this.renderAvatar(true)}
<View
style={[
styles.messageContent,
header && styles.messageContentWithHeader,
this.hasError() && header && styles.messageContentWithHeader,
this.hasError() && !header && styles.messageContentWithError,
this.isTemp() && styles.temp
]}
>
{this.renderContent()}
</View>
</View>
</React.Fragment>
);
}
return (
<View style={styles.flex}>
{this.renderAvatar()}
<View
style={[
styles.messageContent,
header && styles.messageContentWithHeader,
this.hasError() && header && styles.messageContentWithHeader,
this.hasError() && !header && styles.messageContentWithError,
this.isTemp() && styles.temp
]}
>
{this.renderInner()}
</View>
</View>
);
}
render() {
const {
editing, style, header, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
editing, style, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
} = this.props;
const accessibilityLabel = I18n.t('Message_accessibility', { user: author.username, time: moment(ts).format(timeFormat), message: msg });
@ -365,28 +584,10 @@ export default class Message extends PureComponent {
onPress={this.onPress}
>
<View
style={[styles.container, header && styles.marginBottom, editing && styles.editing, style]}
style={[styles.container, editing && styles.editing, style]}
accessibilityLabel={accessibilityLabel}
>
<View style={styles.flex}>
{this.renderAvatar()}
<View
style={[
styles.messageContent,
header && styles.messageContentWithHeader,
this.hasError() && header && styles.messageContentWithHeader,
this.hasError() && !header && styles.messageContentWithError,
this.isTemp() && styles.temp
]}
>
{this.renderUsername()}
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderReactions()}
{this.renderBroadcastReply()}
</View>
</View>
{this.renderMessage()}
{reactionsModal
? (
<ReactionsModal

View File

@ -8,6 +8,9 @@ import Modal from 'react-native-modal';
import ImageViewer from 'react-native-image-zoom-viewer';
import { responsive } from 'react-native-responsive-ui';
import sharedStyles from '../../views/Styles';
import { COLOR_WHITE } from '../../constants/colors';
const styles = StyleSheet.create({
imageWrapper: {
flex: 1
@ -18,16 +21,16 @@ const styles = StyleSheet.create({
marginVertical: 10
},
title: {
color: '#ffffff',
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
fontWeight: '600'
...sharedStyles.textSemibold
},
description: {
color: '#ffffff',
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 14,
fontWeight: '500'
...sharedStyles.textMedium
},
indicatorContainer: {
alignItems: 'center',

View File

@ -8,6 +8,8 @@ import Modal from 'react-native-modal';
import Emoji from './Emoji';
import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { COLOR_WHITE } from '../../constants/colors';
const styles = StyleSheet.create({
titleContainer: {
@ -16,18 +18,20 @@ const styles = StyleSheet.create({
paddingVertical: 10
},
title: {
color: '#ffffff',
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
fontWeight: '600'
...sharedStyles.textSemibold
},
reactCount: {
color: '#dddddd',
fontSize: 10
color: COLOR_WHITE,
fontSize: 13,
...sharedStyles.textRegular
},
peopleReacted: {
color: '#ffffff',
fontWeight: '500'
color: COLOR_WHITE,
fontSize: 14,
...sharedStyles.textMedium
},
peopleItemContainer: {
flex: 1,
@ -51,7 +55,7 @@ const styles = StyleSheet.create({
position: 'absolute',
left: 0,
top: 10,
color: '#ffffff'
color: COLOR_WHITE
}
});
const standardEmojiStyle = { fontSize: 20 };

View File

@ -2,19 +2,23 @@ import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import { RectButton } from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginTop: 15,
marginTop: 6,
alignSelf: 'flex-end',
backgroundColor: '#f3f4f5',
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4
},
attachmentContainer: {
@ -30,16 +34,16 @@ const styles = StyleSheet.create({
},
author: {
flex: 1,
color: '#0C0D0F',
fontSize: 16,
fontWeight: '500',
marginRight: 10
...sharedStyles.textColorNormal,
...sharedStyles.textMedium
},
time: {
fontSize: 12,
fontWeight: 'normal',
color: '#9ea2a8',
marginLeft: 5
marginLeft: 10,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular,
fontWeight: '300'
},
fieldsContainer: {
flex: 1,
@ -51,7 +55,14 @@ const styles = StyleSheet.create({
padding: 10
},
fieldTitle: {
fontWeight: 'bold'
fontSize: 14,
...sharedStyles.textColorNormal,
...sharedStyles.textSemibold
},
fieldValue: {
fontSize: 14,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
marginTop: {
marginTop: 4
@ -86,7 +97,7 @@ const Reply = ({
};
const renderTitle = () => {
if (!(attachment.author_icon || attachment.author_name || attachment.ts)) {
if (!attachment.author_name) {
return null;
}
return (
@ -121,7 +132,7 @@ const Reply = ({
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={styles.fieldTitle}>{field.title}</Text>
<Text>{field.value}</Text>
<Text style={styles.fieldValue}>{field.value}</Text>
</View>
))}
</View>
@ -129,18 +140,17 @@ const Reply = ({
};
return (
<RectButton
<Touchable
onPress={() => onPress(attachment, baseUrl, user)}
style={[styles.button, index > 0 && styles.marginTop]}
activeOpacity={0.5}
underlayColor='#fff'
background={Touchable.Ripple('#fff')}
>
<View style={styles.attachmentContainer}>
{renderTitle()}
{renderText()}
{renderFields()}
</View>
</RectButton>
</Touchable>
);
};

View File

@ -2,9 +2,14 @@ import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'lodash/isEqual';
import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import {
COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY
} from '../../constants/colors';
const styles = StyleSheet.create({
button: {
@ -14,8 +19,8 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'column',
borderRadius: 4,
backgroundColor: '#F3F4F5',
borderColor: '#F3F4F5',
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1
},
textContainer: {
@ -26,14 +31,14 @@ const styles = StyleSheet.create({
alignItems: 'flex-start'
},
title: {
fontWeight: '500',
color: '#1D74F5',
fontSize: 16
color: COLOR_PRIMARY,
fontSize: 16,
...sharedStyles.textMedium
},
description: {
marginTop: 5,
fontSize: 16,
color: '#0C0D0F'
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
marginTop: {
marginTop: 4
@ -46,32 +51,60 @@ const styles = StyleSheet.create({
}
});
const onPress = (url) => {
openLink(url);
};
const Url = ({ url, index }) => {
const UrlImage = React.memo(({ image, user, baseUrl }) => {
if (!image) {
return null;
}
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} />;
});
const UrlContent = React.memo(({ title, description }) => (
<View style={styles.textContainer}>
{title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null}
{description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null}
</View>
));
const Url = React.memo(({
url, index, user, baseUrl
}) => {
if (!url) {
return null;
}
const onPress = () => openLink(url.url);
return (
<RectButton
onPress={() => onPress(url.url)}
<Touchable
onPress={onPress}
style={[styles.button, index > 0 && styles.marginTop, styles.container]}
activeOpacity={0.5}
underlayColor='#fff'
background={Touchable.Ripple('#fff')}
>
{url.image ? <FastImage source={{ uri: url.image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} /> : null}
<View style={styles.textContainer}>
<Text style={styles.title} numberOfLines={2}>{url.title}</Text>
<Text style={styles.description} numberOfLines={2}>{url.description}</Text>
</View>
</RectButton>
<React.Fragment>
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
<UrlContent title={url.title} description={url.description} />
</React.Fragment>
</Touchable>
);
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
UrlImage.propTypes = {
image: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string
};
UrlContent.propTypes = {
title: PropTypes.string,
description: PropTypes.string
};
Url.propTypes = {
url: PropTypes.object.isRequired,
index: PropTypes.number
index: PropTypes.number,
user: PropTypes.object,
baseUrl: PropTypes.string
};
export default Url;

View File

@ -3,18 +3,20 @@ import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import moment from 'moment';
import sharedStyles from '../../views/Styles';
import messageStyles from './styles';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 2
alignItems: 'center'
},
username: {
color: '#0C0D0F',
fontWeight: '600',
fontSize: 16,
lineHeight: 22
lineHeight: 22,
...sharedStyles.textColorNormal,
...sharedStyles.textMedium
},
titleContainer: {
flex: 1,
@ -23,16 +25,8 @@ const styles = StyleSheet.create({
},
alias: {
fontSize: 14,
color: '#9EA2A8',
paddingLeft: 6,
lineHeight: 16
},
time: {
fontSize: 12,
color: '#9EA2A8',
paddingLeft: 10,
fontWeight: '300',
lineHeight: 16
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
}
});
@ -58,18 +52,18 @@ export default class User extends React.PureComponent {
extraStyle.opacity = 0.3;
}
const aliasUsername = alias ? (<Text style={styles.alias}>@{username}</Text>) : null;
const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
const time = moment(ts).format(timeFormat);
return (
<View style={styles.container}>
<View style={styles.titleContainer}>
<Text style={styles.username}>
<Text style={styles.username} numberOfLines={1}>
{alias || username}
{aliasUsername}
</Text>
{aliasUsername}
</View>
<Text style={styles.time}>{time}</Text>
<Text style={messageStyles.time}>{time}</Text>
</View>
);
}

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { StyleSheet, View } from 'react-native';
import Modal from 'react-native-modal';
import VideoPlayer from 'react-native-video-controls';
import { RectButton } from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
@ -19,7 +19,7 @@ const styles = StyleSheet.create({
borderRadius: 4,
height: 150,
backgroundColor: '#1f2329',
marginBottom: 10,
marginBottom: 6,
alignItems: 'center',
justifyContent: 'center'
},
@ -48,13 +48,13 @@ export default class Video extends React.PureComponent {
return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
}
toggleModal() {
toggleModal = () => {
this.setState(prevState => ({
isVisible: !prevState.isVisible
}));
}
open() {
open = () => {
const { file } = this.props;
if (isTypeSupported(file.video_type)) {
return this.toggleModal();
@ -76,18 +76,17 @@ export default class Video extends React.PureComponent {
return (
[
<View key='button'>
<RectButton
<Touchable
onPress={this.open}
style={styles.button}
onPress={() => this.open()}
activeOpacity={0.5}
underlayColor='#fff'
background={Touchable.Ripple('#fff')}
>
<CustomIcon
name='play'
size={54}
style={styles.image}
/>
</RectButton>
</Touchable>
<Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />
</View>,
<Modal
@ -99,7 +98,7 @@ export default class Video extends React.PureComponent {
>
<VideoPlayer
source={{ uri: this.uri }}
onBack={() => this.toggleModal()}
onBack={this.toggleModal}
disableVolume
/>
</Modal>

View File

@ -11,6 +11,7 @@ import {
replyBroadcast as replyBroadcastAction
} from '../../actions/messages';
import { vibrate } from '../../utils/vibration';
import debounce from '../../utils/debounce';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@ -27,15 +28,14 @@ import { vibrate } from '../../utils/vibration';
export default class MessageContainer extends React.Component {
static propTypes = {
item: PropTypes.object.isRequired,
reactions: PropTypes.any.isRequired,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
customTimeFormat: PropTypes.string,
customThreadTimeFormat: PropTypes.string,
style: ViewPropTypes.style,
status: PropTypes.number,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
previousItem: PropTypes.object,
@ -47,13 +47,17 @@ export default class MessageContainer extends React.Component {
Message_TimeFormat: PropTypes.string,
editingMessage: PropTypes.object,
useRealName: PropTypes.bool,
status: PropTypes.number,
navigation: PropTypes.object,
// methods - props
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
// methods - redux
errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func
toggleReactionPicker: PropTypes.func,
fetchThreadName: PropTypes.func
}
static defaultProps = {
@ -72,7 +76,7 @@ export default class MessageContainer extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { reactionsModal } = this.state;
const {
status, reactions, broadcast, _updatedAt, editingMessage, item
status, editingMessage, item, _updatedAt, navigation
} = this.props;
if (reactionsModal !== nextState.reactionsModal) {
@ -81,27 +85,20 @@ export default class MessageContainer extends React.Component {
if (status !== nextProps.status) {
return true;
}
// eslint-disable-next-line
if (!!_updatedAt ^ !!nextProps._updatedAt) {
if (item.tmsg !== nextProps.item.tmsg) {
return true;
}
if (!equal(reactions, nextProps.reactions)) {
return true;
}
if (broadcast !== nextProps.broadcast) {
return true;
}
if (!equal(editingMessage, nextProps.editingMessage)) {
if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
return true;
} else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {
return true;
}
}
return _updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString();
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
}
onLongPress = () => {
const { onLongPress } = this.props;
onLongPress(this.parseMessage());
@ -117,12 +114,30 @@ export default class MessageContainer extends React.Component {
onReactionPress(emoji, item._id);
}
onReactionLongPress = () => {
this.setState({ reactionsModal: true });
vibrate();
}
onDiscussionPress = () => {
const { onDiscussionPress, item } = this.props;
onDiscussionPress(item);
}
onThreadPress = debounce(() => {
const { navigation, item } = this.props;
if (item.tmid) {
navigation.push('RoomView', {
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
});
} else if (item.tlm) {
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
navigation.push('RoomView', {
rid: item.rid, tmid: item._id, name: title, t: 'thread'
});
}
}, 1000, true)
get timeFormat() {
const { customTimeFormat, Message_TimeFormat } = this.props;
return customTimeFormat || Message_TimeFormat;
@ -141,12 +156,33 @@ export default class MessageContainer extends React.Component {
&& (previousItem.u.username === item.u.username)
&& !(previousItem.groupable === false || item.groupable === false || broadcast === true)
&& (item.ts - previousItem.ts < Message_GroupingPeriod * 1000)
&& (previousItem.tmid === item.tmid)
)) {
return false;
}
return true;
}
isThreadReply = () => {
const {
item, previousItem
} = this.props;
if (previousItem && item.tmid && (previousItem.tmid !== item.tmid) && (previousItem._id !== item.tmid)) {
return true;
}
return false;
}
isThreadSequential = () => {
const {
item, previousItem
} = this.props;
if (previousItem && item.tmid && ((previousItem.tmid === item.tmid) || (previousItem._id === item.tmid))) {
return true;
}
return false;
}
parseMessage = () => {
const { item } = this.props;
return JSON.parse(JSON.stringify(item));
@ -165,14 +201,15 @@ export default class MessageContainer extends React.Component {
render() {
const { reactionsModal } = this.state;
const {
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast, fetchThreadName, customThreadTimeFormat
} = this.props;
const {
msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role
_id, msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg
} = item;
const isEditing = editingMessage._id === item._id;
return (
<Message
id={_id}
msg={msg}
author={u}
ts={ts}
@ -184,10 +221,13 @@ export default class MessageContainer extends React.Component {
alias={alias}
editing={isEditing}
header={this.isHeader()}
isThreadReply={this.isThreadReply()}
isThreadSequential={this.isThreadSequential()}
avatar={avatar}
user={user}
edited={editedBy && !!editedBy.username}
timeFormat={this.timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style}
archived={archived}
broadcast={broadcast}
@ -196,6 +236,14 @@ export default class MessageContainer extends React.Component {
reactionsModal={reactionsModal}
useRealName={useRealName}
role={role}
drid={drid}
dcount={dcount}
dlm={dlm}
tmid={tmid}
tcount={tcount}
tlm={tlm}
tmsg={tmsg}
fetchThreadName={fetchThreadName}
closeReactions={this.closeReactions}
onErrorPress={this.onErrorPress}
onLongPress={this.onLongPress}
@ -203,6 +251,8 @@ export default class MessageContainer extends React.Component {
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
toggleReactionPicker={this.toggleReactionPicker}
onDiscussionPress={this.onDiscussionPress}
onThreadPress={this.onThreadPress}
/>
);
}

View File

@ -1,15 +1,24 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Platform } from 'react-native';
import sharedStyles from '../../views/Styles';
import {
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER
} from '../../constants/colors';
const codeFontFamily = Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
});
export default StyleSheet.create({
root: {
flexDirection: 'row'
},
container: {
paddingVertical: 5,
paddingVertical: 4,
width: '100%',
paddingHorizontal: 15,
paddingHorizontal: 14,
flexDirection: 'column',
transform: [{ scaleY: -1 }],
flex: 1
},
messageContent: {
@ -26,10 +35,16 @@ export default StyleSheet.create({
flexDirection: 'row',
flex: 1
},
text: {
fontSize: 16,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
textInfo: {
fontStyle: 'italic',
color: '#a0a0a0',
fontSize: 16
fontSize: 16,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
editing: {
backgroundColor: '#fff5df'
@ -39,38 +54,41 @@ export default StyleSheet.create({
height: 20
},
temp: { opacity: 0.3 },
marginBottom: {
marginBottom: 10
marginTop: {
marginTop: 6
},
reactionsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 10
marginTop: 6
},
reactionButton: {
marginRight: 10,
marginBottom: 10,
marginRight: 6,
marginBottom: 6,
borderRadius: 2
},
reactionButtonReacted: {
backgroundColor: '#e8f2ff'
},
reactionContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 4,
borderWidth: 1.5,
borderColor: '#e1e5e8',
borderRadius: 2,
borderWidth: 1,
borderColor: COLOR_BORDER,
height: 28,
minWidth: 46
minWidth: 46.3
},
reactedContainer: {
borderColor: '#1d74f580'
borderColor: COLOR_PRIMARY
},
reactionCount: {
fontSize: 14,
marginLeft: 3,
marginRight: 8.5,
fontWeight: '600',
color: '#1D74F5'
color: COLOR_PRIMARY,
...sharedStyles.textSemibold
},
reactionEmoji: {
fontSize: 13,
@ -82,47 +100,56 @@ export default StyleSheet.create({
marginLeft: 7
},
avatar: {
marginTop: 5
marginTop: 4
},
avatarSmall: {
marginLeft: 16
},
addReaction: {
color: '#1D74F5'
color: COLOR_PRIMARY
},
errorButton: {
paddingHorizontal: 15,
paddingVertical: 5
},
broadcastButton: {
width: 107,
buttonContainer: {
marginTop: 6,
flexDirection: 'row',
alignItems: 'center'
},
button: {
paddingHorizontal: 15,
height: 44,
marginTop: 15,
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1d74f5',
borderRadius: 4
backgroundColor: COLOR_PRIMARY,
borderRadius: 2
},
broadcastButtonIcon: {
color: '#fff',
marginRight: 11
smallButton: {
height: 30
},
broadcastButtonText: {
color: '#fff',
buttonIcon: {
color: COLOR_WHITE,
marginRight: 6
},
buttonText: {
color: COLOR_WHITE,
fontSize: 14,
fontWeight: '500'
...sharedStyles.textMedium
},
mention: {
...sharedStyles.textMedium,
color: '#0072FE',
fontWeight: '500',
padding: 5,
backgroundColor: '#E8F2FF'
},
mentionLoggedUser: {
color: '#fff',
backgroundColor: '#1D74F5'
color: COLOR_WHITE,
backgroundColor: COLOR_PRIMARY
},
mentionAll: {
color: '#fff',
color: COLOR_WHITE,
backgroundColor: '#FF5B5A'
},
paragraph: {
@ -136,8 +163,6 @@ export default StyleSheet.create({
imageContainer: {
flex: 1,
flexDirection: 'column',
borderColor: '#F3F4F5',
borderWidth: 1,
borderRadius: 4
},
image: {
@ -145,7 +170,8 @@ export default StyleSheet.create({
maxWidth: 400,
minHeight: 200,
borderRadius: 4,
marginBottom: 10
borderColor: COLOR_BORDER,
borderWidth: 1
},
inlineImage: {
width: 300,
@ -154,6 +180,60 @@ export default StyleSheet.create({
},
edited: {
fontSize: 14,
color: '#9EA2A8'
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
codeInline: {
...sharedStyles.textRegular,
...codeFontFamily,
borderWidth: 1,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderRadius: 4
},
codeBlock: {
...sharedStyles.textRegular,
...codeFontFamily,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4,
padding: 4
},
link: {
color: COLOR_PRIMARY,
...sharedStyles.textRegular
},
startedDiscussion: {
fontStyle: 'italic',
fontSize: 16,
marginBottom: 6,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
time: {
fontSize: 12,
paddingLeft: 10,
lineHeight: 22,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular,
fontWeight: '300'
},
repliedThread: {
flexDirection: 'row',
flex: 1,
alignItems: 'center',
marginTop: 6,
marginBottom: 12
},
repliedThreadIcon: {
color: COLOR_PRIMARY,
marginRight: 10,
marginLeft: 16
},
repliedThreadName: {
fontSize: 16,
flex: 1,
color: COLOR_PRIMARY,
...sharedStyles.textRegular
}
});

View File

@ -5,12 +5,13 @@ import fr from './locales/fr';
import de from './locales/de';
import ptBR from './locales/pt-BR';
import zhCN from './locales/zh-CN';
import ptPT from './locales/pt-PT';
I18n.fallbacks = true;
I18n.defaultLocale = 'en';
I18n.translations = {
en, ru, 'pt-BR': ptBR, 'zh-CN': zhCN, fr, de
en, ru, 'pt-BR': ptBR, 'zh-CN': zhCN, fr, de, 'pt-PT': ptPT
};
export default I18n;

View File

@ -124,6 +124,7 @@ export default {
Connect: 'Connect',
Connect_to_a_server: 'Connect to a server',
Connected: 'Connected',
connecting_server: 'connecting to server',
Connecting: 'Connecting...',
Continue_with: 'Continue with',
Copied_to_clipboard: 'Copied to clipboard!',
@ -141,6 +142,7 @@ export default {
description: 'description',
Description: 'Description',
Disable_notifications: 'Disable notifications',
Discussions: 'Discussions',
Direct_Messages: 'Direct Messages',
Dont_Have_An_Account: 'Don\'t have an account?',
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
@ -158,6 +160,7 @@ export default {
File_description: 'File description',
File_name: 'File name',
Finish_recording: 'Finish recording',
Following_thread: 'Following thread',
For_your_security_you_must_enter_your_current_password_to_continue: 'For your security, you must enter your current password to continue',
Forgot_my_password: 'Forgot my password',
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.',
@ -166,6 +169,7 @@ export default {
Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type',
Has_joined_the_channel: 'Has joined the channel',
Has_joined_the_conversation: 'Has joined the conversation',
Has_left_the_channel: 'Has left the channel',
Invisible: 'Invisible',
Invite: 'Invite',
@ -196,6 +200,8 @@ export default {
Message_actions: 'Message actions',
Message_pinned: 'Message pinned',
Message_removed: 'Message removed',
message: 'message',
messages: 'messages',
Messages: 'Messages',
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
Microphone_Permission: 'Microphone Permission',
@ -215,10 +221,12 @@ export default {
No_pinned_messages: 'No pinned messages',
No_results_found: 'No results found',
No_starred_messages: 'No starred messages',
No_thread_messages: 'No thread messages',
No_announcement_provided: 'No announcement provided.',
No_description_provided: 'No description provided.',
No_topic_provided: 'No topic provided.',
No_Message: 'No Message',
No_messages_yet: 'No messages yet',
No_Reactions: 'No Reactions',
Not_logged: 'Not logged',
Nothing_to_save: 'Nothing to save!',
@ -254,6 +262,9 @@ export default {
Read_Only: 'Read Only',
Register: 'Register',
Repeat_Password: 'Repeat Password',
Replied_on: 'Replied on:',
replies: 'replies',
reply: 'reply',
Reply: 'Reply',
Resend: 'Resend',
Reset_password: 'Reset password',
@ -283,6 +294,7 @@ export default {
Send: 'Send',
Send_audio_message: 'Send audio message',
Send_message: 'Send message',
Sent_an_attachment: 'Sent an attachment',
Server: 'Server',
Servers: 'Servers',
Set_username_subtitle: 'The username is used to allow others to mention you in messages',
@ -299,6 +311,7 @@ export default {
starred: 'starred',
Starred: 'Starred',
Start_of_conversation: 'Start of conversation',
Started_discussion: 'Started a discussion:',
Submit: 'Submit',
Take_a_photo: 'Take a photo',
tap_to_change_status: 'tap to change status',
@ -308,6 +321,8 @@ export default {
There_was_an_error_while_action: 'There was an error while {{action}}!',
This_room_is_blocked: 'This room is blocked',
This_room_is_read_only: 'This room is read only',
Thread: 'Thread',
Threads: 'Threads',
Timezone: 'Timezone',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'topic',
@ -318,6 +333,7 @@ export default {
unarchive: 'unarchive',
UNARCHIVE: 'UNARCHIVE',
Unblock_user: 'Unblock user',
Unfollowed_thread: 'Unfollowed thread',
Unmute: 'Unmute',
unmuted: 'unmuted',
Unpin: 'Unpin',
@ -349,7 +365,7 @@ export default {
Yesterday: 'Yesterday',
You_are_in_preview_mode: 'You are in preview mode',
You_are_offline: 'You are offline',
You_can_search_using_RegExp_eg: 'You can search using RegExp. e.g. `/^text$/i`',
You_can_search_using_RegExp_eg: 'You can use RegExp. e.g. `/^text$/i`',
You_colon: 'You: ',
you_were_mentioned: 'you were mentioned',
you: 'you',

View File

@ -131,6 +131,7 @@ export default {
Connect: 'Conectar',
Connect_to_a_server: 'Conectar a um servidor',
Connected: 'Conectado',
connecting_server: 'conectando no servidor',
Connecting: 'Conectando...',
Continue_with: 'Entrar com',
Copied_to_clipboard: 'Copiado para a área de transferência!',
@ -148,6 +149,7 @@ export default {
description: 'descrição',
Description: 'Descrição',
Disable_notifications: 'Desabilitar notificações',
Discussions: 'Discussões',
Direct_Messages: 'Mensagens Diretas',
Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
@ -165,6 +167,7 @@ export default {
File_description: 'Descrição do arquivo',
File_name: 'Nome do arquivo',
Finish_recording: 'Encerrar gravação',
Following_thread: 'Começou a seguir tópico',
For_your_security_you_must_enter_your_current_password_to_continue: 'Para sua segurança, você precisa digitar sua senha',
Forgot_my_password: 'Esqueci minha senha',
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.',
@ -173,6 +176,7 @@ export default {
Group_by_favorites: 'Agrupar favoritos',
Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal',
Has_joined_the_conversation: 'Entrou na conversa',
Has_left_the_channel: 'Saiu da conversa',
Invisible: 'Invisível',
Invite: 'Convidar',
@ -200,6 +204,8 @@ export default {
Message_actions: 'Ações',
Message_pinned: 'Fixou uma mensagem',
Message_removed: 'Mensagem removida',
message: 'mensagem',
messages: 'mensagens',
Messages: 'Mensagens',
Microphone_Permission_Message: 'Rocket Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.',
Microphone_Permission: 'Acesso ao Microfone',
@ -218,10 +224,12 @@ export default {
No_pinned_messages: 'Não há mensagens fixadas',
No_results_found: 'Nenhum resultado encontrado',
No_starred_messages: 'Não há mensagens favoritas',
No_thread_messages: 'Não há tópicos',
No_announcement_provided: 'Sem anúncio.',
No_description_provided: 'Sem descrição.',
No_topic_provided: 'Sem tópico.',
No_Message: 'Não há mensagens',
No_messages_yet: 'Não há mensagens ainda',
No_Reactions: 'Sem reações',
Nothing_to_save: 'Nada para salvar!',
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
@ -256,6 +264,9 @@ export default {
Read_Only: 'Somente Leitura',
Register: 'Registrar',
Repeat_Password: 'Repetir Senha',
Replied_on: 'Respondido em:',
replies: 'respostas',
reply: 'resposta',
Reply: 'Responder',
Resend: 'Reenviar',
Reset_password: 'Resetar senha',
@ -285,6 +296,7 @@ export default {
Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem',
Sent_an_attachment: 'Enviou um anexo',
Server: 'Servidor',
Set_username_subtitle: 'O usuário é utilizado para permitir que você seja mencionado em mensagens',
Settings: 'Configurações',
@ -300,6 +312,7 @@ export default {
starred: 'favoritou',
Starred: 'Mensagens Favoritas',
Start_of_conversation: 'Início da conversa',
Started_discussion: 'Iniciou uma discussão:',
Submit: 'Enviar',
Take_a_photo: 'Tirar uma foto',
Terms_of_Service: ' Termos de Serviço ',
@ -307,6 +320,8 @@ export default {
There_was_an_error_while_action: 'Aconteceu um erro {{action}}!',
This_room_is_blocked: 'Este quarto está bloqueado',
This_room_is_read_only: 'Este quarto é apenas de leitura',
Thread: 'Tópico',
Threads: 'Tópicos',
Timezone: 'Fuso horário',
topic: 'tópico',
Topic: 'Tópico',
@ -316,6 +331,7 @@ export default {
unarchive: 'desarquivar',
UNARCHIVE: 'DESARQUIVAR',
Unblock_user: 'Desbloquear usuário',
Unfollowed_thread: 'Parou de seguir tópico',
Unmute: 'Permitir que o usuário fale',
unmuted: 'permitiu que o usuário fale',
Unpin: 'Desafixar Mensagem',
@ -346,7 +362,7 @@ export default {
Yesterday: 'Ontem',
You_are_in_preview_mode: 'Está é uma prévia do canal',
You_are_offline: 'Você está offline',
You_can_search_using_RegExp_eg: 'Você pode pesquisar usando expressões regulares, por exemplo `/^text$/i`',
You_can_search_using_RegExp_eg: 'Você pode usar expressões regulares, por exemplo `/^text$/i`',
You_colon: 'Você: ',
you_were_mentioned: 'você foi mencionado',
you: 'você',

360
app/i18n/locales/pt-PT.js Normal file
View File

@ -0,0 +1,360 @@
export default {
'1_person_reacted': '1 pessoa reagiu',
'1_user': '1 utilizador',
'error-action-not-allowed': '{{action}} não é permitida',
'error-application-not-found': 'Aplicação não encontrada',
'error-archived-duplicate-name': 'Existe um canal arquivado com o nome {{room_name}}',
'error-avatar-invalid-url': 'URL de avatar inválido: {{url}}',
'error-avatar-url-handling': 'Erro ao manipular a configuração de avatar de um URL ({{url}}) para {{username}}',
'error-cant-invite-for-direct-room': 'Não pode convidar utilizadores para salas de mensagens directas',
'error-could-not-change-email': 'Não foi possível alterar o e-mail',
'error-could-not-change-name': 'Não foi possível alterar o nome',
'error-could-not-change-username': 'Não foi possível alterar o nome de utilizador',
'error-delete-protected-role': 'Não é possível eliminar uma função protegida',
'error-department-not-found': 'Departamento não encontrado',
'error-direct-message-file-upload-not-allowed': 'Partilha de ficheiros não permitido em mensagens diretas',
'error-duplicate-channel-name': 'Um canal com o nome {{channel_name}} existe',
'error-email-domain-blacklisted': 'O domínio de e-mail está na lista negra',
'error-email-send-failed': 'Erro ao tentar enviar e-mail: {{message}}',
'error-field-unavailable': '{{field}} já está em uso :(',
'error-file-too-large': 'Ficheiro demasiado grande',
'error-importer-not-defined': 'O importador não foi definido correctamente, a classe Import está em falta.',
'error-input-is-not-a-valid-field': '{{input}} não é um {{field}} válido',
'error-invalid-actionlink': 'Link de acção inválido',
'error-invalid-arguments': 'Argumentos inválidos',
'error-invalid-asset': 'Ficheiro inválida',
'error-invalid-channel': 'Canal inválido.',
'error-invalid-channel-start-with-chars': 'Canal inválido. Começa por @ ou #',
'error-invalid-custom-field': 'Campo personalizado inválido',
'error-invalid-custom-field-name': 'Nome de campo personalizado inválido. Use apenas letras, números, hífens e sublinhados.',
'error-invalid-date': 'Data inválida fornecida.',
'error-invalid-description': 'Descrição inválida',
'error-invalid-domain': 'Domínio inválido',
'error-invalid-email': 'E-mail inválido {{emai}}',
'error-invalid-email-address': 'Endereço de e-mail invalido',
'error-invalid-file-height': 'Altura de ficheiro inválida',
'error-invalid-file-type': 'Tipo de ficheiro inválido',
'error-invalid-file-width': 'Largura de ficheiro inválida',
'error-invalid-from-address': 'Você informou um endereço DE inválido.',
'error-invalid-integration': 'Integração inválida',
'error-invalid-message': 'Mensagem inválida',
'error-invalid-method': 'Método inválido',
'error-invalid-name': 'Nome inválido',
'error-invalid-password': 'Palavra-passe inválida',
'error-invalid-redirectUri': 'redirectUri inválido',
'error-invalid-role': 'Função inválido',
'error-invalid-room': 'Sala inválida',
'error-invalid-room-name': '{{room_name}} não é um nome de sala válido',
'error-invalid-room-type': '{{type}} não é um tipo de sala válido.',
'error-invalid-settings': 'Configurações inválidas fornecidas',
'error-invalid-subscription': 'Subscrição inválida',
'error-invalid-token': 'Token inválido',
'error-invalid-triggerWords': 'triggerWords inválido',
'error-invalid-urls': 'URLs inválidos',
'error-invalid-user': 'Utilizador inválido',
'error-invalid-username': 'Nome de utilizador inválido',
'error-invalid-webhook-response': 'O URL do webhook respondeu com um estado diferente de 200',
'error-message-deleting-blocked': 'A remoção de mensagens está bloqueada',
'error-message-editing-blocked': 'A edição de mensagens está bloqueada',
'error-message-size-exceeded': 'O tamanho da mensagem excede Message_MaxAllowedSize',
'error-missing-unsubscribe-link': 'Você deve fornecer o link para cancelar a subscrição: [unsubscribe].',
'error-no-tokens-for-this-user': 'Não há tokens para este utilizador',
'error-not-allowed': 'Não permitido',
'error-not-authorized': 'Não autorizado',
'error-push-disabled': 'Push está desactivado',
'error-remove-last-owner': 'Este é o último proprietário. Por favor, defina um novo proprietário antes de remover este.',
'error-role-in-use': 'Não é possível remover função porque está em uso',
'error-role-name-required': 'Nome da função requerido',
'error-the-field-is-required': 'O campo {{field}} é obrigatório.',
'error-too-many-requests': 'Erro, demasiados pedidos. Por favor, diminua a velocidade. Você deve esperar {{seconds}} segundos antes de tentar novamente.',
'error-user-is-not-activated': 'O utilizador não está activado',
'error-user-has-no-roles': 'O utilizador não tem funções',
'error-user-limit-exceeded': 'O número de utilizadores que você está a tentar convidar para #channel_name excede o limite definido pelo administrador',
'error-user-not-in-room': 'O utilizador não está nesta sala',
'error-user-registration-custom-field': 'error-user-registration-custom-field',
'error-user-registration-disabled': 'O registo de utilizadores está desactivado',
'error-user-registration-secret': 'O registo de utilizadores só é permitido por meio de um URL secreto',
'error-you-are-last-owner': 'Você é o último proprietário. Por favor, defina novo proprietário antes de sair da sala.',
Actions: 'Acções',
activity: 'actividade',
Activity: 'Actividade',
Add_Reaction: 'Adicionar Reacção',
Add_Server: 'Adicionar Servidor',
Add_user: 'Adicionar utilizador',
Alert: 'Alerta',
alert: 'alerta',
alerts: 'alertas',
All_users_in_the_channel_can_write_new_messages: 'Todos os utilizadores no canal podem escrever novas mensagens',
All: 'Todos',
Allow_Reactions: 'Permitir Reacções',
Alphabetical: 'Alfabética',
and_more: 'e mais',
and: 'e',
announcement: 'anúncio',
Announcement: 'Anúncio',
ARCHIVE: 'ARQUIVAR',
archive: 'arquivar',
are_typing: 'estão a escrever',
Are_you_sure_question_mark: 'Tem a certeza?',
Are_you_sure_you_want_to_leave_the_room: 'Tem certeza de que quer sair da sala {{room}}?',
Authenticating: 'Autenticando',
Avatar_changed_successfully: 'Avatar alterado com sucesso!',
Avatar_Url: 'URL do Avatar',
Away: 'Ausente',
Block_user: 'Bloquear utilizador',
Broadcast_channel_Description: 'Apenas utilizadores autorizados podem escrever novas mensagens, mas os outros utilizadores poderão responder',
Broadcast_Channel: 'Canal de Transmissão',
Busy: 'Ocupado',
By_proceeding_you_are_agreeing: 'Ao prosseguir você concorda com o(s) nosso(s)',
Cancel_editing: 'Cancelar edição',
Cancel_recording: 'Cancelar gravação',
Cancel: 'Cancelar',
changing_avatar: 'a alterar avatar',
creating_channel: 'a criar canal',
Channel_Name: 'Nome do Canal',
Channels: 'Canais',
Chats: 'Chats',
Close: 'Fechar',
Close_emoji_selector: 'Fechar selector de emoticons',
Choose: 'Escolher',
Choose_from_library: 'Escolher da biblioteca',
Code: 'Código',
Collaborative: 'Colaborativa',
Confirm: 'Confirmar',
Connect: 'Ligar',
Connect_to_a_server: 'Ligue-se a um servidor',
Connected: 'Ligado',
Connecting: 'A ligar...',
Continue_with: 'Continuar com',
Copied_to_clipboard: 'Copiado para a área de transferência!',
Copy: 'Copiar',
Permalink: 'Link permanente',
Create_account: 'Criar uma conta',
Create_Channel: 'Criar Canal',
Created_snippet: 'Criado um extracto',
Create_a_new_workspace: 'Criar um novo espaço de trabalho',
Create: 'Criar',
Delete_Room_Warning: 'Apagar uma sala irá remover todas as mensagens contidas nela. Isto não pode ser desfeito.',
delete: 'apagar',
Delete: 'Apagar',
DELETE: 'APAGAR',
description: 'descrição',
Description: 'Descrição',
Disable_notifications: 'Desactivar notificações',
Direct_Messages: 'Mensagens Directas',
Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_really_want_to_key_this_room_question_mark: 'Você quer mesmo {{key}} esta sala?',
edit: 'editar',
erasing_room: 'apagando sala',
Edit: 'Editar',
Email_or_password_field_is_empty: 'O campo de e-mail ou palavra-passe está vazio',
Email: 'E-mail',
email: 'e-mail',
Enable_notifications: 'Activar notificações',
Everyone_can_access_this_channel: 'Todos podem aceder a este canal',
Error_uploading: 'Erro ao fazer o envio',
Favorites: 'Favoritos',
Files: 'Ficheiros',
File_description: 'Descrição do ficheiro',
File_name: 'Nome do ficheiro',
Finish_recording: 'Terminar a gravação',
For_your_security_you_must_enter_your_current_password_to_continue: 'Para sua segurança, você deve escrever a sua palavra-passe actual para continuar',
Forgot_my_password: 'Esqueci minha palavra-passe',
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver registado, enviaremos instruções sobre como repor a sua palavra-passe. Se você não receber um e-mail em breve, volte e tente novamente.',
Forgot_password: 'Esquecer palavra-passe',
Forgot_Password: 'Esquecer Palavra-passe',
Group_by_favorites: 'Agrupar por favoritos',
Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal',
Has_joined_the_conversation: 'Entrou na conversa',
Has_left_the_channel: 'Saiu do canal',
Invisible: 'Invisível',
Invite: 'Convidar',
is_a_valid_RocketChat_instance: 'é uma instância válida do Rocket.Chat',
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'está a escrever',
Invalid_server_version: 'O servidor ao qual esta tentando ligar-se, utiliza uma versão que não é suporta pela aplicação: {{currentVersion}}.\n\nA versão mínima requerida é {{minVersion}}',
Join_the_community: 'Junte-se à comunidade',
Join: 'Entrar',
Just_invited_people_can_access_this_channel: 'Apenas utilizadores convidados podem aceder a este canal',
Language: 'Idioma',
last_message: 'última mensagem',
Leave_channel: 'Sair do canal',
leaving_room: 'a sair da sala',
leave: 'sair',
Legal: 'Legal',
Livechat: 'Livechat',
Login: 'Entrar',
Login_error: 'As suas credenciais foram rejeitadas! Por favor, tente novamente.',
Login_with: 'Entrar com',
Logout: 'Sair',
members: 'membros',
Members: 'Membros',
Mentioned_Messages: 'Mensagens Mencionadas',
mentioned: 'mencionado',
Mentions: 'Menções',
Message_accessibility: 'Mensagem de {{user}} às {{time}}: {{message}}',
Message_actions: 'Acções de mensagem',
Message_pinned: 'Mensagem afixada',
Message_removed: 'Mensagem removida',
Messages: 'Mensagens',
Microphone_Permission_Message: 'O Rocket Chat necessita de acesso ao seu microfone para que você possa enviar mensagens de áudio.',
Microphone_Permission: 'Permissão de Microfone',
Mute: 'Silenciar',
muted: 'silenciado',
My_servers: 'Meus servidores',
N_people_reacted: '{{n}} pessoas reagiram',
N_users: '{{n}} utilizadores',
name: 'nome',
Name: 'Nome',
New_Message: 'Nova Mensagem',
New_Password: 'Nova Palavra-passe',
New_Server: 'Novo Servidor',
Next: 'Próximo',
No_files: 'Nenhum ficheiro',
No_mentioned_messages: 'Nenhuma mensagem mencionada',
No_pinned_messages: 'Nenhuma mensagem afixada',
No_results_found: 'Nenhum resultado encontrado',
No_starred_messages: 'Nenhuma mensagem marcada com estrela',
No_announcement_provided: 'Nenhum anúncio fornecido.',
No_description_provided: 'Nenhuma descrição fornecida.',
No_topic_provided: 'Nenhum tópico fornecido.',
No_Message: 'Nenhuma mensagem',
No_Reactions: 'Nenhuma reação',
Not_logged: 'Não ligado',
Nothing_to_save: 'Nada para guardar!',
Notify_active_in_this_room: 'Notifica utilizadores activos nesta sala',
Notify_all_in_this_room: 'Notifica todos os utilizadores nesta sala',
Offline: 'Desligado',
Oops: 'Oops!',
Online: 'Ligado',
Only_authorized_users_can_write_new_messages: 'Apenas utilizadores autorizados podem escrever novas mensagens',
Open_emoji_selector: 'Abra o selector de emoticons',
Open_Source_Communication: 'Comunicação Open Source',
Password: 'Palavra-passe',
Permalink_copied_to_clipboard: 'Link permanente copiado para a área de transferência!',
Pin: 'Afixar',
Pinned_Messages: 'Mensagens Afixadas',
pinned: 'afixada',
Pinned: 'Afixada',
Please_enter_your_password: 'Por favor, introduza a sua palavra-passe',
Preferences_saved: 'Preferências guardadas!',
Privacy_Policy: ' Política de Privacidade',
Private_Channel: 'Canal Privado',
Private_Groups: 'Grupos Privados',
Private: 'Privado',
Profile_saved_successfully: 'Perfil actualizado com sucesso!',
Profile: 'Perfil',
Public_Channel: 'Canal Público',
Public: 'Público',
Quote: 'Citar',
Reactions_are_disabled: 'Reacções desactivadas',
Reactions_are_enabled: 'Reacções activadas',
Reactions: 'Reacções',
Read_Only_Channel: 'Canal só de leitura',
Read_Only: 'Só de Leitura',
Register: 'Registar',
Repeat_Password: 'Repita a palavra-passe',
Reply: 'Responder',
Resend: 'Reenviar',
Reset_password: 'Repor palavra-passe',
resetting_password: 'a repor palavra-passe',
RESET: 'REPOR',
Roles: 'Funções',
Room_actions: 'Ações de sala',
Room_changed_announcement: 'Anúncio da sala alterado para: {{announcement}} por {{userBy}}',
Room_changed_description: 'Descrição da sala alterada para: {{description}} por {{userBy}}',
Room_changed_privacy: 'Tipo de sala alterado para: {{type}} por {{userBy}}',
Room_changed_topic: 'Tópico da sala alterado para: {{topic}} por {{userBy}}',
Room_Files: 'Fiheiros da Sala',
Room_Info_Edit: 'Editar Informação da Sala',
Room_Info: 'Informação da Sala',
Room_Members: 'Membros da Sala',
Room_name_changed: 'Nome da sala alterado para: {{name}} por {{userBy}}',
SAVE: 'GUARDAR',
Save_Changes: 'Guardar Alterações',
Save: 'Guardar',
saving_preferences: 'a guardar preferências',
saving_profile: 'a guardar perfil',
saving_settings: 'a guardar configurações',
Search_Messages: 'Pesquisar Mensagens',
Search: 'Pesquisar',
Select_Avatar: 'Seleccionar Avatar',
Select_Users: 'Seleccionar Utilizadores',
Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem',
Sent_an_attachment: 'Enviou um ficheiro',
Server: 'Servidor',
Servers: 'Servidores',
Set_username_subtitle: 'O nome de utilizador é usado para permitir que outros mencionem você em mensagens',
Settings: 'Definições',
Settings_succesfully_changed: 'Definições guardadas com sucesso!',
Share: 'Partilhar',
Sign_in_your_server: 'Entre no seu servidor',
Sign_Up: 'Inscreva-se',
Some_field_is_invalid_or_empty: 'Algum campo é inválido ou está vazio',
Sorting_by: 'Ordenar por {{key}}',
Star_room: 'Marcar como favorito',
Star: 'Dar estrela',
Starred_Messages: 'Mensagens com estrela',
starred: 'deu uma estrela',
Starred: 'Deu uma estrela',
Start_of_conversation: 'Início da conversa',
Submit: 'Enviar',
Take_a_photo: 'Tirar uma foto',
tap_to_change_status: 'toque para alterar o estado',
Tap_to_view_servers_list: 'Toque para ver a lista de servidores',
Terms_of_Service: ' Termos do Serviço ',
The_URL_is_invalid: 'O URL que você inseriu é inválido. Verifique e tente novamente, por favor!',
There_was_an_error_while_action: 'Houve um erro enquanto {{action}}!',
This_room_is_blocked: 'Esta sala está bloqueada',
This_room_is_read_only: 'Esta sala é apenas de leitura',
Timezone: 'Fuso Horário',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'tópico',
Topic: 'Tópico',
Try_again: 'Tente novamente',
Two_Factor_Authentication: 'Autenticação 2FA',
Type_the_channel_name_here: 'Escreva o nome do canal aqui',
unarchive: 'desarquivar',
UNARCHIVE: 'DESARQUIVAR',
Unblock_user: 'Desbloquear utilizador',
Unmute: 'Retirar silêncio',
unmuted: 'silêncio removido',
Unpin: 'Desafixar',
unread_messages: 'não lidas',
Unread: 'Não lidas',
Unread_on_top: 'Não lidas no topo',
Unstar: 'Retirar estrela',
Updating: 'A actualizar...',
Uploading: 'A enviar',
Upload_file_question_mark: 'Enviar ficheiro?',
User_added_by: 'Utilizador {{userAdded}} adicionado por {{userBy}}',
User_has_been_key: 'Utilizador foi {{key}}!',
User_is_no_longer_role_by_: '{{userBy}} removeu o estatuto de {{role}} de {{user}}',
User_muted_by: 'Utilizador {{userMuted}} foi silenciado por {{userBy}}',
User_removed_by: 'Utilizador {{userRemoved}} removido por {{userBy}}',
User_sent_an_attachment: '{{user}} enviou um ficheiro',
User_unmuted_by: '{{userBy}} retirou o silêncio a {{userUnmuted}}',
User_was_set_role_by_: '{{userBy}} deu estatuto de {{role}} a {{user}}',
Username_is_empty: 'O nome de utilizador está vazio',
Username: 'Nome de utilizador',
Username_or_email: 'Nome de utilizador ou e-mail',
Validating: 'A validar',
Video_call: 'Video chamada',
Voice_call: 'Chamada de voz',
Welcome: 'Bem vindo(a)',
Welcome_to_RocketChat: 'Bem vindo(a) ao Rocket.Chat',
Whats_your_2fa: 'Qual é o seu código 2FA?',
Yes_action_it: 'Sim, {{action}}!',
Yesterday: 'Ontem',
You_are_in_preview_mode: 'Você está no modo de pré-visualização',
You_are_offline: 'Você está desligado',
You_can_search_using_RegExp_eg: 'Você pode pesquisar usando RegEx. por exemplo, `/^text$/i`',
You_colon: 'Você: ',
you_were_mentioned: 'você foi mencionado',
you: 'você',
You: 'Você',
You_will_not_be_able_to_recover_this_message: 'Você será incapaz de recuperar esta mensagem!'
};

View File

@ -6,6 +6,7 @@ import { Provider } from 'react-redux';
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
import { Linking } from 'react-native';
import { appInit } from './actions';
import { deepLinkingOpen } from './actions/deepLinking';
import OnboardingView from './views/OnboardingView';
import NewServerView from './views/NewServerView';
@ -28,41 +29,35 @@ import MentionedMessagesView from './views/MentionedMessagesView';
import StarredMessagesView from './views/StarredMessagesView';
import SearchMessagesView from './views/SearchMessagesView';
import PinnedMessagesView from './views/PinnedMessagesView';
import ThreadMessagesView from './views/ThreadMessagesView';
import SelectedUsersView from './views/SelectedUsersView';
import CreateChannelView from './views/CreateChannelView';
import LegalView from './views/LegalView';
import TermsServiceView from './views/TermsServiceView';
import PrivacyPolicyView from './views/PrivacyPolicyView';
import ForgotPasswordView from './views/ForgotPasswordView';
import RegisterView from './views/RegisterView';
import OAuthView from './views/OAuthView';
import SetUsernameView from './views/SetUsernameView';
import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from './constants/colors';
import parseQuery from './lib/methods/helpers/parseQuery';
import { initializePushNotifications } from './push';
import { initializePushNotifications, onNotification } from './push';
import store from './lib/createStore';
useScreens();
initializePushNotifications();
const handleOpenURL = ({ url }) => {
const parseDeepLinking = (url) => {
if (url) {
url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
const regex = /^(room|auth)\?/;
if (url.match(regex)) {
url = url.replace(regex, '');
const params = parseQuery(url);
store.dispatch(deepLinkingOpen(params));
url = url.replace(regex, '').trim();
if (url) {
return parseQuery(url);
}
}
}
return null;
};
Linking
.getInitialURL()
.then(url => handleOpenURL({ url }))
.catch(e => console.warn(e));
Linking.addEventListener('url', handleOpenURL);
const defaultHeader = {
headerStyle: {
backgroundColor: HEADER_BACKGROUND
@ -84,15 +79,8 @@ const OutsideStack = createStackNavigator({
LoginSignupView,
LoginView,
ForgotPasswordView,
RegisterView
}, {
defaultNavigationOptions: defaultHeader
});
const LegalStack = createStackNavigator({
LegalView,
TermsServiceView,
PrivacyPolicyView
RegisterView,
LegalView
}, {
defaultNavigationOptions: defaultHeader
});
@ -105,7 +93,6 @@ const OAuthStack = createStackNavigator({
const OutsideStackModal = createStackNavigator({
OutsideStack,
LegalStack,
OAuthStack
},
{
@ -126,23 +113,54 @@ const ChatsStack = createStackNavigator({
StarredMessagesView,
SearchMessagesView,
PinnedMessagesView,
SelectedUsersView
SelectedUsersView,
ThreadMessagesView
}, {
defaultNavigationOptions: defaultHeader
});
ChatsStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed';
}
return {
drawerLockMode
};
};
const ProfileStack = createStackNavigator({
ProfileView
}, {
defaultNavigationOptions: defaultHeader
});
ProfileStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed';
}
return {
drawerLockMode
};
};
const SettingsStack = createStackNavigator({
SettingsView
}, {
defaultNavigationOptions: defaultHeader
});
SettingsStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed';
}
return {
drawerLockMode
};
};
const ChatsDrawer = createDrawerNavigator({
ChatsStack,
ProfileStack,
@ -184,12 +202,48 @@ const App = createAppContainer(createSwitchNavigator(
}
));
export default () => (
<Provider store={store}>
<App
ref={(navigatorRef) => {
Navigation.setTopLevelNavigator(navigatorRef);
}}
/>
</Provider>
);
export default class Root extends React.Component {
constructor(props) {
super(props);
this.init();
}
componentDidMount() {
this.listenerTimeout = setTimeout(() => {
Linking.addEventListener('url', ({ url }) => {
const parsedDeepLinkingURL = parseDeepLinking(url);
if (parsedDeepLinkingURL) {
store.dispatch(deepLinkingOpen(parsedDeepLinkingURL));
}
});
}, 5000);
}
componentWillUnmount() {
clearTimeout(this.listenerTimeout);
}
init = async() => {
const [notification, deepLinking] = await Promise.all([initializePushNotifications(), Linking.getInitialURL()]);
const parsedDeepLinkingURL = parseDeepLinking(deepLinking);
if (notification) {
onNotification(notification);
} else if (parsedDeepLinkingURL) {
store.dispatch(deepLinkingOpen(parsedDeepLinkingURL));
} else {
store.dispatch(appInit());
}
}
render() {
return (
<Provider store={store}>
<App
ref={(navigatorRef) => {
Navigation.setTopLevelNavigator(navigatorRef);
}}
/>
</Provider>
);
}
}

View File

@ -1,39 +1,83 @@
import { InteractionManager } from 'react-native';
import semver from 'semver';
import reduxStore from '../createStore';
import database from '../realm';
import * as actions from '../../actions';
import log from '../../utils/log';
const getLastMessage = () => {
const setting = database.objects('customEmojis').sorted('_updatedAt', true)[0];
return setting && setting._updatedAt;
const getUpdatedSince = () => {
const emoji = database.objects('customEmojis').sorted('_updatedAt', true)[0];
return emoji && emoji._updatedAt.toISOString();
};
// TODO: fix api (get emojis by date/version....)
const create = (customEmojis) => {
if (customEmojis && customEmojis.length) {
customEmojis.forEach((emoji) => {
try {
database.create('customEmojis', emoji, true);
} catch (e) {
log('getEmojis create', e);
}
});
}
};
export default async function() {
try {
const lastMessage = getLastMessage();
// RC 0.61.0
const result = await this.sdk.get('emoji-custom');
let { emojis } = result;
emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage);
if (emojis.length === 0) {
return;
}
emojis = this._prepareEmojis(emojis);
InteractionManager.runAfterInteractions(() => {
database.write(() => {
emojis.forEach((emoji) => {
try {
database.create('customEmojis', emoji, true);
} catch (e) {
log('create custom emojis', e);
}
const serverVersion = reduxStore.getState().server.version;
const updatedSince = getUpdatedSince();
// if server version is lower than 0.75.0, fetches from old api
if (semver.lt(serverVersion, '0.75.0')) {
// RC 0.61.0
const result = await this.sdk.get('emoji-custom');
InteractionManager.runAfterInteractions(() => {
let { emojis } = result;
emojis = emojis.filter(emoji => !updatedSince || emoji._updatedAt > updatedSince);
database.write(() => {
create(emojis);
});
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(result.emojis)));
});
});
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(emojis)));
} else {
const params = {};
if (updatedSince) {
params.updatedSince = updatedSince;
}
// RC 0.75.0
const result = await this.sdk.get('emoji-custom.list', params);
if (!result.success) {
return;
}
InteractionManager.runAfterInteractions(
() => database.write(() => {
const { emojis } = result;
create(emojis.update);
if (emojis.delete && emojis.delete.length) {
emojis.delete.forEach((emoji) => {
try {
const emojiRecord = database.objectForPrimaryKey('customEmojis', emoji._id);
if (emojiRecord) {
database.delete(emojiRecord);
}
} catch (e) {
log('getEmojis delete', e);
}
});
}
const allEmojis = database.objects('customEmojis');
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(allEmojis)));
})
);
}
} catch (e) {
log('getCustomEmojis', e);
}

View File

@ -1,34 +1,75 @@
import { InteractionManager } from 'react-native';
import semver from 'semver';
import database from '../realm';
import log from '../../utils/log';
import defaultPermissions from '../../constants/permissions';
import reduxStore from '../createStore';
const getUpdatedSince = () => {
const permissions = database.objects('permissions').sorted('_updatedAt', true)[0];
return permissions && permissions._updatedAt.toISOString();
};
const create = (permissions) => {
if (permissions && permissions.length) {
permissions.forEach((permission) => {
try {
database.create('permissions', permission, true);
} catch (e) {
log('getPermissions create', e);
}
});
}
};
export default async function() {
try {
// RC 0.66.0
const result = await this.sdk.get('permissions.list');
const serverVersion = reduxStore.getState().server.version;
if (!result.success) {
return;
}
const permissions = result.permissions.filter(permission => defaultPermissions.includes(permission._id));
permissions
.map((permission) => {
permission._updatedAt = new Date();
permission.roles = permission.roles.map(role => ({ value: role }));
return permission;
// if server version is lower than 0.73.0, fetches from old api
if (semver.lt(serverVersion, '0.73.0')) {
// RC 0.66.0
const result = await this.sdk.get('permissions.list');
if (!result.success) {
return;
}
InteractionManager.runAfterInteractions(() => {
database.write(() => {
create(result.permissions);
});
});
} else {
const params = {};
const updatedSince = getUpdatedSince();
if (updatedSince) {
params.updatedSince = updatedSince;
}
// RC 0.73.0
const result = await this.sdk.get('permissions.listAll', params);
InteractionManager.runAfterInteractions(
() => database.write(() => permissions.forEach((permission) => {
try {
database.create('permissions', permission, true);
} catch (e) {
log('getPermissions create', e);
}
}))
);
if (!result.success) {
return;
}
InteractionManager.runAfterInteractions(
() => database.write(() => {
create(result.update);
if (result.delete && result.delete.length) {
result.delete.forEach((p) => {
try {
const permission = database.objectForPrimaryKey('permissions', p._id);
if (permission) {
database.delete(permission);
}
} catch (e) {
log('getPermissions delete', e);
}
});
}
})
);
}
} catch (e) {
log('getPermissions', e);
}

View File

@ -0,0 +1,31 @@
import { InteractionManager } from 'react-native';
import database from '../realm';
import log from '../../utils/log';
export default async function() {
try {
// RC 0.70.0
const result = await this.sdk.get('roles.list');
if (!result.success) {
return;
}
const { roles } = result;
if (roles && roles.length) {
InteractionManager.runAfterInteractions(() => {
database.write(() => roles.forEach((role) => {
try {
database.create('roles', role, true);
} catch (e) {
log('getRoles create', e);
}
}));
});
}
} catch (e) {
log('getRoles', e);
}
}

View File

@ -24,6 +24,9 @@ export const merge = (subscription, room) => {
subscription.archived = room.archived;
subscription.joinCodeRequired = room.joinCodeRequired;
subscription.broadcast = room.broadcast;
if (!subscription.roles || !subscription.roles.length) {
subscription.roles = [];
}
if (room.muted && room.muted.length) {
subscription.muted = room.muted.filter(user => user).map(user => ({ value: user }));
@ -31,11 +34,6 @@ export const merge = (subscription, room) => {
subscription.muted = [];
}
}
if (subscription.roles && subscription.roles.length) {
subscription.roles = subscription.roles.map(role => (role.value ? role : { value: role }));
} else {
subscription.roles = [];
}
if (subscription.mobilePushNotifications === 'nothing') {
subscription.notifications = true;

View File

@ -18,7 +18,12 @@ function normalizeAttachments(msg) {
}
export default (msg) => {
if (!msg) { return; }
/**
* 2019-03-29: Realm object properties are *always* optional, but `u.username` is required
* https://realm.io/docs/javascript/latest/#to-one-relationships
*/
if (!msg || !msg.u || !msg.u.username) { return; }
msg = normalizeAttachments(msg);
msg.reactions = msg.reactions || [];
// TODO: api problems

View File

@ -39,8 +39,18 @@ export default function loadMessagesForRoom(...args) {
if (data && data.length) {
InteractionManager.runAfterInteractions(() => {
database.write(() => data.forEach((message) => {
message = buildMessage(message);
try {
database.create('messages', buildMessage(message), true);
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
}
// if it belongs to a thread
if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
} catch (e) {
log('loadMessagesForRoom -> create messages', e);
}

View File

@ -19,7 +19,7 @@ async function load({ rid: roomId, lastOpen }) {
lastUpdate = getLastUpdate(roomId);
}
// RC 0.60.0
const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate, count: 50 });
const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate });
return result;
}
@ -31,16 +31,23 @@ export default function loadMissedMessages(...args) {
if (data) {
if (data.updated && data.updated.length) {
const { updated } = data;
updated.forEach(buildMessage);
InteractionManager.runAfterInteractions(() => {
database.write(() => updated.forEach((message) => {
try {
message = buildMessage(message);
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
}
if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
} catch (e) {
log('loadMissedMessages -> create messages', e);
}
}));
resolve(updated);
});
}
if (data.deleted && data.deleted.length) {
@ -51,6 +58,10 @@ export default function loadMissedMessages(...args) {
deleted.forEach((m) => {
const message = database.objects('messages').filtered('_id = $0', m._id);
database.delete(message);
const thread = database.objects('threads').filtered('_id = $0', m._id);
database.delete(thread);
const threadMessage = database.objects('threadMessages').filtered('_id = $0', m._id);
database.delete(threadMessage);
});
});
} catch (e) {
@ -59,7 +70,7 @@ export default function loadMissedMessages(...args) {
});
}
}
resolve([]);
resolve();
} catch (e) {
log('loadMissedMessages', e);
reject(e);

View File

@ -0,0 +1,50 @@
import { InteractionManager } from 'react-native';
import EJSON from 'ejson';
import buildMessage from './helpers/buildMessage';
import database from '../realm';
import log from '../../utils/log';
async function load({ tmid, offset }) {
try {
// RC 1.0
const result = await this.sdk.get('chat.getThreadMessages', {
tmid, count: 50, offset, sort: { ts: -1 }, query: { _hidden: { $ne: true } }
});
if (!result || !result.success) {
return [];
}
return result.messages;
} catch (error) {
console.log(error);
return [];
}
}
export default function loadThreadMessages({ tmid, offset = 0 }) {
return new Promise(async(resolve, reject) => {
try {
const data = await load.call(this, { tmid, offset });
if (data && data.length) {
InteractionManager.runAfterInteractions(() => {
database.write(() => data.forEach((m) => {
try {
const message = buildMessage(EJSON.fromJSONValue(m));
message.rid = tmid;
database.create('threadMessages', message, true);
} catch (e) {
log('loadThreadMessages -> create messages', e);
}
}));
return resolve(data);
});
} else {
return resolve([]);
}
} catch (e) {
log('loadThreadMessages', e);
reject(e);
}
});
}

View File

@ -29,7 +29,7 @@ export async function cancelUpload(path) {
}
}
export async function sendFileMessage(rid, fileInfo) {
export async function sendFileMessage(rid, fileInfo, tmid) {
try {
const data = await RNFetchBlob.wrap(fileInfo.path);
if (!fileInfo.size) {
@ -86,6 +86,8 @@ export async function sendFileMessage(rid, fileInfo) {
name: completeResult.name,
description: completeResult.description,
url: completeResult.path
}, {
tmid
});
database.write(() => {

View File

@ -5,12 +5,13 @@ import reduxStore from '../createStore';
import log from '../../utils/log';
import random from '../../utils/random';
export const getMessage = (rid, msg = {}) => {
export const getMessage = (rid, msg = '', tmid) => {
const _id = random(17);
const message = {
_id,
rid,
msg,
tmid,
ts: new Date(),
_updatedAt: new Date(),
status: messagesStatus.TEMP,
@ -30,21 +31,28 @@ export const getMessage = (rid, msg = {}) => {
};
export async function sendMessageCall(message) {
const { _id, rid, msg } = message;
const {
_id, rid, msg, tmid
} = message;
// RC 0.60.0
const data = await this.sdk.post('chat.sendMessage', { message: { _id, rid, msg } });
const data = await this.sdk.post('chat.sendMessage', {
message: {
_id, rid, msg, tmid
}
});
return data;
}
export default async function(rid, msg) {
export default async function(rid, msg, tmid) {
try {
const message = getMessage(rid, msg);
const room = database.objects('subscriptions').filtered('rid == $0', rid);
const message = getMessage(rid, msg, tmid);
const [room] = database.objects('subscriptions').filtered('rid == $0', rid);
// TODO: do we need this?
database.write(() => {
room.lastMessage = message;
});
if (room) {
database.write(() => {
room.draftMessage = null;
});
}
try {
const ret = await sendMessageCall.call(this, message);

View File

@ -1,18 +1,22 @@
import EJSON from 'ejson';
import log from '../../../utils/log';
import protectedFunction from '../helpers/protectedFunction';
import buildMessage from '../helpers/buildMessage';
import database from '../../realm';
import debounce from '../../../utils/debounce';
const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
const removeListener = listener => listener.stop();
let promises;
let timer = null;
let connectedListener;
let disconnectedListener;
export default function subscribeRoom({ rid }) {
if (promises) {
promises.then(unsubscribe);
promises = false;
}
let promises;
let timer = null;
let connectedListener;
let disconnectedListener;
let notifyRoomListener;
let messageReceivedListener;
const typingTimeouts = {};
const loop = () => {
if (timer) {
return;
@ -41,6 +45,116 @@ export default function subscribeRoom({ rid }) {
}
};
const getUserTyping = username => (
database
.memoryDatabase.objects('usersTyping')
.filtered('rid = $0 AND username = $1', rid, username)
);
const removeUserTyping = (username) => {
const userTyping = getUserTyping(username);
try {
database.memoryDatabase.write(() => {
database.memoryDatabase.delete(userTyping);
});
if (typingTimeouts[username]) {
clearTimeout(typingTimeouts[username]);
typingTimeouts[username] = null;
}
} catch (error) {
console.log('TCL: removeUserTyping -> error', error);
}
};
const addUserTyping = (username) => {
const userTyping = getUserTyping(username);
// prevent duplicated
if (userTyping.length === 0) {
try {
database.memoryDatabase.write(() => {
database.memoryDatabase.create('usersTyping', { rid, username });
});
if (typingTimeouts[username]) {
clearTimeout(typingTimeouts[username]);
typingTimeouts[username] = null;
}
typingTimeouts[username] = setTimeout(() => {
removeUserTyping(username);
}, 10000);
} catch (error) {
console.log('TCL: addUserTyping -> error', error);
}
}
};
const handleNotifyRoomReceived = protectedFunction((ddpMessage) => {
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
if (rid !== _rid) {
return;
}
if (ev === 'typing') {
const [username, typing] = ddpMessage.fields.args;
if (typing) {
addUserTyping(username);
} else {
removeUserTyping(username);
}
} else if (ev === 'deleteMessage') {
database.write(() => {
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {
const { _id } = ddpMessage.fields.args[0];
const message = database.objects('messages').filtered('_id = $0', _id);
database.delete(message);
const thread = database.objects('threads').filtered('_id = $0', _id);
database.delete(thread);
const threadMessage = database.objects('threadMessages').filtered('_id = $0', _id);
database.delete(threadMessage);
const cleanTmids = database.objects('messages').filtered('tmid = $0', _id).snapshot();
if (cleanTmids && cleanTmids.length) {
cleanTmids.forEach((m) => {
m.tmid = null;
});
}
}
});
}
});
const read = debounce(() => {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room._id) {
this.readMessages(rid);
}
}, 300);
const handleMessageReceived = protectedFunction((ddpMessage) => {
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
if (rid !== message.rid) {
return;
}
requestAnimationFrame(() => {
try {
database.write(() => {
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
} else if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
});
read();
} catch (e) {
console.warn('handleMessageReceived', e);
}
});
});
const stop = () => {
if (promises) {
promises.then(unsubscribe);
@ -54,12 +168,32 @@ export default function subscribeRoom({ rid }) {
disconnectedListener.then(removeListener);
disconnectedListener = false;
}
if (notifyRoomListener) {
notifyRoomListener.then(removeListener);
notifyRoomListener = false;
}
if (messageReceivedListener) {
messageReceivedListener.then(removeListener);
messageReceivedListener = false;
}
clearTimeout(timer);
timer = false;
Object.keys(typingTimeouts).forEach((key) => {
if (typingTimeouts[key]) {
clearTimeout(typingTimeouts[key]);
typingTimeouts[key] = null;
}
});
database.memoryDatabase.write(() => {
const usersTyping = database.memoryDatabase.objects('usersTyping').filtered('rid == $0', rid);
database.memoryDatabase.delete(usersTyping);
});
};
connectedListener = this.sdk.onStreamData('connected', handleConnected);
disconnectedListener = this.sdk.onStreamData('close', handleDisconnected);
notifyRoomListener = this.sdk.onStreamData('stream-notify-room', handleNotifyRoomReceived);
messageReceivedListener = this.sdk.onStreamData('stream-room-messages', handleMessageReceived);
try {
promises = this.sdk.subscribeRoom(rid);

View File

@ -11,7 +11,8 @@ const serversSchema = {
id: 'string',
name: { type: 'string', optional: true },
iconURL: { type: 'string', optional: true },
roomsUpdatedAt: { type: 'date', optional: true }
roomsUpdatedAt: { type: 'date', optional: true },
version: 'string?'
}
};
@ -27,20 +28,12 @@ const settingsSchema = {
}
};
const permissionsRolesSchema = {
name: 'permissionsRoles',
primaryKey: 'value',
properties: {
value: 'string'
}
};
const permissionsSchema = {
name: 'permissions',
primaryKey: '_id',
properties: {
_id: 'string',
roles: { type: 'list', objectType: 'permissionsRoles' },
roles: 'string[]',
_updatedAt: { type: 'date', optional: true }
}
};
@ -54,14 +47,6 @@ const roomsSchema = {
}
};
const subscriptionRolesSchema = {
name: 'subscriptionRolesSchema',
primaryKey: 'value',
properties: {
value: 'string'
}
};
const userMutedInRoomSchema = {
name: 'usersMuted',
primaryKey: 'value',
@ -84,7 +69,7 @@ const subscriptionSchema = {
rid: { type: 'string', indexed: true },
open: { type: 'bool', optional: true },
alert: { type: 'bool', optional: true },
roles: { type: 'list', objectType: 'subscriptionRolesSchema' },
roles: 'string[]',
unread: { type: 'int', optional: true },
userMentions: { type: 'int', optional: true },
roomUpdatedAt: { type: 'date', optional: true },
@ -101,7 +86,10 @@ const subscriptionSchema = {
joinCodeRequired: { type: 'bool', optional: true },
notifications: { type: 'bool', optional: true },
muted: { type: 'list', objectType: 'usersMuted' },
broadcast: { type: 'bool', optional: true }
broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true },
lastThreadSync: 'date?'
}
};
@ -204,8 +192,73 @@ const messagesSchema = {
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
// mentions: [],
// channels: [],
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
urls: { type: 'list', objectType: 'url', default: [] },
_updatedAt: { type: 'date', optional: true },
status: { type: 'int', optional: true },
pinned: { type: 'bool', optional: true },
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true },
drid: { type: 'string', optional: true },
dcount: { type: 'int', optional: true },
dlm: { type: 'date', optional: true },
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]'
}
};
const threadsSchema = {
name: 'threads',
primaryKey: '_id',
properties: {
_id: 'string',
msg: { type: 'string', optional: true },
t: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
urls: { type: 'list', objectType: 'url', default: [] },
_updatedAt: { type: 'date', optional: true },
status: { type: 'int', optional: true },
pinned: { type: 'bool', optional: true },
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true },
drid: { type: 'string', optional: true },
dcount: { type: 'int', optional: true },
dlm: { type: 'date', optional: true },
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]',
draftMessage: 'string?'
}
};
const threadMessagesSchema = {
name: 'threadMessages',
primaryKey: '_id',
properties: {
_id: 'string',
msg: { type: 'string', optional: true },
t: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
@ -233,21 +286,13 @@ const frequentlyUsedEmojiSchema = {
}
};
const customEmojiAliasesSchema = {
name: 'customEmojiAliases',
primaryKey: 'value',
properties: {
value: 'string'
}
};
const customEmojisSchema = {
name: 'customEmojis',
primaryKey: '_id',
properties: {
_id: 'string',
name: 'string',
aliases: { type: 'list', objectType: 'customEmojiAliases' },
aliases: 'string[]',
extension: 'string',
_updatedAt: { type: 'date', optional: true }
}
@ -278,21 +323,40 @@ const uploadsSchema = {
}
};
const usersTypingSchema = {
name: 'usersTyping',
properties: {
rid: { type: 'string', indexed: true },
username: { type: 'string', optional: true }
}
};
const activeUsersSchema = {
name: 'activeUsers',
primaryKey: 'id',
properties: {
id: 'string',
name: 'string?',
username: 'string?',
status: 'string?',
utcOffset: 'double?'
}
};
const schema = [
settingsSchema,
subscriptionSchema,
subscriptionRolesSchema,
messagesSchema,
threadsSchema,
threadMessagesSchema,
usersSchema,
roomsSchema,
attachment,
attachmentFields,
messagesEditedBySchema,
permissionsSchema,
permissionsRolesSchema,
url,
frequentlyUsedEmojiSchema,
customEmojiAliasesSchema,
customEmojisSchema,
messagesReactionsSchema,
messagesReactionsUsernamesSchema,
@ -301,6 +365,8 @@ const schema = [
uploadsSchema
];
const inMemorySchema = [usersTypingSchema, activeUsersSchema];
class DB {
databases = {
serversDB: new Realm({
@ -308,7 +374,23 @@ class DB {
schema: [
serversSchema
],
schemaVersion: 1
schemaVersion: 6,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
for (let i = 0; i < newServers.length; i++) {
newServers[i].roomsUpdatedAt = null;
}
}
}
}),
inMemoryDB: new Realm({
path: 'memory.realm',
schema: inMemorySchema,
schemaVersion: 2,
inMemory: true
})
}
@ -332,17 +414,68 @@ class DB {
return this.database.objects(...args);
}
objectForPrimaryKey(...args) {
return this.database.objectForPrimaryKey(...args);
}
get database() {
return this.databases.activeDB;
}
get memoryDatabase() {
return this.databases.inMemoryDB;
}
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '');
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,
schema,
schemaVersion: 1
schemaVersion: 9,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
const newSubs = newRealm.objects('subscriptions');
// eslint-disable-next-line no-plusplus
for (let i = 0; i < newSubs.length; i++) {
newSubs[i].lastOpen = null;
newSubs[i].ls = null;
}
const newMessages = newRealm.objects('messages');
newRealm.delete(newMessages);
const newThreads = newRealm.objects('threads');
newRealm.delete(newThreads);
const newThreadMessages = newRealm.objects('threadMessages');
newRealm.delete(newThreadMessages);
}
if (newRealm.schemaVersion === 9) {
const newSubs = newRealm.objects('subscriptions');
newRealm.delete(newSubs);
const newEmojis = newRealm.objects('customEmojis');
newRealm.delete(newEmojis);
const newSettings = newRealm.objects('settings');
newRealm.delete(newSettings);
}
}
});
}
}
export default new DB();
const db = new DB();
export default db;
// Realm workaround for "Cannot create asynchronous query while in a write transaction"
// inpired from https://github.com/realm/realm-js/issues/1188#issuecomment-359223918
export function safeAddListener(results, callback, database = db) {
if (!results || !results.addListener) {
console.log('⚠️ safeAddListener called for non addListener-compliant object');
return;
}
if (database.isInTransaction) {
setTimeout(() => {
safeAddListener(results, callback);
}, 50);
} else {
results.addListener(callback);
}
}

View File

@ -1,22 +1,19 @@
import { AsyncStorage } from 'react-native';
import foreach from 'lodash/forEach';
import { AsyncStorage, InteractionManager } from 'react-native';
import semver from 'semver';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import reduxStore from './createStore';
import defaultSettings from '../constants/settings';
import messagesStatus from '../constants/messagesStatus';
import database from './realm';
import database, { safeAddListener } from './realm';
import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo';
import EventEmitter from '../utils/events';
import {
setUser, setLoginServices, loginRequest, loginFailure, logout
} from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
import { setActiveUser } from '../actions/activeUsers';
import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setRoles } from '../actions/roles';
import subscribeRooms from './methods/subscriptions/rooms';
import subscribeRoom from './methods/subscriptions/room';
@ -28,11 +25,12 @@ import getSettings from './methods/getSettings';
import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions';
import getCustomEmoji from './methods/getCustomEmojis';
import getRoles from './methods/getRoles';
import canOpenRoom from './methods/canOpenRoom';
import _buildMessage from './methods/helpers/buildMessage';
import loadMessagesForRoom from './methods/loadMessagesForRoom';
import loadMissedMessages from './methods/loadMissedMessages';
import loadThreadMessages from './methods/loadThreadMessages';
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
@ -43,7 +41,7 @@ import { roomsRequest } from '../actions/rooms';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
const returnAnArray = obj => obj || [];
const MIN_ROCKETCHAT_VERSION = '0.66.0';
const MIN_ROCKETCHAT_VERSION = '0.70.0';
const RocketChat = {
TOKEN_KEY,
@ -65,7 +63,7 @@ const RocketChat = {
if (data.length) {
return resolve(data[0]);
}
data.addListener(() => {
safeAddListener(data, () => {
if (!data.length) { return; }
data.removeAllListeners();
resolve(data[0]);
@ -80,26 +78,24 @@ const RocketChat = {
console.warn(`AsyncStorage error: ${ error.message }`);
}
},
async testServer(server) {
async getServerInfo(server) {
try {
const result = await fetch(`${ server }/api/v1/info`).then(response => response.json());
if (result.success && result.info) {
if (semver.lt(result.info.version, MIN_ROCKETCHAT_VERSION)) {
const result = await fetch(`${ server }/api/info`).then(response => response.json());
if (result.success) {
if (semver.lt(result.version, MIN_ROCKETCHAT_VERSION)) {
return {
success: false,
message: 'Invalid_server_version',
messageOptions: {
currentVersion: result.info.version,
currentVersion: result.version,
minVersion: MIN_ROCKETCHAT_VERSION
}
};
}
return {
success: true
};
return result;
}
} catch (e) {
log('testServer', e);
log('getServerInfo', e);
}
return {
success: false,
@ -114,25 +110,52 @@ const RocketChat = {
reduxStore.dispatch(setUser(ddpMessage.fields));
}
if (this._setUserTimer) {
clearTimeout(this._setUserTimer);
this._setUserTimer = null;
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser({ status: 'offline' }));
}
this._setUserTimer = setTimeout(() => {
reduxStore.dispatch(setActiveUser(this.activeUsers));
this._setUserTimer = null;
return this.activeUsers = {};
}, 2000);
if (!this._setUserTimer) {
this._setUserTimer = setTimeout(() => {
const batchUsers = this.activeUsers;
InteractionManager.runAfterInteractions(() => {
database.memoryDatabase.write(() => {
Object.keys(batchUsers).forEach((key) => {
if (batchUsers[key] && batchUsers[key].id) {
try {
const data = batchUsers[key];
if (data.removed) {
const userRecord = database.memoryDatabase.objectForPrimaryKey('activeUsers', data.id);
if (userRecord) {
userRecord.status = 'offline';
}
} else {
database.memoryDatabase.create('activeUsers', data, true);
}
} catch (error) {
console.log(error);
}
}
});
});
});
this._setUserTimer = null;
return this.activeUsers = {};
}, 10000);
}
const activeUser = reduxStore.getState().activeUsers[ddpMessage.id];
if (!ddpMessage.fields) {
this.activeUsers[ddpMessage.id] = {};
this.activeUsers[ddpMessage.id] = {
id: ddpMessage.id,
removed: true
};
} else {
this.activeUsers[ddpMessage.id] = { ...this.activeUsers[ddpMessage.id], ...activeUser, ...ddpMessage.fields };
this.activeUsers[ddpMessage.id] = {
id: ddpMessage.id, ...this.activeUsers[ddpMessage.id], ...ddpMessage.fields
};
}
},
async loginSuccess({ user }) {
EventEmitter.emit('connected');
reduxStore.dispatch(setUser(user));
reduxStore.dispatch(roomsRequest());
@ -141,11 +164,18 @@ const RocketChat = {
}
this.roomsSub = await this.subscribeRooms();
this.sdk.subscribe('activeUsers');
this.sdk.subscribe('roles');
this.getPermissions();
this.getCustomEmoji();
this.getRoles();
this.registerPushToken().catch(e => console.log(e));
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
this.activeUsersSubTimeout = setTimeout(() => {
this.sdk.subscribe('activeUsers');
}, 5000);
},
connect({ server, user }) {
database.setActiveDB(server);
@ -190,53 +220,6 @@ const RocketChat = {
});
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
this.sdk.onStreamData('stream-room-messages', (ddpMessage) => {
// TODO: debounce
const message = _buildMessage(ddpMessage.fields.args[0]);
requestAnimationFrame(() => reduxStore.dispatch(roomMessageReceived(message)));
});
this.sdk.onStreamData('stream-notify-room', protectedFunction((ddpMessage) => {
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
if (ev === 'typing') {
reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
} else if (ev === 'deleteMessage') {
database.write(() => {
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {
const { _id } = ddpMessage.fields.args[0];
const message = database.objects('messages').filtered('_id = $0', _id);
database.delete(message);
}
});
}
}));
this.sdk.onStreamData('rocketchat_roles', protectedFunction((ddpMessage) => {
this.roles = this.roles || {};
if (this.roleTimer) {
clearTimeout(this.roleTimer);
this.roleTimer = null;
}
this.roleTimer = setTimeout(() => {
reduxStore.dispatch(setRoles(this.roles));
database.write(() => {
foreach(this.roles, (description, _id) => {
try {
database.create('roles', { _id, description }, true);
} catch (e) {
log('create roles', e);
}
});
});
this.roleTimer = null;
return this.roles = {};
}, 1000);
this.roles[ddpMessage.id] = (ddpMessage.fields && ddpMessage.fields.description) || undefined;
}));
},
register(credentials) {
@ -309,7 +292,8 @@ const RocketChat = {
language: result.me.language,
status: result.me.status,
customFields: result.me.customFields,
emails: result.me.emails
emails: result.me.emails,
roles: result.me.roles
};
return user;
} catch (e) {
@ -326,6 +310,11 @@ const RocketChat = {
this.roomsSub.stop();
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
try {
await this.removePushToken();
} catch (error) {
@ -377,6 +366,7 @@ const RocketChat = {
},
loadMissedMessages,
loadMessagesForRoom,
loadThreadMessages,
getMessage,
sendMessage,
getRooms,
@ -421,8 +411,6 @@ const RocketChat = {
data = data.filtered('t != $0', 'd');
}
data = data.slice(0, 7);
const array = Array.from(data);
data = JSON.parse(JSON.stringify(array));
const usernames = data.map(sub => sub.name);
try {
@ -473,6 +461,7 @@ const RocketChat = {
getSettings,
getPermissions,
getCustomEmoji,
getRoles,
parseSettings: settings => settings.reduce((ret, item) => {
ret[item._id] = item[defaultSettings[item._id].type];
return ret;
@ -551,6 +540,9 @@ const RocketChat = {
unsubscribe(subscription) {
return this.sdk.unsubscribe(subscription);
},
onStreamData(...args) {
return this.sdk.onStreamData(...args);
},
emitTyping(room, t = true) {
const { login } = reduxStore.getState();
return this.sdk.methodCall('stream-notify-room', `${ room }/typing`, login.user.username, t);
@ -572,9 +564,9 @@ const RocketChat = {
// RC 0.64.0
return this.sdk.post('rooms.favorite', { roomId, favorite });
},
getRoomMembers(rid, allUsers) {
getRoomMembers(rid, allUsers, skip = 0, limit = 10) {
// RC 0.42.0
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers);
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
},
getUserRoles() {
// RC 0.27.0
@ -584,16 +576,19 @@ const RocketChat = {
// RC 0.65.0
return this.sdk.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId });
},
async getRoomMember(rid, currentUserId) {
try {
if (rid === `${ currentUserId }${ currentUserId }`) {
return Promise.resolve(currentUserId);
}
const membersResult = await RocketChat.getRoomMembers(rid, true);
return Promise.resolve(membersResult.records.find(m => m._id !== currentUserId));
} catch (error) {
return Promise.reject(error);
getChannelInfo(roomId) {
// RC 0.48.0
return this.sdk.get('channels.info', { roomId });
},
getUserInfo(userId) {
// RC 0.48.0
return this.sdk.get('users.info', { userId });
},
getRoomMemberId(rid, currentUserId) {
if (rid === `${ currentUserId }${ currentUserId }`) {
return currentUserId;
}
return rid.replace(currentUserId, '').trim();
},
toggleBlockUser(rid, blocked, block) {
if (block) {
@ -649,21 +644,29 @@ const RocketChat = {
// RC 0.51.0
return this.sdk.methodCall('addUsersToRoom', { rid, users });
},
getSingleMessage(msgId) {
// RC 0.57.0
return this.sdk.methodCall('getSingleMessage', msgId);
},
hasPermission(permissions, rid) {
let roles = [];
let roomRoles = [];
try {
// get the room from realm
const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (!room) {
return permissions.reduce((result, permission) => {
result[permission] = false;
return result;
}, {});
}
// get room roles
roles = room.roles; // eslint-disable-line prefer-destructuring
roomRoles = room.roles;
} catch (error) {
console.log('hasPermission -> error', error);
}
// get permissions from realm
const permissionsFiltered = database.objects('permissions')
.filter(permission => permissions.includes(permission._id));
// transform room roles to array
const roomRoles = Array.from(Object.keys(roles), i => roles[i].value);
// get user roles on the server from redux
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || [];
// merge both roles
@ -675,7 +678,7 @@ const RocketChat = {
result[permission] = false;
const permissionFound = permissionsFiltered.find(p => p._id === permission);
if (permissionFound) {
result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r.value));
result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r));
}
return result;
}, {});
@ -762,6 +765,25 @@ const RocketChat = {
roomId,
searchText
});
},
toggleFollowMessage(mid, follow) {
// RC 1.0
if (follow) {
return this.sdk.methodCall('followMessage', { mid });
}
return this.sdk.methodCall('unfollowMessage', { mid });
},
getThreadsList({ rid, count, offset }) {
// RC 1.0
return this.sdk.get('chat.getThreadsList', {
rid, count, offset, sort: { ts: -1 }
});
},
getSyncThreadsList({ rid, updatedSince }) {
// RC 1.0
return this.sdk.get('chat.syncThreadsList', {
rid, updatedSince
});
}
};

File diff suppressed because one or more lines are too long

View File

@ -1,273 +0,0 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import { emojify } from 'react-emojione';
import { RectButton } from 'react-native-gesture-handler';
import Avatar from '../containers/Avatar';
import Status from '../containers/Status';
import RoomTypeIcon from '../containers/RoomTypeIcon';
import I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 15
},
centerContainer: {
flex: 1,
height: '100%'
},
title: {
flex: 1,
fontSize: 18,
color: '#0C0D0F',
fontWeight: '400',
marginRight: 5,
paddingTop: 0,
paddingBottom: 0
},
alert: {
fontWeight: '600'
},
row: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start'
},
titleContainer: {
width: '100%',
marginTop: isIOS ? 5 : 2,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
date: {
fontSize: 14,
color: '#9EA2A8',
fontWeight: 'normal',
paddingTop: 0,
paddingBottom: 0
},
updateAlert: {
color: '#1D74F5',
fontWeight: '700'
},
unreadNumberContainer: {
minWidth: 23,
padding: 3,
borderRadius: 4,
backgroundColor: '#1D74F5',
alignItems: 'center',
justifyContent: 'center'
},
unreadNumberText: {
color: '#fff',
overflow: 'hidden',
fontSize: 14,
fontWeight: '500',
letterSpacing: 0.56
},
status: {
marginRight: 7,
marginTop: 3
},
markdownText: {
flex: 1,
color: '#9EA2A8',
fontSize: 15,
fontWeight: 'normal'
},
markdownTextAlert: {
color: '#0C0D0F'
},
avatar: {
marginRight: 10
}
});
const renderNumber = (unread, userMentions) => {
if (!unread || unread <= 0) {
return;
}
if (unread >= 1000) {
unread = '999+';
}
if (userMentions > 0) {
unread = `@ ${ unread }`;
}
return (
<View style={styles.unreadNumberContainer}>
<Text style={styles.unreadNumberText}>{ unread }</Text>
</View>
);
};
const attrs = ['name', 'unread', 'userMentions', 'StoreLastMessage', 'alert', 'type'];
@connect(state => ({
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
},
StoreLastMessage: state.settings.Store_Last_Message,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class RoomItem extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
StoreLastMessage: PropTypes.bool,
_updatedAt: PropTypes.string,
lastMessage: PropTypes.object,
favorite: PropTypes.bool,
alert: PropTypes.bool,
unread: PropTypes.number,
userMentions: PropTypes.number,
id: PropTypes.string,
onPress: PropTypes.func,
user: PropTypes.shape({
id: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string
}),
avatarSize: PropTypes.number,
testID: PropTypes.string,
height: PropTypes.number
}
static defaultProps = {
avatarSize: 48
}
shouldComponentUpdate(nextProps) {
const { lastMessage, _updatedAt } = this.props;
const oldlastMessage = lastMessage;
const newLastmessage = nextProps.lastMessage;
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
return true;
}
if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) {
return true;
}
// eslint-disable-next-line react/destructuring-assignment
return attrs.some(key => nextProps[key] !== this.props[key]);
}
get avatar() {
const {
type, name, avatarSize, baseUrl, user
} = this.props;
return <Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={styles.avatar} user={user} />;
}
get lastMessage() {
const {
lastMessage, type, StoreLastMessage, user
} = this.props;
if (!StoreLastMessage) {
return '';
}
if (!lastMessage) {
return I18n.t('No_Message');
}
let prefix = '';
const me = lastMessage.u.username === user.username;
if (!lastMessage.msg && Object.keys(lastMessage.attachments).length > 0) {
if (me) {
return I18n.t('User_sent_an_attachment', { user: I18n.t('You') });
} else {
return I18n.t('User_sent_an_attachment', { user: lastMessage.u.username });
}
}
if (me) {
prefix = I18n.t('You_colon');
} else if (type !== 'd') {
prefix = `${ lastMessage.u.username }: `;
}
let msg = `${ prefix }${ lastMessage.msg.replace(/[\n\t\r]/igm, '') }`;
msg = emojify(msg, { output: 'unicode' });
return msg;
}
get type() {
const { type, id } = this.props;
if (type === 'd') {
return <Status style={styles.status} size={10} id={id} />;
}
return <RoomTypeIcon type={type} />;
}
formatDate = date => moment(date).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
})
render() {
const {
favorite, unread, userMentions, name, _updatedAt, alert, testID, height, onPress
} = this.props;
const date = this.formatDate(_updatedAt);
let accessibilityLabel = name;
if (unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
} else if (unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
}
if (userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
}
if (date) {
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
}
return (
<RectButton
onPress={onPress}
activeOpacity={0.8}
underlayColor='#e1e5e8'
testID={testID}
>
<View
style={[styles.container, favorite && styles.favorite, height && { height }]}
accessibilityLabel={accessibilityLabel}
>
{this.avatar}
<View style={styles.centerContainer}>
<View style={styles.titleContainer}>
{this.type}
<Text style={[styles.title, alert && styles.alert]} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
</View>
<View style={styles.row}>
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
{this.lastMessage}
</Text>
{renderNumber(unread, userMentions)}
</View>
</View>
</View>
</RectButton>
);
}
}

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Text } from 'react-native';
import { emojify } from 'react-emojione';
import PropTypes from 'prop-types';
import _ from 'lodash';
import I18n from '../../i18n';
import styles from './styles';
const formatMsg = ({
lastMessage, type, showLastMessage, username
}) => {
if (!showLastMessage) {
return '';
}
if (!lastMessage) {
return I18n.t('No_Message');
}
let prefix = '';
const isLastMessageSentByMe = lastMessage.u.username === username;
if (!lastMessage.msg && Object.keys(lastMessage.attachments).length) {
const user = isLastMessageSentByMe ? I18n.t('You') : lastMessage.u.username;
return I18n.t('User_sent_an_attachment', { user });
}
if (isLastMessageSentByMe) {
prefix = I18n.t('You_colon');
} else if (type !== 'd') {
prefix = `${ lastMessage.u.username }: `;
}
let msg = `${ prefix }${ lastMessage.msg.replace(/[\n\t\r]/igm, '') }`;
if (msg) {
msg = emojify(msg, { output: 'unicode' });
}
return msg;
};
const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps);
const LastMessage = React.memo(({
lastMessage, type, showLastMessage, username, alert
}) => (
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
{formatMsg({
lastMessage, type, showLastMessage, username
})}
</Text>
), arePropsEqual);
LastMessage.propTypes = {
lastMessage: PropTypes.object,
type: PropTypes.string,
showLastMessage: PropTypes.bool,
username: PropTypes.string,
alert: PropTypes.bool
};
export default LastMessage;

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import Status from '../../containers/Status';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import styles from './styles';
const TypeIcon = React.memo(({ type, id, prid }) => {
if (type === 'd') {
return <Status style={styles.status} size={10} id={id} />;
}
return <RoomTypeIcon type={prid ? 'discussion' : type} />;
});
TypeIcon.propTypes = {
type: PropTypes.string,
id: PropTypes.string,
prid: PropTypes.string
};
export default TypeIcon;

View File

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import styles from './styles';
const UnreadBadge = React.memo(({ unread, userMentions, type }) => {
if (!unread || unread <= 0) {
return;
}
if (unread >= 1000) {
unread = '999+';
}
const mentioned = userMentions > 0 && type !== 'd';
return (
<View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentionedContainer]}>
<Text style={[styles.unreadText, mentioned && styles.unreadMentionedText]}>{ unread }</Text>
</View>
);
});
UnreadBadge.propTypes = {
unread: PropTypes.number,
userMentions: PropTypes.number,
type: PropTypes.string
};
export default UnreadBadge;

View File

@ -0,0 +1,120 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import { connect } from 'react-redux';
import { RectButton } from 'react-native-gesture-handler';
import Avatar from '../../containers/Avatar';
import I18n from '../../i18n';
import styles, { ROW_HEIGHT } from './styles';
import UnreadBadge from './UnreadBadge';
import TypeIcon from './TypeIcon';
import LastMessage from './LastMessage';
export { ROW_HEIGHT };
const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
@connect(state => ({
userId: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}))
export default class RoomItem extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool,
_updatedAt: PropTypes.string,
lastMessage: PropTypes.object,
alert: PropTypes.bool,
unread: PropTypes.number,
userMentions: PropTypes.number,
id: PropTypes.string,
prid: PropTypes.string,
onPress: PropTypes.func,
userId: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string,
avatarSize: PropTypes.number,
testID: PropTypes.string,
height: PropTypes.number
}
static defaultProps = {
avatarSize: 48
}
shouldComponentUpdate(nextProps) {
const { lastMessage, _updatedAt } = this.props;
const oldlastMessage = lastMessage;
const newLastmessage = nextProps.lastMessage;
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
return true;
}
if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) {
return true;
}
// eslint-disable-next-line react/destructuring-assignment
return attrs.some(key => nextProps[key] !== this.props[key]);
}
formatDate = date => moment(date).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
})
render() {
const {
unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, onPress, id, prid, showLastMessage, lastMessage
} = this.props;
const date = this.formatDate(_updatedAt);
let accessibilityLabel = name;
if (unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
} else if (unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
}
if (userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
}
if (date) {
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
}
return (
<RectButton
onPress={onPress}
activeOpacity={0.8}
underlayColor='#e1e5e8'
testID={testID}
>
<View
style={[styles.container, height && { height }]}
accessibilityLabel={accessibilityLabel}
>
<Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
<View style={styles.centerContainer}>
<View style={styles.titleContainer}>
<TypeIcon type={type} id={id} prid={prid} />
<Text style={[styles.title, alert && styles.alert]} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
</View>
<View style={styles.row}>
<LastMessage lastMessage={lastMessage} type={type} showLastMessage={showLastMessage} username={username} alert={alert} />
<UnreadBadge unread={unread} userMentions={userMentions} type={type} />
</View>
</View>
</View>
</RectButton>
);
}
}

View File

@ -0,0 +1,97 @@
import { StyleSheet, PixelRatio } from 'react-native';
import sharedStyles from '../../views/Styles';
import {
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_UNREAD, COLOR_TEXT
} from '../../constants/colors';
export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
export default StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 14,
height: ROW_HEIGHT
},
centerContainer: {
flex: 1,
paddingVertical: 10,
paddingRight: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLOR_SEPARATOR
},
title: {
flex: 1,
fontSize: 17,
lineHeight: 20,
...sharedStyles.textColorNormal,
...sharedStyles.textMedium
},
alert: {
...sharedStyles.textSemibold
},
row: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start'
},
titleContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
date: {
fontSize: 13,
marginLeft: 4,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
updateAlert: {
color: COLOR_PRIMARY,
...sharedStyles.textSemibold
},
unreadNumberContainer: {
minWidth: 21,
height: 21,
paddingVertical: 3,
paddingHorizontal: 5,
borderRadius: 10.5,
backgroundColor: COLOR_UNREAD,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 10
},
unreadMentionedContainer: {
backgroundColor: COLOR_PRIMARY
},
unreadText: {
color: COLOR_TEXT,
overflow: 'hidden',
fontSize: 13,
...sharedStyles.textMedium,
letterSpacing: 0.56,
textAlign: 'center'
},
unreadMentionedText: {
color: COLOR_WHITE
},
status: {
marginRight: 7,
marginTop: 3
},
markdownText: {
flex: 1,
fontSize: 14,
lineHeight: 17,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
},
markdownTextAlert: {
...sharedStyles.textColorNormal
},
avatar: {
marginRight: 10
}
});

View File

@ -6,13 +6,14 @@ import PropTypes from 'prop-types';
import Avatar from '../containers/Avatar';
import Touch from '../utils/touch';
import { isIOS } from '../utils/deviceInfo';
import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles';
import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors';
const styles = StyleSheet.create({
button: {
height: 54,
backgroundColor: '#fff'
backgroundColor: COLOR_WHITE
},
container: {
flexDirection: 'row'
@ -23,23 +24,23 @@ const styles = StyleSheet.create({
},
textContainer: {
flex: 1,
flexDirection: 'column'
flexDirection: 'column',
justifyContent: 'center'
},
name: {
fontSize: 18,
color: '#0C0D0F',
marginTop: isIOS ? 6 : 3,
marginBottom: 1,
textAlign: 'left'
fontSize: 17,
...sharedStyles.textMedium,
...sharedStyles.textColorNormal
},
username: {
fontSize: 14,
color: '#9EA2A8'
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
},
icon: {
marginHorizontal: 15,
alignSelf: 'center',
color: '#1D74F5'
color: COLOR_PRIMARY
}
});
@ -48,7 +49,7 @@ const UserItem = ({
}) => (
<Touch onPress={onPress} onLongPress={onLongPress} style={styles.button} testID={testID}>
<View style={[styles.container, style]}>
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} user={user} />
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} />
<View style={styles.textContainer}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.username}>@{username}</Text>

View File

@ -4,7 +4,7 @@ import PushNotification from './push';
import store from '../lib/createStore';
import { deepLinkingOpen } from '../actions/deepLinking';
const onNotification = (notification) => {
export const onNotification = (notification) => {
if (notification) {
const data = notification.getData();
if (data) {
@ -31,13 +31,11 @@ const onNotification = (notification) => {
}
};
const getDeviceToken = () => PushNotification.getDeviceToken();
const setBadgeCount = count => PushNotification.setBadgeCount(count);
const initializePushNotifications = () => {
PushNotification.configure({
export const getDeviceToken = () => PushNotification.getDeviceToken();
export const setBadgeCount = count => PushNotification.setBadgeCount(count);
export const initializePushNotifications = () => {
setBadgeCount();
return PushNotification.configure({
onNotification
});
setBadgeCount();
};
export { initializePushNotifications, getDeviceToken, setBadgeCount };

View File

@ -25,12 +25,7 @@ class PushNotification {
this.onRegister = params.onRegister;
this.onNotification = params.onNotification;
NotificationsAndroid.refreshToken();
PendingNotifications.getInitialNotification()
.then((notification) => {
this.onNotification(notification);
})
.catch(e => console.warn(e));
return PendingNotifications.getInitialNotification();
}
}

View File

@ -30,6 +30,7 @@ class PushNotification {
this.onNotification = params.onNotification;
NotificationsIOS.consumeBackgroundQueue();
return Promise.resolve();
}
}
export default new PushNotification();

View File

@ -1,15 +0,0 @@
import * as types from '../actions/actionsTypes';
const initialState = {};
export default (state = initialState, action) => {
switch (action.type) {
case types.ACTIVE_USERS.SET:
return {
...state,
...action.data
};
default:
return state;
}
};

View File

@ -3,15 +3,12 @@ import settings from './reducers';
import login from './login';
import meteor from './connect';
import messages from './messages';
import room from './room';
import rooms from './rooms';
import server from './server';
import selectedUsers from './selectedUsers';
import createChannel from './createChannel';
import app from './app';
import customEmojis from './customEmojis';
import activeUsers from './activeUsers';
import roles from './roles';
import sortPreferences from './sortPreferences';
export default combineReducers({
@ -23,10 +20,7 @@ export default combineReducers({
selectedUsers,
createChannel,
app,
room,
rooms,
customEmojis,
activeUsers,
roles,
sortPreferences
});

View File

@ -1,11 +1,10 @@
import * as types from '../actions/actionsTypes';
const initialState = {
isFetching: false,
failure: false,
message: {},
actionMessage: {},
replyMessage: {},
replying: false,
editing: false,
showActions: false,
showErrorActions: false,
@ -14,23 +13,6 @@ const initialState = {
export default function messages(state = initialState, action) {
switch (action.type) {
case types.MESSAGES.REQUEST:
return {
...state,
isFetching: true
};
case types.MESSAGES.SUCCESS:
return {
...state,
isFetching: false
};
case types.LOGIN.FAILURE:
return {
...state,
isFetching: false,
failure: true,
errorMessage: action.err
};
case types.MESSAGES.ACTIONS_SHOW:
return {
...state,
@ -83,12 +65,14 @@ export default function messages(state = initialState, action) {
replyMessage: {
...action.message,
mention: action.mention
}
},
replying: true
};
case types.MESSAGES.REPLY_CANCEL:
return {
...state,
replyMessage: {}
replyMessage: {},
replying: false
};
case types.MESSAGES.SET_INPUT:
return {

View File

@ -1,15 +0,0 @@
import * as types from '../actions/actionsTypes';
const initialState = {};
export default (state = initialState, action) => {
switch (action.type) {
case types.ROLES.SET:
return {
...state,
...action.data
};
default:
return state;
}
};

View File

@ -1,37 +0,0 @@
import * as types from '../actions/actionsTypes';
const initialState = {
usersTyping: []
};
export default function room(state = initialState, action) {
switch (action.type) {
case types.ROOM.OPEN:
return {
...initialState,
...action.room,
lastOpen: new Date()
};
case types.ROOM.CLOSE:
return {
...initialState
};
case types.ROOM.SET_LAST_OPEN:
return {
...state,
lastOpen: action.date
};
case types.ROOM.ADD_USER_TYPING:
return {
...state,
usersTyping: [...state.usersTyping.filter(user => user !== action.username), action.username]
};
case types.ROOM.REMOVE_USER_TYPING:
return {
...state,
usersTyping: [...state.usersTyping.filter(user => user !== action.username)]
};
default:
return state;
}
}

View File

@ -5,6 +5,7 @@ const initialState = {
connected: false,
failure: false,
server: '',
version: null,
loading: true,
adding: false
};
@ -29,6 +30,7 @@ export default function server(state = initialState, action) {
return {
...state,
server: action.server,
version: action.version,
connecting: true,
connected: false,
loading: true
@ -37,6 +39,7 @@ export default function server(state = initialState, action) {
return {
...state,
server: action.server,
version: action.version,
connecting: false,
connected: true,
loading: false

View File

@ -1,7 +1,7 @@
import { AsyncStorage } from 'react-native';
import { delay } from 'redux-saga';
import {
takeLatest, take, select, put, all, race
takeLatest, take, select, put, all
} from 'redux-saga/effects';
import Navigation from '../lib/Navigation';
@ -10,28 +10,25 @@ import { selectServerRequest } from '../actions/server';
import database from '../lib/realm';
import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events';
import { appStart } from '../actions';
const roomTypes = {
channel: 'c', direct: 'd', group: 'p'
};
const navigate = function* navigate({ params }) {
yield put(appStart('inside'));
if (params.rid) {
const canOpenRoom = yield RocketChat.canOpenRoom(params);
if (canOpenRoom) {
const [type, name] = params.path.split('/');
yield Navigation.navigate('RoomsListView');
Navigation.navigate('RoomView', { rid: params.rid, name, t: roomTypes[type] });
}
}
};
const handleOpen = function* handleOpen({ params }) {
const isReady = yield select(state => state.app.ready);
if (!isReady) {
yield take(types.APP.READY);
}
if (!params.host) {
return;
}
@ -54,29 +51,28 @@ const handleOpen = function* handleOpen({ params }) {
// if deep link is from same server
if (server === host) {
if (user) {
yield race({
typing: take(types.SERVER.SELECT_SUCCESS),
timeout: delay(3000)
});
const connected = yield select(state => state.server.connected);
if (!connected) {
yield put(selectServerRequest(host));
yield take(types.SERVER.SELECT_SUCCESS);
}
yield navigate({ params });
} else {
yield put(appStart('outside'));
}
} else {
// if deep link is from a different server
const result = yield RocketChat.testServer(server);
if (!result.success) {
return;
}
// search if deep link's server already exists
const servers = yield database.databases.serversDB.objects('servers').filtered('id = $0', host); // TODO: need better test
if (servers.length && user) {
yield put(selectServerRequest(host));
yield race({
typing: take(types.SERVER.SELECT_SUCCESS),
timeout: delay(3000)
});
yield take(types.SERVER.SELECT_SUCCESS);
yield navigate({ params });
} else {
// if deep link is from a different server
const result = yield RocketChat.getServerInfo(server);
if (!result.success) {
return;
}
Navigation.navigate('OnboardingView', { previousServer: server });
yield delay(1000);
EventEmitter.emit('NewServer', { server: host });

View File

@ -9,6 +9,7 @@ import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
import database from '../lib/realm';
const restore = function* restore() {
try {
@ -27,7 +28,8 @@ const restore = function* restore() {
]);
yield put(actions.appStart('outside'));
} else if (server) {
yield put(selectServerRequest(server));
const serverObj = database.databases.serversDB.objectForPrimaryKey('servers', server);
yield put(selectServerRequest(server, serverObj && serverObj.version));
}
yield put(actions.appReady({}));

View File

@ -4,8 +4,6 @@ import { takeLatest, put, call } from 'redux-saga/effects';
import Navigation from '../lib/Navigation';
import { MESSAGES } from '../actions/actionsTypes';
import {
messagesSuccess,
messagesFailure,
deleteSuccess,
deleteFailure,
editSuccess,
@ -25,19 +23,6 @@ const editMessage = message => RocketChat.editMessage(message);
const toggleStarMessage = message => RocketChat.toggleStarMessage(message);
const togglePinMessage = message => RocketChat.togglePinMessage(message);
const get = function* get({ room }) {
try {
if (room.lastOpen) {
yield RocketChat.loadMissedMessages(room);
} else {
yield RocketChat.loadMessagesForRoom(room);
}
yield put(messagesSuccess());
} catch (err) {
yield put(messagesFailure(err));
}
};
const handleDeleteRequest = function* handleDeleteRequest({ message }) {
try {
yield call(deleteMessage, message);
@ -97,7 +82,6 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
};
const root = function* root() {
yield takeLatest(MESSAGES.REQUEST, get);
yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest);
yield takeLatest(MESSAGES.EDIT_REQUEST, handleEditRequest);
yield takeLatest(MESSAGES.TOGGLE_STAR_REQUEST, handleToggleStarRequest);

View File

@ -1,122 +1,30 @@
import { Alert } from 'react-native';
import {
put, call, takeLatest, take, select, race, fork, cancel, takeEvery
call, takeLatest, take, select
} from 'redux-saga/effects';
import { delay } from 'redux-saga';
import EJSON from 'ejson';
import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes';
import { addUserTyping, removeUserTyping } from '../actions/room';
import { messagesRequest, editCancel, replyCancel } from '../actions/messages';
import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import log from '../utils/log';
import I18n from '../i18n';
let sub;
let thread;
const cancelTyping = function* cancelTyping(username) {
while (true) {
const { typing, timeout } = yield race({
typing: take(types.ROOM.SOMEONE_TYPING),
timeout: call(delay, 5000)
});
if (timeout || (typing.username === username && !typing.typing)) {
return yield put(removeUserTyping(username));
}
}
};
const usersTyping = function* usersTyping({ rid }) {
while (true) {
const { _rid, username, typing } = yield take(types.ROOM.SOMEONE_TYPING);
if (_rid === rid) {
yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username)));
if (typing) {
yield fork(cancelTyping, username);
}
}
}
};
const handleMessageReceived = function* handleMessageReceived({ message }) {
try {
const room = yield select(state => state.room);
if (message.rid === room.rid) {
database.write(() => {
database.create('messages', EJSON.fromJSONValue(message), true);
});
if (room._id) {
RocketChat.readMessages(room.rid);
}
}
} catch (e) {
console.warn('handleMessageReceived', e);
}
};
let opened = false;
const watchRoomOpen = function* watchRoomOpen({ room }) {
try {
if (opened) {
return;
}
opened = true;
const auth = yield select(state => state.login.isAuthenticated);
if (!auth) {
yield take(types.LOGIN.SUCCESS);
}
yield put(messagesRequest({ ...room }));
if (room._id) {
RocketChat.readMessages(room.rid);
}
sub = yield RocketChat.subscribeRoom(room);
thread = yield fork(usersTyping, { rid: room.rid });
yield race({
open: take(types.ROOM.OPEN),
close: take(types.ROOM.CLOSE)
});
opened = false;
cancel(thread);
sub.stop();
yield put(editCancel());
yield put(replyCancel());
} catch (e) {
log('watchRoomOpen', e);
}
};
const watchuserTyping = function* watchuserTyping({ status }) {
const watchUserTyping = function* watchUserTyping({ rid, status }) {
const auth = yield select(state => state.login.isAuthenticated);
if (!auth) {
yield take(types.LOGIN.SUCCESS);
}
const room = yield select(state => state.room);
if (!room) {
return;
}
try {
yield RocketChat.emitTyping(room.rid, status);
yield RocketChat.emitTyping(rid, status);
if (status) {
yield call(delay, 5000);
yield RocketChat.emitTyping(room.rid, false);
yield RocketChat.emitTyping(rid, false);
}
} catch (e) {
log('watchuserTyping', e);
log('watchUserTyping', e);
}
};
@ -147,9 +55,7 @@ const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
};
const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
yield takeEvery(types.ROOM.OPEN, watchRoomOpen);
yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
};

View File

@ -5,15 +5,37 @@ import Navigation from '../lib/Navigation';
import { SERVER } from '../actions/actionsTypes';
import * as actions from '../actions';
import { serverFailure, selectServerRequest, selectServerSuccess } from '../actions/server';
import { setRoles } from '../actions/roles';
import { setUser } from '../actions/login';
import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import log from '../utils/log';
import I18n from '../i18n';
const handleSelectServer = function* handleSelectServer({ server }) {
const getServerInfo = function* getServerInfo({ server }) {
try {
const serverInfo = yield RocketChat.getServerInfo(server);
if (!serverInfo.success) {
Alert.alert(I18n.t('Oops'), I18n.t(serverInfo.message, serverInfo.messageOptions));
yield put(serverFailure());
return;
}
database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server, version: serverInfo.version }, true);
});
return serverInfo;
} catch (e) {
log('getServerInfo', e);
}
};
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
try {
let serverInfo;
if (fetchVersion) {
serverInfo = yield getServerInfo({ server });
}
yield AsyncStorage.setItem('currentServer', server);
const userStringified = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
@ -24,19 +46,15 @@ const handleSelectServer = function* handleSelectServer({ server }) {
yield put(actions.appStart('inside'));
} else {
RocketChat.connect({ server });
yield put(actions.appStart('outside'));
}
const settings = database.objects('settings');
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
const emojis = database.objects('customEmojis');
yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length))));
const roles = database.objects('roles');
yield put(setRoles(roles.reduce((result, role) => {
result[role._id] = role.description;
return result;
}, {})));
yield put(selectServerSuccess(server));
yield put(selectServerSuccess(server, fetchVersion ? serverInfo && serverInfo.version : version));
} catch (e) {
log('handleSelectServer', e);
}
@ -44,13 +62,9 @@ const handleSelectServer = function* handleSelectServer({ server }) {
const handleServerRequest = function* handleServerRequest({ server }) {
try {
const result = yield RocketChat.testServer(server);
if (!result.success) {
Alert.alert(I18n.t('Oops'), I18n.t(result.message, result.messageOptions));
yield put(serverFailure());
return;
}
const serverInfo = yield getServerInfo({ server });
// TODO: cai aqui O.o
const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) {
Navigation.navigate('LoginView');
@ -58,10 +72,7 @@ const handleServerRequest = function* handleServerRequest({ server }) {
Navigation.navigate('LoginSignupView');
}
database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server }, true);
});
yield put(selectServerRequest(server));
yield put(selectServerRequest(server, serverInfo.version, false));
} catch (e) {
yield put(serverFailure());
log('handleServerRequest', e);

View File

@ -1,10 +1,11 @@
import React from 'react';
import { TouchableHighlight } from 'react-native';
import PropTypes from 'prop-types';
import { COLOR_WHITE } from '../../constants/colors';
const Touch = ({ children, onPress, ...props }) => (
<TouchableHighlight
underlayColor='#FFFFFF'
underlayColor={COLOR_WHITE}
activeOpacity={0.5}
onPress={onPress}
{...props}

View File

@ -1,11 +1,8 @@
import React from 'react';
import { StyleSheet, Image } from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusBar from '../containers/StatusBar';
import { isAndroid } from '../utils/deviceInfo';
import { appInit as appInitAction } from '../actions';
const styles = StyleSheet.create({
image: {
@ -14,25 +11,9 @@ const styles = StyleSheet.create({
}
});
@connect(null, dispatch => ({
appInit: () => dispatch(appInitAction())
}))
export default class Loading extends React.PureComponent {
static propTypes = {
appInit: PropTypes.func
}
constructor(props) {
super(props);
props.appInit();
}
render() {
return (
<React.Fragment>
<StatusBar />
{isAndroid ? <Image source={{ uri: 'launch_screen' }} style={styles.image} /> : null}
</React.Fragment>
);
}
}
export default React.memo(() => (
<React.Fragment>
<StatusBar />
{isAndroid ? <Image source={{ uri: 'launch_screen' }} style={styles.image} /> : null}
</React.Fragment>
));

View File

@ -20,6 +20,7 @@ import { showErrorAlert } from '../utils/info';
import { isAndroid } from '../utils/deviceInfo';
import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { COLOR_TEXT_DESCRIPTION, COLOR_WHITE } from '../constants/colors';
const styles = StyleSheet.create({
container: {
@ -28,7 +29,7 @@ const styles = StyleSheet.create({
},
list: {
width: '100%',
backgroundColor: '#FFFFFF'
backgroundColor: COLOR_WHITE
},
separator: {
marginLeft: 60
@ -39,22 +40,23 @@ const styles = StyleSheet.create({
input: {
height: 54,
paddingHorizontal: 18,
color: '#9EA2A8',
backgroundColor: '#fff',
fontSize: 18
fontSize: 17,
...sharedStyles.textRegular,
...sharedStyles.textColorNormal,
backgroundColor: COLOR_WHITE
},
swithContainer: {
height: 54,
backgroundColor: '#fff',
backgroundColor: COLOR_WHITE,
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
paddingHorizontal: 18
},
label: {
color: '#0C0D0F',
fontSize: 18,
fontWeight: '500'
fontSize: 17,
...sharedStyles.textMedium,
...sharedStyles.textColorNormal
},
invitedHeader: {
marginTop: 18,
@ -64,14 +66,15 @@ const styles = StyleSheet.create({
alignItems: 'center'
},
invitedTitle: {
color: '#2F343D',
fontSize: 22,
fontWeight: 'bold',
fontSize: 18,
...sharedStyles.textSemibold,
...sharedStyles.textColorNormal,
lineHeight: 41
},
invitedCount: {
color: '#9EA2A8',
fontSize: 15
fontSize: 14,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
}
});
@ -347,6 +350,7 @@ export default class CreateChannelView extends LoggedView {
value={channelName}
onChangeText={this.onChangeText}
placeholder={I18n.t('Channel_Name')}
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
returnKeyType='done'
testID='create-channel-name'
autoCorrect={false}

View File

@ -5,14 +5,16 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-navigation';
import { RectButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux';
import sharedStyles from './Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps';
import LoggedView from './View';
import I18n from '../i18n';
import DisclosureIndicator from '../containers/DisclosureIndicator';
import { CloseModalButton } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { COLOR_SEPARATOR, COLOR_WHITE } from '../constants/colors';
import openLink from '../utils/openLink';
const styles = StyleSheet.create({
container: {
@ -21,13 +23,13 @@ const styles = StyleSheet.create({
},
scroll: {
marginTop: 35,
backgroundColor: '#fff',
borderColor: '#cbced1',
backgroundColor: COLOR_WHITE,
borderColor: COLOR_SEPARATOR,
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth
},
separator: {
backgroundColor: '#cbced1',
backgroundColor: COLOR_SEPARATOR,
height: StyleSheet.hairlineWidth,
width: '100%',
marginLeft: 20
@ -35,7 +37,7 @@ const styles = StyleSheet.create({
item: {
width: '100%',
height: 48,
backgroundColor: '#fff',
backgroundColor: COLOR_WHITE,
paddingLeft: 20,
paddingRight: 10,
flexDirection: 'row',
@ -44,22 +46,24 @@ const styles = StyleSheet.create({
},
text: {
...sharedStyles.textMedium,
color: '#0c0d0f',
...sharedStyles.textColorNormal,
fontSize: 18
}
});
const Separator = () => <View style={styles.separator} />;
@connect(state => ({
server: state.server.server
}))
/** @extends React.Component */
export default class LegalView extends LoggedView {
static navigationOptions = ({ navigation }) => ({
headerLeft: <CloseModalButton testID='legal-view-close' navigation={navigation} />,
static navigationOptions = () => ({
title: I18n.t('Legal')
})
static propTypes = {
navigation: PropTypes.object
server: PropTypes.string
}
constructor(props) {
@ -67,8 +71,11 @@ export default class LegalView extends LoggedView {
}
onPressItem = ({ route }) => {
const { navigation } = this.props;
navigation.navigate(route);
const { server } = this.props;
if (!server) {
return;
}
openLink(`${ server }/${ route }`);
}
renderItem = ({ text, route, testID }) => (
@ -83,9 +90,9 @@ export default class LegalView extends LoggedView {
<SafeAreaView style={styles.container} testID='legal-view' forceInset={{ bottom: 'never' }}>
<StatusBar />
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.scroll}>
{this.renderItem({ text: 'Terms_of_Service', route: 'TermsServiceView', testID: 'legal-terms-button' })}
{this.renderItem({ text: 'Terms_of_Service', route: 'terms-of-service', testID: 'legal-terms-button' })}
<Separator />
{this.renderItem({ text: 'Privacy_Policy', route: 'PrivacyPolicyView', testID: 'legal-privacy-button' })}
{this.renderItem({ text: 'Privacy_Policy', route: 'privacy-policy', testID: 'legal-privacy-button' })}
</ScrollView>
</SafeAreaView>
);

View File

@ -17,6 +17,7 @@ import Button from '../containers/Button';
import I18n from '../i18n';
import { LegalButton } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { COLOR_SEPARATOR, COLOR_BORDER } from '../constants/colors';
const styles = StyleSheet.create({
container: {
@ -32,7 +33,7 @@ const styles = StyleSheet.create({
serviceButtonContainer: {
borderRadius: 2,
borderWidth: 1,
borderColor: '#e1e5e8',
borderColor: COLOR_BORDER,
width: '100%',
height: 48,
flexDirection: 'row',
@ -49,8 +50,8 @@ const styles = StyleSheet.create({
},
serviceText: {
...sharedStyles.textRegular,
fontSize: 16,
color: '#2f343d'
...sharedStyles.textColorNormal,
fontSize: 16
},
serviceName: {
...sharedStyles.textBold
@ -72,7 +73,7 @@ const styles = StyleSheet.create({
separatorLine: {
flex: 1,
height: 1,
backgroundColor: '#e1e5e8'
backgroundColor: COLOR_SEPARATOR
},
separatorLineLeft: {
marginRight: 15

View File

@ -18,6 +18,7 @@ import I18n from '../i18n';
import { loginRequest as loginRequestAction } from '../actions/login';
import { LegalButton } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { COLOR_PRIMARY } from '../constants/colors';
const styles = StyleSheet.create({
buttonsContainer: {
@ -31,12 +32,12 @@ const styles = StyleSheet.create({
},
dontHaveAccount: {
...sharedStyles.textRegular,
color: '#9ea2a8',
...sharedStyles.textColorDescription,
fontSize: 13
},
createAccount: {
...sharedStyles.textSemibold,
color: '#1d74f5',
color: COLOR_PRIMARY,
fontSize: 13
},
loginTitle: {
@ -96,7 +97,7 @@ export default class LoginView extends LoggedView {
componentWillReceiveProps(nextProps) {
const { Site_Name, error } = this.props;
if (Site_Name && nextProps.Site_Name !== Site_Name) {
if (nextProps.Site_Name && nextProps.Site_Name !== Site_Name) {
this.setTitle(nextProps.Site_Name);
} else if (nextProps.failure && !equal(error, nextProps.error)) {
if (nextProps.error && nextProps.error.error === 'totp-required') {

View File

@ -16,7 +16,6 @@ import StatusBar from '../../containers/StatusBar';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis,
room: state.room,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
@ -33,7 +32,7 @@ export default class MentionedMessagesView extends LoggedView {
user: PropTypes.object,
baseUrl: PropTypes.string,
customEmojis: PropTypes.object,
room: PropTypes.object
navigation: PropTypes.object
}
constructor(props) {
@ -42,6 +41,8 @@ export default class MentionedMessagesView extends LoggedView {
loading: false,
messages: []
};
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
}
componentDidMount() {
@ -71,10 +72,9 @@ export default class MentionedMessagesView extends LoggedView {
this.setState({ loading: true });
try {
const { room } = this.props;
const result = await RocketChat.getMessages(
room.rid,
room.t,
this.rid,
this.t,
{ 'mentions._id': { $in: [user.id] } },
messages.length
);
@ -93,7 +93,7 @@ export default class MentionedMessagesView extends LoggedView {
renderEmpty = () => (
<View style={styles.listEmptyContainer} testID='mentioned-messages-view'>
<Text>{I18n.t('No_mentioned_messages')}</Text>
<Text style={styles.noDataFound}>{I18n.t('No_mentioned_messages')}</Text>
</View>
)
@ -101,7 +101,6 @@ export default class MentionedMessagesView extends LoggedView {
const { user, customEmojis, baseUrl } = this.props;
return (
<Message
style={styles.message}
customEmojis={customEmojis}
baseUrl={baseUrl}
user={user}

View File

@ -1,17 +1,22 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../Styles';
import { COLOR_WHITE } from '../../constants/colors';
export default StyleSheet.create({
list: {
flex: 1,
backgroundColor: '#ffffff'
},
message: {
transform: [{ scaleY: 1 }]
backgroundColor: COLOR_WHITE
},
listEmptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff'
backgroundColor: COLOR_WHITE
},
noDataFound: {
fontSize: 14,
...sharedStyles.textRegular,
...sharedStyles.textColorNormal
}
});

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