[RELEASE] Merge beta into master (#961)

* 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)

* issue #799 merger message views (#876)

On Room Actions, we have Files, Mentions, Starred and Pinned.
They have similar APIs and logic.
All of those could be merged into one generic view (MessagesView).
Maybe even Search could be in this merge.

Note: They're similar, but have own rules (unstar, unpin, etc).

This change may reduce 1MB to our release bundle size, since we're going to remove a lot of boilerplate.

* Bump version to 1.14.0 (#889)

* Remove "updating" indicator inside the room (#895)

* Switch toast lib (#898)

* removed toast from ios

* changed showToast to showAlert

* removed from android

* fix lint

* conflict resolved

* fixed lint

* Fix toast position

* Change toast style

* Use followMessage from rest

* Temporary disable some visual toast tests

* Unnecessary lib version change

* [NEW] Report message (#818)

* [NEW] Admin (#800)

* added admin panel

* reverting some changes

* fixed problem with authToken

* changed tab to space

* done requested changes

* fixed lint

* added react-native-webview

* Install webview pod

* Message render performance (#880)

- Refactored Message component to use React.memo and re-render only what's necessary
- Added a test mode to toggle markdown parse by long press drawer (it'll be removed in the next release)

* [CHORE] Add pre-commit rules (#816)

Run lint and jest during pre-commit

* [IMPROVEMENT] Add toggle markdown to settings (#907)

* Add toggle markdown to settings

* Remove unused translation

* [FIX] Message grouping not re-rendering (#911)

* [FIX] Get custom emoji on reactions modal (#913)

* Update RN to 0.59.8 (#896)

* update IOS react native to 0.59.8

* update Android react native to 0.59.8

* fix eslint errors

* Android debug working

* Android build

* Fix lint

* Making jest happy

* Update CircleCI android image

* Fix android build

* Use 32 bits

* Fix iOS build

* Update detox

* Use new Xcode build system

* Use old build system

* Update realm (64 bits support)

* [CHORE] Upgrade Mac CI image to 10.2.1 (#914)

* [CHORE] Update readme (#885)

* [FIX] Reaction count not rerendering (#917)

* [CHORE] Android app bundle (#915)

* [CHORE] Upgrade Mac CI image to 10.2.1

* [CHORE] Android App Bundle

* Fix CI

* Fix arch typo (#918)

* [IMPROVEMENT] Messagebox typing and buttons refactor (#920)

* Debounce onChangeText

* Refactor FilesActions

* Clear input asap

* Different buttons on iOS/Android

* Minor fragment refactor

* Import emoji keyboard on android only

* [CHORE] Use react-native-firebase (#928)

We need to migrate from deprecated react-native-fabric to react-native-firebase.
This PR enables following Firebase features:
* Analytics
* Crashlytics
* Performance

It also tracks screen view without the necessity of HOC.

Future work:
I won't do it in this PR because it's large enough, but we need to log more app events, like 'sent_message', 'open_admin', 'media_upload', etc.

* [FIX] Analytics error events (#930)

* [IMPROVEMENT] Update user presence endpoint (#924)

* [IMPROVEMENT] Update user presence endpoint

* Use `from` parameter in case of reconnection

* [IMPROVEMENT] Share channel (#908)

* Generate and share permalink to rooms

* Create constant to share type

* Fix unnecessary await

* Remove unnecessary test

* Revert delete e2e test

* [FIX] App crash with backspace on input message (#906)

* Fix - App crash with backspace on input message

* Improving code to fix backspace bug

* Fix destructuring undefined

* Improvement code to fix backspace bug

* [FIX] Gitlab url hardcoded  (#921)

* [FIX] Gitlab url hardcoded problem
* Closes https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/251

* Fix API_Gitlab_URL type

* [CHORE] Split Google Services in debug and production (#941)

* Split android

* Split iOS

* Update CI

* [FIX] Crash on message long press (#945)

* [FIX] Reply preview showing the entire message (#947)

* [IMPROVEMENT] Open links as push instead of modal (#949)

* [FIX] Crashing during app launch on Samsung devices (#937)

* Apply alpha update

* Update to Realm released fix

* [FIX] Thread crash if room is undefined (#956)
This commit is contained in:
Diego Mello 2019-06-05 09:42:18 -03:00 committed by GitHub
parent fe46929238
commit 1610ba5759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13825 changed files with 2720952 additions and 22968 deletions

View File

@ -44,7 +44,7 @@ jobs:
e2e-test:
macos:
xcode: "10.1.0"
xcode: "10.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
@ -90,7 +90,7 @@ jobs:
android-build:
<<: *defaults
docker:
- image: circleci/android:api-28-node8-alpha
- image: circleci/android:api-28-node
environment:
# GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
@ -131,24 +131,18 @@ jobs:
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
if [[ $FABRIC_KEY ]]; then
echo -e "" > ./app/fabric.properties
echo -e "apiKey=$FABRIC_KEY" >> ./app/fabric.properties
echo -e "apiSecret=$FABRIC_SECRET" >> ./app/fabric.properties
fi
- run:
name: Install Android Depedencies
name: Set Google Services
command: |
cd android
./gradlew androidDependencies
cd android/app
cp google-services.prod.json google-services.json
- run:
name: Build Android App
command: |
cd android
if [[ $KEYSTORE ]]; then
./gradlew assembleRelease
./gradlew bundleRelease
else
./gradlew assembleDebug
fi
@ -172,7 +166,7 @@ jobs:
ios-build:
macos:
xcode: "10.1.0"
xcode: "10.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
@ -201,13 +195,11 @@ jobs:
command: |
yarn
# - run:
# name: Fix known build error
# command: |
# # Fix error https://github.com/facebook/react-native/issues/14382
# cd node_modules/react-native/scripts/
# curl https://raw.githubusercontent.com/facebook/react-native/5c53f89dd86160301feee024bce4ce0c89e8c187/scripts/ios-configure-glog.sh > ios-configure-glog.sh
# chmod +x ios-configure-glog.sh
- run:
name: Set Google Services
command: |
cd ios
cp GoogleService-Info.prod.plist GoogleService-Info.plist
- run:
name: Fastlane Build
@ -215,11 +207,6 @@ jobs:
command: |
cd ios
agvtool new-version -all $CIRCLE_BUILD_NUM
/usr/libexec/PlistBuddy -c "Set Fabric:APIKey $FABRIC_KEY" ./RocketChatRN/Info.plist
if [[ $FABRIC_KEY ]]; then
echo -e > "./Fabric.framework/run $FABRIC_KEY $FABRIC_SECRET" > ./RocketChatRN/Fabric.sh
fi
if [[ $MATCH_KEYCHAIN_NAME ]]; then
fastlane ios release
@ -240,7 +227,7 @@ jobs:
ios-testflight:
macos:
xcode: "10.1.0"
xcode: "10.2.1"
steps:
- checkout

View File

@ -118,7 +118,7 @@ module.exports = {
"new-cap": [2],
"use-isnan": 2,
"valid-typeof": 2,
"linebreak-style": [2, "unix"],
"linebreak-style": 0,
"prefer-template": 2,
"template-curly-spacing": [2, "always"],
"quotes": [2, "single"],

View File

@ -61,11 +61,9 @@ Readme will guide you on how to config.
## Current priorities
1) [NEW] Jitsi integration ([#711][i711])
2) [NEW] Federation ([#706][i706])
3) [NEW] Threads ([#707][i707])
4) [NEW] Record video ([#712][i712])
5) [NEW] Slash Commands ([#405][i405])
6) [NEW] Draft message per room ([#708][i708])
7) [NEW] Share extension ([#391][i391])
3) [NEW] Record video ([#712][i712])
4) [NEW] Slash Commands ([#405][i405])
5) [NEW] Share extension ([#391][i391])
[i711]: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/711
[i706]: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/706
@ -80,11 +78,11 @@ Readme will guide you on how to config.
|--------------------------------------------------------------- |-------- |
| Jitsi Integration | ❌ |
| Federation (Directory) | ❌ |
| Threads | |
| Threads | |
| Record Audio | ✅ |
| Record Video | ❌ |
| Commands | ❌ |
| Draft message per room | |
| Draft message per room | |
| Share Extension | ❌ |
| Notifications Preferences | ✅ |
| Edited status | ✅ |
@ -102,7 +100,7 @@ Readme will guide you on how to config.
| Theming | ❌ |
| Settings -> Review the App | ❌ |
| Settings -> Default Browser | ❌ |
| Admin panel | |
| Admin panel | |
| Reply message from notification | ❌ |
| Unread counter banner on message list | ✅ |
| E2E | ❌ |

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
apply plugin: "com.android.application"
apply plugin: "io.fabric"
apply plugin: "com.google.firebase.firebase-perf"
import com.android.build.OutputFile
@ -95,25 +97,22 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "chat.rocket.reactnative"
minSdkVersion 21
targetSdkVersion 28
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "1.13.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
versionName "1.14.0"
vectorDrawables.useSupportLibrary = true
}
packagingOptions {
pickFirst '**/libjsc.so'
}
signingConfigs {
release {
if (project.hasProperty('KEYSTORE')) {
@ -129,14 +128,11 @@ android {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86"
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
buildTypes {
release {
// shrinkResources enableProguardInReleaseBuilds
// zipAlignEnabled enableProguardInReleaseBuilds
// useProguard enableProguardInReleaseBuilds
minifyEnabled enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
signingConfig signingConfigs.release
@ -147,7 +143,7 @@ android {
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a":1, "x86":2]
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
@ -155,51 +151,23 @@ android {
}
}
}
}
buildscript {
repositories {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
// These docs use an open ended version so that our plugin
// can be updated quickly in response to Android tooling updates
// We recommend changing it to the latest version from our changelog:
// https://docs.fabric.io/android/changelog.html#fabric-gradle-plugin
classpath 'io.fabric.tools:gradle:1.+'
}
}
apply plugin: 'io.fabric'
repositories {
maven { url 'https://maven.fabric.io/public' }
}
configurations.all {
resolutionStrategy {
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'
}
bundle {
language {
enableSplit = false
}
density {
enableSplit = true
}
abi {
enableSplit = true
}
}
}
dependencies {
implementation project(':react-native-firebase')
implementation project(':react-native-webview')
implementation project(':react-native-orientation-locker')
implementation project(':react-native-splash-screen')
implementation project(':react-native-screens')
@ -208,31 +176,30 @@ dependencies {
implementation project(':react-native-gesture-handler')
implementation project(':react-native-image-crop-picker')
implementation project(':react-native-i18n')
implementation project(':react-native-fabric')
implementation project(':react-native-audio')
implementation project(":reactnativekeyboardinput")
implementation project(':react-native-video')
implementation project(':react-native-vector-icons')
implementation project(':rn-fetch-blob')
implementation project(':react-native-toast')
implementation project(':react-native-fast-image')
implementation project(':realm')
implementation project(':reactnativenotifications')
implementation fileTree(dir: "libs", include: ["*.jar"])
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.android.support:appcompat-v7:${ rootProject.ext.supportLibVersion }"
implementation "com.android.support:support-v4:${ rootProject.ext.supportLibVersion }"
implementation "com.android.support:customtabs:${ rootProject.ext.supportLibVersion }"
implementation "com.android.support:design:${ rootProject.ext.supportLibVersion }"
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.firebase:firebase-core:16.0.1"
implementation "com.google.firebase:firebase-messaging:17.3.4"
implementation "com.google.android.gms:play-services-base:16.1.0"
implementation "com.google.firebase:firebase-messaging:18.0.0"
implementation "com.google.firebase:firebase-core:16.0.9"
implementation "com.google.firebase:firebase-perf:16.2.5"
implementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') {
transitive = true;
transitive = true
}
}
@ -244,4 +211,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
}
apply plugin: 'com.google.gms.google-services'
com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true

View File

@ -1,2 +0,0 @@
apiKey=ef3f46fdf18479fd3e1b9b78d0ec73751a255e14
apiSecret=e8e3d04c28bc04acd009484da5bb9d1440c4f53851564e9f95c3225ec8b0bc76

View File

@ -1,242 +1,37 @@
{
"project_info": {
"project_number": "673693445664",
"firebase_url": "https://rocketchat-9e9be.firebaseio.com",
"project_id": "rocketchat-9e9be",
"storage_bucket": "rocketchat-9e9be.appspot.com"
"project_number": "115198584049",
"firebase_url": "https://rocketchat-reactnative-test.firebaseio.com",
"project_id": "rocketchat-reactnative-test",
"storage_bucket": "rocketchat-reactnative-test.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",
"mobilesdk_app_id": "1:115198584049:android:8be27b1f7c42a2ed",
"android_client_info": {
"package_name": "chat.rocket.reactnative"
}
},
"oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_id": "115198584049-ack609b1338b827fta26s9rd2ab1aad5.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
"current_key": "AIzaSyAWwowhAfACHBw3YxmDOXY3QyakgjhJLqc"
}
],
"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_id": "115198584049-ack609b1338b827fta26s9rd2ab1aad5.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
}
}
}

View File

@ -0,0 +1,231 @@
{
"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": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.reactnative"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:673693445664:android:16da2e50aff9f0c9",
"android_client_info": {
"package_name": "chat.rocket.android"
}
},
"oauth_client": [
{
"client_id": "673693445664-k0mvosdjoe5dbvqce3b377ckabb5dgu8.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "chat.rocket.android",
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
}
},
{
"client_id": "673693445664-hrjftksij02vqtd467ln2cubvu48ft5j.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "chat.rocket.android",
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
}
},
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.reactnative"
}
}
]
}
}
},
{
"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": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.reactnative"
}
}
]
}
}
},
{
"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": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.reactnative"
}
}
]
}
}
},
{
"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": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "chat.rocket.reactnative"
}
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" android:networkSecurityConfig="@xml/react_native_config" />
</manifest>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">10.0.3.2</domain>
</domain-config>
</network-security-config>

View File

@ -1,8 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="chat.rocket.reactnative">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
@ -15,7 +15,7 @@
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:resizeableActivity="true">
>
<activity
android:name=".MainActivity"
android:label="@string/app_name"

View File

@ -3,6 +3,11 @@ package chat.rocket.reactnative;
import android.app.Application;
import com.facebook.react.ReactApplication;
import io.invertase.firebase.RNFirebasePackage;
import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage;
import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage;
import io.invertase.firebase.perf.RNFirebasePerformancePackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import org.wonday.orientation.OrientationPackage;
import org.devio.rn.splashscreen.SplashScreenReactPackage;
import com.facebook.react.ReactNativeHost;
@ -14,12 +19,9 @@ import com.AlexanderZaytsev.RNI18n.RNI18nPackage;
import com.reactnative.ivpusic.imagepicker.PickerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.crashlytics.android.Crashlytics;
import com.dylanvann.fastimage.FastImageViewPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.remobile.toast.RCTToastPackage;
import com.rnim.rn.audio.ReactNativeAudioPackage;
import com.smixx.fabric.FabricPackage;
import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
@ -30,7 +32,6 @@ import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.actionsheet.ActionSheetPackage;
import io.fabric.sdk.android.Fabric;
import io.realm.react.RealmReactPackage;
import com.swmansion.rnscreens.RNScreensPackage;
@ -52,6 +53,11 @@ public class MainApplication extends Application implements ReactApplication, IN
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNFirebasePackage(),
new RNFirebaseCrashlyticsPackage(),
new RNFirebaseAnalyticsPackage(),
new RNFirebasePerformancePackage(),
new RNCWebViewPackage(),
new OrientationPackage(),
new SplashScreenReactPackage(),
new RNGestureHandlerPackage(),
@ -63,11 +69,9 @@ public class MainApplication extends Application implements ReactApplication, IN
new RNFetchBlobPackage(),
new RealmReactPackage(),
new ReactVideoPackage(),
new RCTToastPackage(),
new ReactNativeAudioPackage(),
new KeyboardInputPackage(MainApplication.this),
new RocketChatNativePackage(),
new FabricPackage(),
new FastImageViewPackage(),
new RNI18nPackage(),
new RNNotificationsPackage(MainApplication.this)
@ -88,7 +92,6 @@ public class MainApplication extends Application implements ReactApplication, IN
@Override
public void onCreate() {
super.onCreate();
Fabric.with(this, new Crashlytics());
SoLoader.init(this, /* native exopackage */ false);
}

View File

@ -1,14 +1,25 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 21
compileSdkVersion = 28
targetSdkVersion = 28
supportLibVersion = "28.0.0"
}
repositories {
mavenLocal()
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.0'
classpath 'com.google.gms:google-services:4.0.1'
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.25.4'
classpath 'com.google.firebase:firebase-plugins:1.1.5'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -25,23 +36,19 @@ allprojects {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
url "$rootDir/../node_modules/jsc-android/dist"
}
}
}
subprojects { subproject ->
afterEvaluate {
if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
defaultConfig {
targetSdkVersion 28
}
}
}
}
subprojects { subproject ->
afterEvaluate {
if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
defaultConfig {
targetSdkVersion 28
}
}
}
}
}

View File

@ -1,6 +1,4 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
# distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip

View File

@ -1,4 +1,8 @@
rootProject.name = 'RocketChatRN'
include ':react-native-firebase'
project(':react-native-firebase').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-firebase/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
include ':react-native-orientation-locker'
project(':react-native-orientation-locker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation-locker/android')
include ':react-native-splash-screen'
@ -11,8 +15,6 @@ 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 ':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'
@ -21,8 +23,6 @@ include ':react-native-i18n'
project(':react-native-i18n').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-i18n/android')
include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
include ':react-native-fabric'
project(':react-native-fabric').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fabric/android')
include ':react-native-audio'
project(':react-native-audio').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio/android')
include ':reactnativekeyboardinput'

View File

@ -12,8 +12,7 @@ function createRequestTypes(base, types = defaultTypes) {
export const LOGIN = createRequestTypes('LOGIN', [
...defaultTypes,
'SET_SERVICES',
'SET_PREFERENCE',
'SET_SORT_PREFERENCE'
'SET_PREFERENCE'
]);
export const USER = createRequestTypes('USER', ['SET']);
export const ROOMS = createRequestTypes('ROOMS', [
@ -67,3 +66,4 @@ export const LOGOUT = 'LOGOUT'; // logout is always success
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']);
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';

View File

@ -40,13 +40,6 @@ export function setAllSettings(settings) {
};
}
export function setCustomEmojis(emojis) {
return {
type: types.SET_CUSTOM_EMOJIS,
payload: emojis
};
}
export function login() {
return {
type: 'LOGIN'

8
app/actions/markdown.js Normal file
View File

@ -0,0 +1,8 @@
import * as types from './actionsTypes';
export function toggleMarkdown(value) {
return {
type: types.TOGGLE_MARKDOWN,
payload: value
};
}

View File

@ -12,6 +12,7 @@ export const COLOR_SEPARATOR = '#A7A7AA';
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
export const COLOR_BORDER = '#e1e5e8';
export const COLOR_UNREAD = '#e1e5e8';
export const COLOR_TOAST = '#0C0D0F';
export const STATUS_COLORS = {
online: '#2de0a5',
busy: COLOR_DANGER,

View File

@ -58,5 +58,8 @@ export default {
},
Threads_enabled: {
type: null
},
API_Gitlab_URL: {
type: 'valueAsString'
}
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, ViewPropTypes } from 'react-native';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
const Avatar = React.memo(({
@ -48,7 +48,7 @@ const Avatar = React.memo(({
Avatar.propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style,
style: PropTypes.any,
text: PropTypes.string,
avatar: PropTypes.string,
size: PropTypes.number,

View File

@ -1,12 +1,12 @@
import React from 'react';
import { ViewPropTypes, Image } from 'react-native';
import { Image } from 'react-native';
import PropTypes from 'prop-types';
export default class CustomEmoji extends React.Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
emoji: PropTypes.object.isRequired,
style: ViewPropTypes.style
style: PropTypes.any
}
shouldComponentUpdate() {

121
app/containers/FileModal.js Normal file
View File

@ -0,0 +1,121 @@
import React from 'react';
import {
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
} from 'react-native';
import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import ImageViewer from 'react-native-image-zoom-viewer';
import VideoPlayer from 'react-native-video-controls';
import sharedStyles from '../views/Styles';
import { COLOR_WHITE } from '../constants/colors';
import { formatAttachmentUrl } from '../lib/utils';
const styles = StyleSheet.create({
safeArea: {
flex: 1
},
modal: {
margin: 0
},
titleContainer: {
width: '100%',
alignItems: 'center',
marginVertical: 10
},
title: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
...sharedStyles.textSemibold
},
description: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 14,
...sharedStyles.textMedium
},
indicator: {
flex: 1
}
});
const Indicator = React.memo(() => (
<ActivityIndicator style={styles.indicator} />
));
const ModalContent = React.memo(({
attachment, onClose, user, baseUrl
}) => {
if (attachment && attachment.image_url) {
const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
return (
<SafeAreaView style={styles.safeArea}>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{attachment.title}</Text>
{attachment.description ? <Text style={styles.description}>{attachment.description}</Text> : null}
</View>
</TouchableWithoutFeedback>
<ImageViewer
imageUrls={[{ url }]}
onClick={onClose}
backgroundColor='transparent'
enableSwipeDown
onSwipeDown={onClose}
renderIndicator={() => null}
renderImage={props => <FastImage {...props} />}
loadingRender={() => <Indicator />}
/>
</SafeAreaView>
);
}
if (attachment && attachment.video_url) {
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
return (
<SafeAreaView style={styles.safeArea}>
<VideoPlayer
source={{ uri }}
onBack={onClose}
disableVolume
/>
</SafeAreaView>
);
}
return null;
});
const FileModal = React.memo(({
isVisible, onClose, attachment, user, baseUrl
}) => (
<Modal
style={styles.modal}
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
onSwipeComplete={onClose}
swipeDirection={['up', 'left', 'right', 'down']}
>
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} />
</Modal>
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
FileModal.propTypes = {
isVisible: PropTypes.bool,
attachment: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onClose: PropTypes.func
};
FileModal.displayName = 'FileModal';
ModalContent.propTypes = {
attachment: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onClose: PropTypes.func
};
ModalContent.displayName = 'FileModalContent';
export default FileModal;

View File

@ -20,9 +20,9 @@ export const CustomHeaderButtons = React.memo(props => (
/>
));
export const DrawerButton = React.memo(({ navigation, testID }) => (
export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => (
<CustomHeaderButtons left>
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} />
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
</CustomHeaderButtons>
));

View File

@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-action-sheet';
import * as moment from 'moment';
import moment from 'moment';
import {
actionsHide as actionsHideAction,
deleteRequest as deleteRequestAction,
@ -14,10 +13,10 @@ import {
toggleReactionPicker as toggleReactionPickerAction,
toggleStarRequest as toggleStarRequestAction
} from '../actions/messages';
import { showToast } from '../utils/info';
import { vibrate } from '../utils/vibration';
import RocketChat from '../lib/rocketchat';
import I18n from '../i18n';
import log from '../utils/log';
@connect(
state => ({
@ -44,6 +43,7 @@ export default class MessageActions extends React.Component {
actionsHide: PropTypes.func.isRequired,
room: PropTypes.object.isRequired,
actionMessage: PropTypes.object,
toast: PropTypes.element,
// user: PropTypes.object.isRequired,
deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired,
@ -118,6 +118,10 @@ export default class MessageActions extends React.Component {
this.REACTION_INDEX = this.options.length - 1;
}
// Report
this.options.push(I18n.t('Report'));
this.REPORT_INDEX = this.options.length - 1;
// Delete
if (this.allowDelete(props)) {
this.options.push(I18n.t('Delete'));
@ -151,7 +155,7 @@ export default class MessageActions extends React.Component {
getPermalink = async(message) => {
try {
return await RocketChat.getPermalink(message);
return await RocketChat.getPermalinkMessage(message);
} catch (error) {
return null;
}
@ -253,9 +257,9 @@ export default class MessageActions extends React.Component {
}
handleCopy = async() => {
const { actionMessage } = this.props;
const { actionMessage, toast } = this.props;
await Clipboard.setString(actionMessage.msg);
showToast(I18n.t('Copied_to_clipboard'));
toast.show(I18n.t('Copied_to_clipboard'));
}
handleShare = async() => {
@ -272,10 +276,10 @@ export default class MessageActions extends React.Component {
}
handlePermalink = async() => {
const { actionMessage } = this.props;
const { actionMessage, toast } = this.props;
const permalink = await this.getPermalink(actionMessage);
Clipboard.setString(permalink);
showToast(I18n.t('Permalink_copied_to_clipboard'));
toast.show(I18n.t('Permalink_copied_to_clipboard'));
}
handlePin = () => {
@ -298,6 +302,16 @@ export default class MessageActions extends React.Component {
toggleReactionPicker(actionMessage);
}
handleReport = async() => {
const { actionMessage } = this.props;
try {
await RocketChat.reportMessage(actionMessage._id);
Alert.alert(I18n.t('Message_Reported'));
} catch (err) {
log('err_report_message', err);
}
}
handleActionPress = (actionIndex) => {
if (actionIndex) {
switch (actionIndex) {
@ -328,6 +342,9 @@ export default class MessageActions extends React.Component {
case this.REACTION_INDEX:
this.handleReaction();
break;
case this.REPORT_INDEX:
this.handleReport();
break;
case this.DELETE_INDEX:
this.handleDelete();
break;

View File

@ -1,63 +0,0 @@
import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import ActionSheet from 'react-native-action-sheet';
import I18n from '../../i18n';
export default class FilesActions extends PureComponent {
static propTypes = {
hideActions: PropTypes.func.isRequired,
takePhoto: PropTypes.func.isRequired,
chooseFromLibrary: PropTypes.func.isRequired
}
constructor(props) {
super(props);
// Cancel
this.options = [I18n.t('Cancel')];
this.CANCEL_INDEX = 0;
// Photo
this.options.push(I18n.t('Take_a_photo'));
this.PHOTO_INDEX = 1;
// Library
this.options.push(I18n.t('Choose_from_library'));
this.LIBRARY_INDEX = 2;
setTimeout(() => {
this.showActionSheet();
});
}
showActionSheet = () => {
ActionSheet.showActionSheetWithOptions({
options: this.options,
cancelButtonIndex: this.CANCEL_INDEX
}, (actionIndex) => {
this.handleActionPress(actionIndex);
});
}
handleActionPress = (actionIndex) => {
const { takePhoto, chooseFromLibrary, hideActions } = this.props;
switch (actionIndex) {
case this.PHOTO_INDEX:
takePhoto();
break;
case this.LIBRARY_INDEX:
chooseFromLibrary();
break;
default:
break;
}
hideActions();
}
render() {
return (
null
);
}
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CancelEditingButton, ToggleEmojiButton } from './buttons';
const LeftButtons = React.memo(({
showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji
}) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} />;
}
return (
<ToggleEmojiButton
show={showEmojiKeyboard}
open={openEmoji}
close={closeEmoji}
/>
);
});
LeftButtons.propTypes = {
showEmojiKeyboard: PropTypes.bool,
openEmoji: PropTypes.func.isRequired,
closeEmoji: PropTypes.func.isRequired,
editing: PropTypes.bool,
editCancel: PropTypes.func.isRequired
};
export default LeftButtons;

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CancelEditingButton, FileButton } from './buttons';
const LeftButtons = React.memo(({
showFileActions, editing, editCancel
}) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} />;
}
return <FileButton onPress={showFileActions} />;
});
LeftButtons.propTypes = {
showFileActions: PropTypes.func.isRequired,
editing: PropTypes.bool,
editCancel: PropTypes.func.isRequired
};
export default LeftButtons;

View File

@ -5,6 +5,7 @@ import moment from 'moment';
import { connect } from 'react-redux';
import Markdown from '../message/Markdown';
import { getCustomEmoji } from '../message/utils';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import {
@ -49,7 +50,6 @@ const styles = StyleSheet.create({
@connect(state => ({
Message_TimeFormat: state.settings.Message_TimeFormat,
customEmojis: state.customEmojis,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class ReplyPreview extends Component {
@ -57,7 +57,6 @@ export default class ReplyPreview extends Component {
message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired,
customEmojis: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired
}
@ -73,7 +72,7 @@ export default class ReplyPreview extends Component {
render() {
const {
message, Message_TimeFormat, customEmojis, baseUrl, username
message, Message_TimeFormat, baseUrl, username
} = this.props;
const time = moment(message.ts).format(Message_TimeFormat);
return (
@ -83,7 +82,7 @@ export default class ReplyPreview extends Component {
<Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text>
</View>
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} />
</View>
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
</View>

View File

@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SendButton, AudioButton, FileButton } from './buttons';
const RightButtons = React.memo(({
showSend, submit, recordAudioMessage, showFileActions
}) => {
if (showSend) {
return <SendButton onPress={submit} />;
}
return (
<React.Fragment>
<AudioButton onPress={recordAudioMessage} />
<FileButton onPress={showFileActions} />
</React.Fragment>
);
});
RightButtons.propTypes = {
showSend: PropTypes.bool,
submit: PropTypes.func.isRequired,
recordAudioMessage: PropTypes.func.isRequired,
showFileActions: PropTypes.func.isRequired
};
export default RightButtons;

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SendButton, AudioButton } from './buttons';
const RightButtons = React.memo(({
showSend, submit, recordAudioMessage
}) => {
if (showSend) {
return <SendButton onPress={submit} />;
}
return <AudioButton onPress={recordAudioMessage} />;
});
RightButtons.propTypes = {
showSend: PropTypes.bool,
submit: PropTypes.func.isRequired,
recordAudioMessage: PropTypes.func.isRequired
};
export default RightButtons;

View File

@ -179,6 +179,7 @@ export default class UploadModal extends Component {
animationOut='fadeOut'
useNativeDriver
hideModalContentWhileAnimating
avoidKeyboard
>
<View style={[styles.container, { width: width - 32 }]}>
<View style={styles.titleContainer}>

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import BaseButton from './BaseButton';
const AudioButton = React.memo(({ onPress }) => (
<BaseButton
onPress={onPress}
testID='messagebox-send-audio'
accessibilityLabel='Send_audio_message'
icon='mic'
/>
));
AudioButton.propTypes = {
onPress: PropTypes.func.isRequired
};
export default AudioButton;

View File

@ -0,0 +1,31 @@
import React from 'react';
import { BorderlessButton } from 'react-native-gesture-handler';
import PropTypes from 'prop-types';
import { COLOR_PRIMARY } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
import styles from '../styles';
import I18n from '../../../i18n';
const BaseButton = React.memo(({
onPress, testID, accessibilityLabel, icon
}) => (
<BorderlessButton
onPress={onPress}
style={styles.actionButton}
testID={testID}
accessibilityLabel={I18n.t(accessibilityLabel)}
accessibilityTraits='button'
>
<CustomIcon name={icon} size={23} color={COLOR_PRIMARY} />
</BorderlessButton>
));
BaseButton.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired,
accessibilityLabel: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired
};
export default BaseButton;

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import BaseButton from './BaseButton';
const CancelEditingButton = React.memo(({ onPress }) => (
<BaseButton
onPress={onPress}
testID='messagebox-cancel-editing'
accessibilityLabel='Cancel_editing'
icon='cross'
/>
));
CancelEditingButton.propTypes = {
onPress: PropTypes.func.isRequired
};
export default CancelEditingButton;

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import BaseButton from './BaseButton';
const FileButton = React.memo(({ onPress }) => (
<BaseButton
onPress={onPress}
testID='messagebox-actions'
accessibilityLabel='Message_actions'
icon='plus'
/>
));
FileButton.propTypes = {
onPress: PropTypes.func.isRequired
};
export default FileButton;

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import BaseButton from './BaseButton';
const SendButton = React.memo(({ onPress }) => (
<BaseButton
onPress={onPress}
testID='messagebox-send-message'
accessibilityLabel='Send_message'
icon='send1'
/>
));
SendButton.propTypes = {
onPress: PropTypes.func.isRequired
};
export default SendButton;

View File

@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import BaseButton from './BaseButton';
const ToggleEmojiButton = React.memo(({ show, open, close }) => {
if (show) {
return (
<BaseButton
onPress={close}
testID='messagebox-close-emoji'
accessibilityLabel='Close_emoji_selector'
icon='keyboard'
/>
);
}
return (
<BaseButton
onPress={open}
testID='messagebox-open-emoji'
accessibilityLabel='Open_emoji_selector'
icon='emoji'
/>
);
});
ToggleEmojiButton.propTypes = {
show: PropTypes.bool,
open: PropTypes.func.isRequired,
close: PropTypes.func.isRequired
};
export default ToggleEmojiButton;

View File

@ -0,0 +1,13 @@
import CancelEditingButton from './CancelEditingButton';
import ToggleEmojiButton from './ToggleEmojiButton';
import SendButton from './SendButton';
import AudioButton from './AudioButton';
import FileButton from './FileButton';
export {
CancelEditingButton,
ToggleEmojiButton,
SendButton,
AudioButton,
FileButton
};

View File

@ -7,8 +7,8 @@ import { connect } from 'react-redux';
import { emojify } from 'react-emojione';
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import ImagePicker from 'react-native-image-crop-picker';
import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import ActionSheet from 'react-native-action-sheet';
import { userTyping as userTypingAction } from '../../actions/room';
import {
@ -23,15 +23,15 @@ import Avatar from '../Avatar';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { emojis } from '../../emojis';
import Recording from './Recording';
import FilesActions from './FilesActions';
import UploadModal from './UploadModal';
import './EmojiKeyboard';
import log from '../../utils/log';
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';
import { COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons';
import { isAndroid } from '../../utils/deviceInfo';
const MENTIONS_TRACKING_TYPE_USERS = '@';
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
@ -48,6 +48,17 @@ const imagePickerConfig = {
cropperCancelText: I18n.t('Cancel')
};
const fileOptions = [I18n.t('Cancel')];
const FILE_CANCEL_INDEX = 0;
// Photo
fileOptions.push(I18n.t('Take_a_photo'));
const FILE_PHOTO_INDEX = 1;
// Library
fileOptions.push(I18n.t('Choose_from_library'));
const FILE_LIBRARY_INDEX = 2;
class MessageBox extends Component {
static propTypes = {
rid: PropTypes.string.isRequired,
@ -77,7 +88,6 @@ class MessageBox extends Component {
this.state = {
mentions: [],
showEmojiKeyboard: false,
showFilesAction: false,
showSend: false,
recording: false,
trackingType: '',
@ -111,6 +121,10 @@ class MessageBox extends Component {
this.setInput(msg);
this.setShowSend(true);
}
if (isAndroid) {
require('./EmojiKeyboard');
}
}
componentWillReceiveProps(nextProps) {
@ -133,7 +147,7 @@ class MessageBox extends Component {
shouldComponentUpdate(nextProps, nextState) {
const {
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
showEmojiKeyboard, showSend, recording, mentions, file
} = this.state;
const {
roomType, replying, editing, isFocused
@ -153,9 +167,6 @@ class MessageBox extends Component {
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true;
}
if (nextState.showFilesAction !== showFilesAction) {
return true;
}
if (nextState.showSend !== showSend) {
return true;
}
@ -171,32 +182,25 @@ class MessageBox extends Component {
return false;
}
onChangeText = (text) => {
onChangeText = debounce((text) => {
const isTextEmpty = text.length === 0;
this.setShowSend(!isTextEmpty);
this.handleTyping(!isTextEmpty);
this.debouncedOnChangeText(text);
}
// eslint-disable-next-line react/sort-comp
debouncedOnChangeText = debounce((text) => {
this.setInput(text);
if (this.component) {
requestAnimationFrame(() => {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
const lastNativeText = this.component._lastNativeText;
const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp);
if (!result) {
return this.stopTrackingMention();
}
const [, lastChar, name] = result;
this.identifyMentionKeyword(name, lastChar);
});
if (!isTextEmpty) {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
const lastNativeText = this.component._lastNativeText;
const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp);
if (!result) {
return this.stopTrackingMention();
}
const [, lastChar, name] = result;
this.identifyMentionKeyword(name, lastChar);
}
}, 100);
}, 100)
onKeyboardResigned = () => {
this.closeEmoji();
@ -239,109 +243,9 @@ class MessageBox extends Component {
this.setShowSend(true);
}
get leftButtons() {
const { showEmojiKeyboard } = this.state;
const { editing } = this.props;
if (editing) {
return (
<BorderlessButton
onPress={this.editCancel}
accessibilityLabel={I18n.t('Cancel_editing')}
accessibilityTraits='button'
style={styles.actionButton}
testID='messagebox-cancel-editing'
>
<CustomIcon
size={22}
color={COLOR_PRIMARY}
name='cross'
/>
</BorderlessButton>
);
}
return !showEmojiKeyboard
? (
<BorderlessButton
onPress={this.openEmoji}
accessibilityLabel={I18n.t('Open_emoji_selector')}
accessibilityTraits='button'
style={styles.actionButton}
testID='messagebox-open-emoji'
>
<CustomIcon
size={22}
color={COLOR_PRIMARY}
name='emoji'
/>
</BorderlessButton>
)
: (
<BorderlessButton
onPress={this.closeEmoji}
accessibilityLabel={I18n.t('Close_emoji_selector')}
accessibilityTraits='button'
style={styles.actionButton}
testID='messagebox-close-emoji'
>
<CustomIcon
size={22}
color={COLOR_PRIMARY}
name='keyboard'
/>
</BorderlessButton>
);
}
get rightButtons() {
const { showSend } = this.state;
const icons = [];
if (showSend) {
icons.push(
<BorderlessButton
key='send-message'
onPress={this.submit}
style={styles.actionButton}
testID='messagebox-send-message'
accessibilityLabel={I18n.t('Send message')}
accessibilityTraits='button'
>
<CustomIcon name='send1' size={23} color={COLOR_PRIMARY} />
</BorderlessButton>
);
return icons;
}
icons.push(
<BorderlessButton
key='audio-message'
onPress={this.recordAudioMessage}
style={styles.actionButton}
testID='messagebox-send-audio'
accessibilityLabel={I18n.t('Send audio message')}
accessibilityTraits='button'
>
<CustomIcon name='mic' size={23} color={COLOR_PRIMARY} />
</BorderlessButton>
);
icons.push(
<BorderlessButton
key='file-message'
onPress={this.toggleFilesActions}
style={styles.actionButton}
testID='messagebox-actions'
accessibilityLabel={I18n.t('Message actions')}
accessibilityTraits='button'
>
<CustomIcon name='plus' size={23} color={COLOR_PRIMARY} />
</BorderlessButton>
);
return icons;
}
getPermalink = async(message) => {
try {
return await RocketChat.getPermalink(message);
return await RocketChat.getPermalinkMessage(message);
} catch (error) {
return null;
}
@ -386,7 +290,7 @@ class MessageBox extends Component {
try {
database.create('users', user, true);
} catch (e) {
log('create users', e);
log('err_create_users', e);
}
});
});
@ -495,10 +399,6 @@ class MessageBox extends Component {
this.setShowSend(false);
}
toggleFilesActions = () => {
this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction }));
}
sendImageMessage = async(file) => {
const { rid, tmid } = this.props;
@ -514,7 +414,7 @@ class MessageBox extends Component {
try {
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
} catch (e) {
log('sendImageMessage', e);
log('err_send_image', e);
}
}
@ -523,7 +423,7 @@ class MessageBox extends Component {
const image = await ImagePicker.openCamera(imagePickerConfig);
this.showUploadModal(image);
} catch (e) {
log('takePhoto', e);
log('err_take_photo', e);
}
}
@ -532,7 +432,7 @@ class MessageBox extends Component {
const image = await ImagePicker.openPicker(imagePickerConfig);
this.showUploadModal(image);
} catch (e) {
log('chooseFromLibrary', e);
log('err_choose_from_library', e);
}
}
@ -540,6 +440,28 @@ class MessageBox extends Component {
this.setState({ file: { ...file, isVisible: true } });
}
showFileActions = () => {
ActionSheet.showActionSheetWithOptions({
options: fileOptions,
cancelButtonIndex: FILE_CANCEL_INDEX
}, (actionIndex) => {
this.handleFileActionPress(actionIndex);
});
}
handleFileActionPress = (actionIndex) => {
switch (actionIndex) {
case FILE_PHOTO_INDEX:
this.takePhoto();
break;
case FILE_LIBRARY_INDEX:
this.chooseFromLibrary();
break;
default:
break;
}
}
editCancel = () => {
const { editCancel } = this.props;
editCancel();
@ -570,7 +492,7 @@ class MessageBox extends Component {
if (e && e.error === 'error-file-too-large') {
return Alert.alert(I18n.t(e.error));
}
log('finishAudioMessage', e);
log('err_finish_audio_message', e);
}
}
}
@ -585,6 +507,7 @@ class MessageBox extends Component {
} = this.props;
const message = this.text;
this.clearInput();
this.closeEmoji();
this.stopTrackingMention();
this.handleTyping(false);
@ -629,7 +552,6 @@ class MessageBox extends Component {
} else {
onSubmit(message);
}
this.clearInput();
}
updateMentions = (keyword, type) => {
@ -713,23 +635,27 @@ class MessageBox extends Component {
testID={`mention-item-${ trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`}
>
{trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
? [
this.renderMentionEmoji(item),
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
]
: [
<Avatar
key='mention-item-avatar'
style={{ margin: 8 }}
text={item.username || item.name}
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>,
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
]
? (
<React.Fragment>
{this.renderMentionEmoji(item)}
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
</React.Fragment>
)
: (
<React.Fragment>
<Avatar
key='mention-item-avatar'
style={{ margin: 8 }}
text={item.username || item.name}
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
</React.Fragment>
)
}
</TouchableOpacity>
);
@ -741,7 +667,7 @@ class MessageBox extends Component {
return null;
}
return (
<View key='messagebox-container' testID='messagebox-container'>
<View testID='messagebox-container'>
<FlatList
style={styles.mentionList}
data={mentions}
@ -763,39 +689,30 @@ class MessageBox extends Component {
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} username={user.username} />;
};
renderFilesActions = () => {
const { showFilesAction } = this.state;
if (!showFilesAction) {
return null;
}
return (
<FilesActions
key='files-actions'
hideActions={this.toggleFilesActions}
takePhoto={this.takePhoto}
chooseFromLibrary={this.chooseFromLibrary}
/>
);
}
renderContent = () => {
const { recording } = this.state;
const { recording, showEmojiKeyboard, showSend } = this.state;
const { editing } = this.props;
if (recording) {
return (<Recording onFinish={this.finishAudioMessage} />);
}
return (
[
this.renderMentions(),
<React.Fragment>
{this.renderMentions()}
<View style={styles.composer} key='messagebox'>
{this.renderReplyPreview()}
<View
style={[styles.textArea, editing && styles.editing]}
testID='messagebox'
>
{this.leftButtons}
<LeftButtons
showEmojiKeyboard={showEmojiKeyboard}
editing={editing}
showFileActions={this.showFileActions}
editCancel={this.editCancel}
openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji}
/>
<TextInput
ref={component => this.component = component}
style={styles.textBoxInput}
@ -810,19 +727,23 @@ class MessageBox extends Component {
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
testID='messagebox-input'
/>
{this.rightButtons}
<RightButtons
showSend={showSend}
submit={this.submit}
recordAudioMessage={this.recordAudioMessage}
showFileActions={this.showFileActions}
/>
</View>
</View>
]
</React.Fragment>
);
}
render() {
const { showEmojiKeyboard, file } = this.state;
return (
[
<React.Fragment>
<KeyboardAccessoryView
key='input'
renderContent={this.renderContent}
kbInputRef={this.component}
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
@ -832,16 +753,14 @@ class MessageBox extends Component {
// revealKeyboardInteractive
requiresSameParentToManageScrollView
addBottomView
/>,
this.renderFilesActions(),
/>
<UploadModal
key='upload-modal'
isVisible={(file && file.isVisible)}
file={file}
close={() => this.setState({ file: {} })}
submit={this.sendImageMessage}
/>
]
</React.Fragment>
);
}
}

View File

@ -0,0 +1,153 @@
import React from 'react';
import {
View, Text, FlatList, StyleSheet, SafeAreaView
} from 'react-native';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import Touchable from 'react-native-platform-touchable';
import Emoji from './message/Emoji';
import { getCustomEmoji } from './message/utils';
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: {
alignItems: 'center',
paddingVertical: 10
},
title: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
...sharedStyles.textSemibold
},
reactCount: {
color: COLOR_WHITE,
fontSize: 13,
...sharedStyles.textRegular
},
peopleReacted: {
color: COLOR_WHITE,
fontSize: 14,
...sharedStyles.textMedium
},
peopleItemContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
emojiContainer: {
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center'
},
itemContainer: {
height: 50,
flexDirection: 'row'
},
listContainer: {
flex: 1
},
closeButton: {
position: 'absolute',
left: 0,
top: 10,
color: COLOR_WHITE
}
});
const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 };
const Item = React.memo(({ item, user, baseUrl }) => {
const count = item.usernames.length;
let usernames = item.usernames.slice(0, 3)
.map(username => (username === user.username ? I18n.t('you') : username)).join(', ');
if (count > 3) {
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
} else {
usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
}
return (
<View style={styles.itemContainer}>
<View style={styles.emojiContainer}>
<Emoji
content={item.emoji}
standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
/>
</View>
<View style={styles.peopleItemContainer}>
<Text style={styles.reactCount}>
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
</Text>
<Text style={styles.peopleReacted}>{ usernames }</Text>
</View>
</View>
);
});
const ModalContent = React.memo(({ message, onClose, ...props }) => {
if (message && message.reactions) {
return (
<SafeAreaView style={{ flex: 1 }}>
<Touchable onPress={onClose}>
<View style={styles.titleContainer}>
<CustomIcon
style={styles.closeButton}
name='cross'
size={20}
/>
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
</View>
</Touchable>
<FlatList
style={styles.listContainer}
data={message.reactions}
renderItem={({ item }) => <Item item={item} {...props} />}
keyExtractor={item => item.emoji}
/>
</SafeAreaView>
);
}
return null;
});
const ReactionsModal = React.memo(({ isVisible, onClose, ...props }) => (
<Modal
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
backdropOpacity={0.8}
onSwipeComplete={onClose}
swipeDirection={['up', 'left', 'right', 'down']}
>
<ModalContent onClose={onClose} {...props} />
</Modal>
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
ReactionsModal.propTypes = {
isVisible: PropTypes.bool,
onClose: PropTypes.func
};
ReactionsModal.displayName = 'ReactionsModal';
ModalContent.propTypes = {
message: PropTypes.object,
onClose: PropTypes.func
};
ModalContent.displayName = 'ReactionsModalContent';
Item.propTypes = {
item: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string
};
Item.displayName = 'ReactionsModalItem';
export default ReactionsModal;

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, ViewPropTypes } from 'react-native';
import { View } from 'react-native';
import { STATUS_COLORS } from '../../constants/colors';
const Status = React.memo(({ status, size, style }) => (
@ -20,7 +20,7 @@ const Status = React.memo(({ status, size, style }) => (
Status.propTypes = {
status: PropTypes.string,
size: PropTypes.number,
style: ViewPropTypes.style
style: PropTypes.any
};
Status.defaultProps = {
status: 'offline',

View File

@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { ViewPropTypes } from 'react-native';
import Status from './Status';
import database, { safeAddListener } from '../../lib/realm';
@ -12,7 +11,7 @@ import database, { safeAddListener } from '../../lib/realm';
export default class StatusContainer extends React.PureComponent {
static propTypes = {
id: PropTypes.string,
style: ViewPropTypes.style,
style: PropTypes.any,
size: PropTypes.number,
offline: PropTypes.bool
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import {
View, StyleSheet, Text, TextInput, ViewPropTypes
View, StyleSheet, Text, TextInput
} from 'react-native';
import PropTypes from 'prop-types';
import { BorderlessButton } from 'react-native-gesture-handler';
@ -73,7 +73,7 @@ export default class RCTextInput extends React.PureComponent {
label: PropTypes.string,
error: PropTypes.object,
secureTextEntry: PropTypes.bool,
containerStyle: ViewPropTypes.style,
containerStyle: PropTypes.any,
inputStyle: PropTypes.object,
inputRef: PropTypes.func,
testID: PropTypes.string,

View File

@ -0,0 +1,44 @@
import React from 'react';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
import Image from './Image';
import Audio from './Audio';
import Video from './Video';
import Reply from './Reply';
const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji
}) => {
if (!attachments || attachments.length === 0) {
return null;
}
return attachments.map((file, index) => {
if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
}
// eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
});
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments));
Attachments.propTypes = {
attachments: PropTypes.array,
timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
useMarkdown: PropTypes.bool,
onOpenFileModal: PropTypes.func,
getCustomEmoji: PropTypes.func
};
Attachments.displayName = 'MessageAttachments';
export default Attachments;

View File

@ -56,20 +56,40 @@ const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
const BUTTON_HIT_SLOP = {
top: 12, right: 12, bottom: 12, left: 12
};
const sliderAnimationConfig = {
duration: 250,
easing: Easing.linear,
delay: 0
};
const Button = React.memo(({ paused, onPress }) => (
<Touchable
style={styles.playPauseButton}
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name={paused ? 'play' : 'pause'} size={36} style={styles.playPauseImage} />
</Touchable>
));
Button.propTypes = {
paused: PropTypes.bool,
onPress: PropTypes.func
};
Button.displayName = 'MessageAudioButton';
export default class Audio extends React.Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
customEmojis: PropTypes.object.isRequired
useMarkdown: PropTypes.bool,
getCustomEmoji: PropTypes.func
}
constructor(props) {
super(props);
this.onLoad = this.onLoad.bind(this);
this.onProgress = this.onProgress.bind(this);
this.onEnd = this.onEnd.bind(this);
const { baseUrl, file, user } = props;
this.state = {
currentTime: 0,
@ -120,22 +140,26 @@ export default class Audio extends React.Component {
});
}
getDuration = () => {
get duration() {
const { duration } = this.state;
return formatTime(duration);
}
setRef = ref => this.player = ref;
togglePlayPause = () => {
const { paused } = this.state;
this.setState({ paused: !paused });
}
onValueChange = value => this.setState({ currentTime: value });
render() {
const {
uri, paused, currentTime, duration
} = this.state;
const {
user, baseUrl, customEmojis, file
user, baseUrl, file, getCustomEmoji, useMarkdown
} = this.props;
const { description } = file;
@ -144,12 +168,10 @@ export default class Audio extends React.Component {
}
return (
[
<View key='audio' style={styles.audioContainer}>
<React.Fragment>
<View style={styles.audioContainer}>
<Video
ref={(ref) => {
this.player = ref;
}}
ref={this.setRef}
source={{ uri }}
onLoad={this.onLoad}
onProgress={this.onProgress}
@ -157,39 +179,24 @@ export default class Audio extends React.Component {
paused={paused}
repeat={false}
/>
<Touchable
style={styles.playPauseButton}
onPress={this.togglePlayPause}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
{
paused
? <CustomIcon name='play' size={36} style={styles.playPauseImage} />
: <CustomIcon name='pause' size={36} style={styles.playPauseImage} />
}
</Touchable>
<Button paused={paused} onPress={this.togglePlayPause} />
<Slider
style={styles.slider}
value={currentTime}
maximumValue={duration}
minimumValue={0}
animateTransitions
animationConfig={{
duration: 250,
easing: Easing.linear,
delay: 0
}}
animationConfig={sliderAnimationConfig}
thumbTintColor={COLOR_PRIMARY}
minimumTrackTintColor={COLOR_PRIMARY}
onValueChange={value => this.setState({ currentTime: value })}
onValueChange={this.onValueChange}
thumbStyle={styles.thumbStyle}
trackStyle={styles.trackStyle}
/>
<Text style={styles.duration}>{this.getDuration()}</Text>
</View>,
<Markdown key='description' msg={description} baseUrl={baseUrl} customEmojis={customEmojis} username={user.username} />
]
<Text style={styles.duration}>{this.duration}</Text>
</View>
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
</React.Fragment>
);
}
}

View File

@ -0,0 +1,43 @@
import React from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils';
import I18n from '../../i18n';
const Broadcast = React.memo(({
author, user, broadcast, replyBroadcast
}) => {
const isOwn = author._id === user.id;
if (broadcast && !isOwn) {
return (
<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;
}, () => true);
Broadcast.propTypes = {
author: PropTypes.object,
user: PropTypes.object,
broadcast: PropTypes.bool,
replyBroadcast: PropTypes.func
};
Broadcast.displayName = 'MessageBroadcast';
export default Broadcast;

View File

@ -0,0 +1,48 @@
import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import I18n from '../../i18n';
import styles from './styles';
import Markdown from './Markdown';
import { getInfoMessage } from './utils';
const Content = React.memo((props) => {
if (props.isInfo) {
return <Text style={styles.textInfo}>{getInfoMessage({ ...props })}</Text>;
}
if (props.tmid && !props.msg) {
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
}
return (
<Markdown
msg={props.msg}
baseUrl={props.baseUrl}
username={props.user.username}
isEdited={props.isEdited}
mentions={props.mentions}
channels={props.channels}
numberOfLines={props.tmid ? 1 : 0}
getCustomEmoji={props.getCustomEmoji}
useMarkdown={props.useMarkdown}
/>
);
}, (prevProps, nextProps) => prevProps.msg === nextProps.msg);
Content.propTypes = {
isInfo: PropTypes.bool,
isEdited: PropTypes.bool,
useMarkdown: PropTypes.bool,
tmid: PropTypes.string,
msg: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
getCustomEmoji: PropTypes.func
};
Content.displayName = 'MessageContent';
export default Content;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
import styles from './styles';
import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons';
import { DISCUSSION } from './constants';
const Discussion = React.memo(({
msg, dcount, dlm, onDiscussionPress
}) => {
const time = formatLastMessage(dlm);
const buttonText = 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>
);
}, (prevProps, nextProps) => {
if (prevProps.msg !== nextProps.msg) {
return false;
}
if (prevProps.dcount !== nextProps.dcount) {
return false;
}
if (prevProps.dlm !== nextProps.dlm) {
return false;
}
return true;
});
Discussion.propTypes = {
msg: PropTypes.string,
dcount: PropTypes.number,
dlm: PropTypes.string,
onDiscussionPress: PropTypes.func
};
Discussion.displayName = 'MessageDiscussion';
export default Discussion;

View File

@ -1,31 +1,28 @@
import React from 'react';
import { Text, ViewPropTypes } from 'react-native';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
export default class Emoji extends React.PureComponent {
static propTypes = {
content: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
standardEmojiStyle: Text.propTypes.style,
customEmojiStyle: ViewPropTypes.style,
customEmojis: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
])
const Emoji = React.memo(({
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
}) => {
const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent);
if (emoji) {
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
}
return <Text style={standardEmojiStyle}>{ emojify(content, { output: 'unicode' }) }</Text>;
}, () => true);
render() {
const {
content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl
} = this.props;
const parsedContent = content.replace(/^:|:$/g, '');
const emojiExtension = customEmojis[parsedContent];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content: parsedContent };
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
}
return <Text style={standardEmojiStyle}>{ emojify(`${ content }`, { output: 'unicode' }) }</Text>;
}
}
Emoji.propTypes = {
content: PropTypes.string,
standardEmojiStyle: PropTypes.object,
customEmojiStyle: PropTypes.object,
baseUrl: PropTypes.string,
getCustomEmoji: PropTypes.func
};
Emoji.displayName = 'MessageEmoji';
export default Emoji;

View File

@ -1,95 +1,79 @@
import React, { Component } from 'react';
import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils';
export default class extends Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
customEmojis: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
])
const Button = React.memo(({ children, onPress }) => (
<Touchable
onPress={onPress}
style={styles.imageContainer}
background={Touchable.Ripple('#fff')}
>
{children}
</Touchable>
));
const Image = React.memo(({ img }) => (
<FastImage
style={styles.image}
source={{ uri: encodeURI(img) }}
resizeMode={FastImage.resizeMode.cover}
/>
));
const ImageContainer = React.memo(({
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
}) => {
const img = formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) {
return null;
}
state = { modalVisible: false, isPressed: false };
shouldComponentUpdate(nextProps, nextState) {
const { modalVisible, isPressed } = this.state;
const { file } = this.props;
if (nextState.modalVisible !== modalVisible) {
return true;
}
if (nextState.isPressed !== isPressed) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
return false;
}
onPressButton = () => {
this.setState({
modalVisible: true
});
}
getDescription() {
const {
file, customEmojis, baseUrl, user
} = this.props;
if (file.description) {
return <Markdown msg={file.description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />;
}
}
isPressed = (state) => {
this.setState({ isPressed: state });
}
render() {
const { modalVisible, isPressed } = this.state;
const { baseUrl, file, user } = this.props;
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;
}
const onPress = () => onOpenFileModal(file);
if (file.description) {
return (
[
<Touchable
key='image'
onPress={this.onPressButton}
style={styles.imageContainer}
background={Touchable.Ripple('#fff')}
>
<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}
description={file.description}
image={img}
isVisible={modalVisible}
onClose={() => this.setState({ modalVisible: false })}
/>
]
<Button onPress={onPress}>
<View>
<Image img={img} />
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
</View>
</Button>
);
}
}
return (
<Button onPress={onPress}>
<Image img={img} />
</Button>
);
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file));
ImageContainer.propTypes = {
file: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
onOpenFileModal: PropTypes.func,
getCustomEmoji: PropTypes.func
};
ImageContainer.displayName = 'MessageImageContainer';
Image.propTypes = {
img: PropTypes.string
};
ImageContainer.displayName = 'MessageImage';
Button.propTypes = {
children: PropTypes.node,
onPress: PropTypes.func
};
ImageContainer.displayName = 'MessageButton';
export default ImageContainer;

View File

@ -4,9 +4,15 @@ import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
import MarkdownFlowdock from 'markdown-it-flowdock';
import styles from './styles';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
import I18n from '../../i18n';
const EmojiPlugin = new PluginContainer(MarkdownEmojiPlugin);
const MentionsPlugin = new PluginContainer(MarkdownFlowdock);
const plugins = [EmojiPlugin, MentionsPlugin];
// Support <http://link|Text>
const formatText = text => text.replace(
@ -15,7 +21,7 @@ const formatText = text => text.replace(
);
const Markdown = React.memo(({
msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true
}) => {
if (!msg) {
return null;
@ -28,14 +34,18 @@ const Markdown = React.memo(({
if (numberOfLines > 0) {
m = m.replace(/[\n]+/g, '\n').trim();
}
if (!useMarkdown) {
return <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
}
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}
{isEdited ? <Text style={styles.edited}> ({I18n.t('edited')})</Text> : null}
</Text>
),
mention: (node) => {
@ -52,23 +62,31 @@ const Markdown = React.memo(({
...styles.mentionLoggedUser
};
}
return (
<Text style={mentionStyle} key={key}>
&nbsp;{content}&nbsp;
</Text>
);
if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
return (
<Text style={mentionStyle} key={key}>
&nbsp;{content}&nbsp;
</Text>
);
}
return `@${ content }`;
},
hashtag: (node) => {
const { content, key } = node;
if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) {
return (
<Text key={key} style={styles.mention}>
&nbsp;#{content}&nbsp;
</Text>
);
}
return `#${ content }`;
},
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 };
const emoji = getCustomEmoji && getCustomEmoji(content);
if (emoji) {
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
}
return <Text key={node.key}>:{content}:</Text>;
@ -90,10 +108,7 @@ const Markdown = React.memo(({
link: styles.link,
...style
}}
plugins={[
new PluginContainer(MarkdownFlowdock),
new PluginContainer(MarkdownEmojiPlugin)
]}
plugins={plugins}
>{m}
</MarkdownRenderer>
);
@ -101,13 +116,17 @@ const Markdown = React.memo(({
Markdown.propTypes = {
msg: PropTypes.string,
username: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
username: PropTypes.string,
baseUrl: PropTypes.string,
style: PropTypes.any,
rules: PropTypes.object,
edited: PropTypes.bool,
numberOfLines: PropTypes.number
isEdited: PropTypes.bool,
numberOfLines: PropTypes.number,
useMarkdown: PropTypes.bool,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
getCustomEmoji: PropTypes.func
};
Markdown.displayName = 'MessageMarkdown';
export default Markdown;

View File

@ -1,609 +1,129 @@
import React, { PureComponent } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
View, Text, ViewPropTypes, TouchableWithoutFeedback
} from 'react-native';
import moment from 'moment';
import { KeyboardUtils } from 'react-native-keyboard-input';
import { View } from 'react-native';
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';
import Avatar from '../Avatar';
import Audio from './Audio';
import Video from './Video';
import Markdown from './Markdown';
import Url from './Url';
import Reply from './Reply';
import ReactionsModal from './ReactionsModal';
import Emoji from './Emoji';
import MessageError from './MessageError';
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';
import RepliedThread from './RepliedThread';
import MessageAvatar from './MessageAvatar';
import Attachments from './Attachments';
import Urls from './Urls';
import Thread from './Thread';
import Reactions from './Reactions';
import Broadcast from './Broadcast';
import Discussion from './Discussion';
import Content from './Content';
const SYSTEM_MESSAGES = [
'r',
'au',
'ru',
'ul',
'uj',
'ut',
'rm',
'user-muted',
'user-unmuted',
'message_pinned',
'subscription-role-added',
'subscription-role-removed',
'room_changed_description',
'room_changed_announcement',
'room_changed_topic',
'room_changed_privacy',
'message_snippeted',
'thread-created'
];
const getInfoMessage = ({
type, role, msg, author
}) => {
const { username } = author;
if (type === 'rm') {
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') {
return I18n.t('Message_pinned');
} else if (type === 'ul') {
return I18n.t('Has_left_the_channel');
} else if (type === 'ru') {
return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
} else if (type === 'au') {
return I18n.t('User_added_by', { userAdded: msg, userBy: username });
} else if (type === 'user-muted') {
return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
} else if (type === 'user-unmuted') {
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
} else if (type === 'subscription-role-added') {
return `${ msg } was set ${ role } by ${ username }`;
} else if (type === 'subscription-role-removed') {
return `${ msg } is no longer ${ role } by ${ username }`;
} else if (type === 'room_changed_description') {
return I18n.t('Room_changed_description', { description: msg, userBy: username });
} else if (type === 'room_changed_announcement') {
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
} else if (type === 'room_changed_topic') {
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
} else if (type === 'room_changed_privacy') {
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
} else if (type === 'message_snippeted') {
return I18n.t('Created_snippet');
}
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,
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
author: PropTypes.shape({
_id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
name: PropTypes.string
}),
status: PropTypes.any,
reactions: PropTypes.any,
editing: PropTypes.bool,
style: ViewPropTypes.style,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
reactionsModal: PropTypes.bool,
type: PropTypes.string,
header: PropTypes.bool,
isThreadReply: PropTypes.bool,
isThreadSequential: PropTypes.bool,
avatar: PropTypes.string,
alias: PropTypes.string,
ts: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string
]),
edited: PropTypes.bool,
attachments: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]),
urls: PropTypes.oneOfType([
PropTypes.array,
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,
fetchThreadName: PropTypes.func
}
static defaultProps = {
archived: false,
broadcast: false,
attachments: [],
urls: [],
reactions: [],
onLongPress: () => {}
}
onPress = debounce(() => {
KeyboardUtils.dismiss();
const { onThreadPress, tlm, tmid } = this.props;
if ((tlm || tmid) && onThreadPress) {
onThreadPress();
}
}, 300, true)
onLongPress = () => {
const { archived, onLongPress } = this.props;
if (this.isInfoMessage() || this.hasError() || archived) {
return;
}
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);
}
isOwn = () => {
const { author, user } = this.props;
return author._id === user.id;
}
isDeleted() {
const { type } = this.props;
return type === 'rm';
}
isTemp() {
const { status } = this.props;
return status === messagesStatus.TEMP || status === messagesStatus.ERROR;
}
hasError() {
const { status } = this.props;
return status === messagesStatus.ERROR;
}
renderAvatar = (small = false) => {
const {
header, avatar, author, baseUrl, user
} = this.props;
if (header) {
return (
<Avatar
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={small ? 20 : 36}
borderRadius={small ? 2 : 4}
avatar={avatar}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
);
}
return null;
}
renderUsername = () => {
const {
header, timeFormat, author, alias, ts, useRealName
} = this.props;
if (header) {
return (
<User
onPress={this.onPress}
timeFormat={timeFormat}
username={(useRealName && author.name) || author.username}
alias={alias}
ts={ts}
temp={this.isTemp()}
/>
);
}
return null;
}
renderContent() {
if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
}
const {
customEmojis, msg, baseUrl, user, edited, tmid
} = this.props;
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() {
const { attachments, timeFormat } = this.props;
if (attachments.length === 0) {
return null;
}
return attachments.map((file, index) => {
const { user, baseUrl, customEmojis } = this.props;
if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
}
// eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
});
}
renderUrl = () => {
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} user={user} baseUrl={baseUrl} />
));
}
renderError = () => {
if (!this.hasError()) {
return null;
}
const { onErrorPress } = this.props;
return (
<Touchable onPress={onErrorPress} style={styles.errorButton}>
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
</Touchable>
);
}
renderReaction = (reaction) => {
const {
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
} = this.props;
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
return (
<Touchable
onPress={() => onReactionPress(reaction.emoji)}
onLongPress={onReactionLongPress}
key={reaction.emoji}
testID={`message-reaction-${ reaction.emoji }`}
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
>
<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>
);
}
renderReactions() {
const { reactions, toggleReactionPicker } = this.props;
if (reactions.length === 0) {
return null;
}
return (
<View style={styles.reactionsContainer}>
{reactions.map(this.renderReaction)}
<Touchable
onPress={toggleReactionPicker}
key='message-add-reaction'
testID='message-add-reaction'
style={styles.reactionButton}
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
>
<View style={styles.reactionContainer}>
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
</View>
</Touchable>
</View>
);
}
renderBroadcastReply() {
const { broadcast, replyBroadcast } = this.props;
if (broadcast && !this.isOwn()) {
return (
<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');
const MessageInner = React.memo((props) => {
if (props.type === 'discussion-created') {
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>
<User {...props} />
<Discussion {...props} />
</React.Fragment>
);
}
return (
<React.Fragment>
<User {...props} />
<Content {...props} />
<Attachments {...props} />
<Urls {...props} />
<Thread {...props} />
<Reactions {...props} />
<Broadcast {...props} />
</React.Fragment>
);
});
MessageInner.displayName = 'MessageInner';
renderThread = () => {
const {
tcount, tlm, onThreadPress, msg
} = this.props;
if (!tlm) {
return null;
}
const time = this.formatLastMessage(tlm);
const buttonText = this.formatMessageCount(tcount, 'thread');
const Message = React.memo((props) => {
if (props.isThreadReply || props.isThreadSequential || props.isInfo) {
const thread = props.isThreadReply ? <RepliedThread isTemp={props.isTemp} {...props} /> : null;
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 style={[styles.container, props.style, props.isTemp && styles.temp]}>
{thread}
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
<MessageAvatar small {...props} />
<View
style={[
styles.messageContent,
props.isHeader && styles.messageContentWithHeader
]}
>
<Content {...props} />
</View>
</React.Fragment>
);
}
return (
</View>
</View>
);
}
return (
<View style={[styles.container, props.style, props.isTemp && styles.temp]}>
<View style={styles.flex}>
{this.renderAvatar()}
<MessageAvatar {...props} />
<View
style={[
styles.messageContent,
header && styles.messageContentWithHeader,
this.hasError() && header && styles.messageContentWithHeader,
this.hasError() && !header && styles.messageContentWithError,
this.isTemp() && styles.temp
props.isHeader && styles.messageContentWithHeader
]}
>
{this.renderInner()}
<MessageInner {...props} />
</View>
</View>
);
}
render() {
const {
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 });
</View>
);
});
Message.displayName = 'Message';
const MessageTouchable = React.memo((props) => {
if (props.hasError) {
return (
<View style={styles.root}>
{this.renderError()}
<TouchableWithoutFeedback
onLongPress={this.onLongPress}
onPress={this.onPress}
>
<View
style={[styles.container, editing && styles.editing, style]}
accessibilityLabel={accessibilityLabel}
>
{this.renderMessage()}
{reactionsModal
? (
<ReactionsModal
isVisible={reactionsModal}
reactions={reactions}
user={user}
customEmojis={customEmojis}
baseUrl={baseUrl}
close={closeReactions}
/>
)
: null
}
</View>
</TouchableWithoutFeedback>
<MessageError {...props} />
<Message {...props} />
</View>
);
}
}
return (
<Touchable
onLongPress={props.onLongPress}
onPress={props.onPress}
disabled={props.isInfo || props.archived || props.isTemp}
>
<View>
<Message {...props} />
</View>
</Touchable>
);
});
MessageTouchable.displayName = 'MessageTouchable';
MessageTouchable.propTypes = {
hasError: PropTypes.bool,
isInfo: PropTypes.bool,
isTemp: PropTypes.bool,
archived: PropTypes.bool,
onLongPress: PropTypes.func,
onPress: PropTypes.func
};
Message.propTypes = {
isThreadReply: PropTypes.bool,
isThreadSequential: PropTypes.bool,
isInfo: PropTypes.bool,
isTemp: PropTypes.bool,
isHeader: PropTypes.bool,
hasError: PropTypes.bool,
style: PropTypes.any,
onLongPress: PropTypes.func,
onPress: PropTypes.func
};
MessageInner.propTypes = {
type: PropTypes.string
};
export default MessageTouchable;

View File

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import Avatar from '../Avatar';
import styles from './styles';
const MessageAvatar = React.memo(({
isHeader, avatar, author, baseUrl, user, small
}) => {
if (isHeader) {
return (
<Avatar
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={small ? 20 : 36}
borderRadius={small ? 2 : 4}
avatar={avatar}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
);
}
return null;
}, (prevProps, nextProps) => prevProps.isHeader === nextProps.isHeader);
MessageAvatar.propTypes = {
isHeader: PropTypes.bool,
avatar: PropTypes.string,
author: PropTypes.obj,
baseUrl: PropTypes.string,
user: PropTypes.obj,
small: PropTypes.bool
};
MessageAvatar.displayName = 'MessageAvatar';
export default MessageAvatar;

View File

@ -0,0 +1,26 @@
import React from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_DANGER } from '../../constants/colors';
import styles from './styles';
const MessageError = React.memo(({ hasError, onErrorPress }) => {
if (!hasError) {
return null;
}
return (
<Touchable onPress={onErrorPress} style={styles.errorButton}>
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
</Touchable>
);
}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError);
MessageError.propTypes = {
hasError: PropTypes.bool,
onErrorPress: PropTypes.func
};
MessageError.displayName = 'MessageError';
export default MessageError;

View File

@ -1,94 +0,0 @@
import React from 'react';
import {
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet
} from 'react-native';
import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types';
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
},
titleContainer: {
width: '100%',
alignItems: 'center',
marginVertical: 10
},
title: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
...sharedStyles.textSemibold
},
description: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 14,
...sharedStyles.textMedium
},
indicatorContainer: {
alignItems: 'center',
justifyContent: 'center'
}
});
const margin = 40;
@responsive
export default class PhotoModal extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
image: PropTypes.string.isRequired,
isVisible: PropTypes.bool,
onClose: PropTypes.func.isRequired,
window: PropTypes.object
}
render() {
const {
image, isVisible, onClose, title, description, window: { width, height }
} = this.props;
return (
<Modal
isVisible={isVisible}
style={{ alignItems: 'center' }}
onBackdropPress={onClose}
onBackButtonPress={onClose}
animationIn='fadeIn'
animationOut='fadeOut'
>
<View style={{ width: width - margin, height: height - margin }}>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
</View>
</TouchableWithoutFeedback>
<View style={styles.imageWrapper}>
<ImageViewer
imageUrls={[{ url: encodeURI(image) }]}
onClick={onClose}
backgroundColor='transparent'
enableSwipeDown
onSwipeDown={onClose}
renderIndicator={() => {}}
renderImage={props => <FastImage {...props} />}
loadingRender={() => (
<View style={[styles.indicatorContainer, { width, height }]}>
<ActivityIndicator />
</View>
)}
/>
</View>
</View>
</Modal>
);
}
}

View File

@ -0,0 +1,105 @@
import React from 'react';
import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils';
const AddReaction = React.memo(({ toggleReactionPicker }) => (
<Touchable
onPress={toggleReactionPicker}
key='message-add-reaction'
testID='message-add-reaction'
style={styles.reactionButton}
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
>
<View style={styles.reactionContainer}>
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
</View>
</Touchable>
));
const Reaction = React.memo(({
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji
}) => {
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
return (
<Touchable
onPress={() => onReactionPress(reaction.emoji)}
onLongPress={onReactionLongPress}
key={reaction.emoji}
testID={`message-reaction-${ reaction.emoji }`}
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
>
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
<Emoji
content={reaction.emoji}
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
/>
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
</View>
</Touchable>
);
});
const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, toggleReactionPicker, onReactionLongPress, getCustomEmoji
}) => {
if (!reactions || reactions.length === 0) {
return null;
}
return (
<View style={styles.reactionsContainer}>
{reactions.map(reaction => (
<Reaction
key={reaction.emoji}
reaction={reaction}
user={user}
baseUrl={baseUrl}
onReactionLongPress={onReactionLongPress}
onReactionPress={onReactionPress}
getCustomEmoji={getCustomEmoji}
/>
))}
<AddReaction toggleReactionPicker={toggleReactionPicker} />
</View>
);
});
// FIXME: can't compare because it's a Realm object (it may be fixed by JSON.parse(JSON.stringify(reactions)))
Reaction.propTypes = {
reaction: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func
};
Reaction.displayName = 'MessageReaction';
Reactions.propTypes = {
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
toggleReactionPicker: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func
};
Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = {
toggleReactionPicker: PropTypes.func
};
AddReaction.displayName = 'MessageAddReaction';
export default Reactions;

View File

@ -1,140 +0,0 @@
import React from 'react';
import {
View, Text, TouchableWithoutFeedback, FlatList, StyleSheet
} from 'react-native';
import PropTypes from 'prop-types';
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: {
width: '100%',
alignItems: 'center',
paddingVertical: 10
},
title: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
...sharedStyles.textSemibold
},
reactCount: {
color: COLOR_WHITE,
fontSize: 13,
...sharedStyles.textRegular
},
peopleReacted: {
color: COLOR_WHITE,
fontSize: 14,
...sharedStyles.textMedium
},
peopleItemContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
emojiContainer: {
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center'
},
itemContainer: {
height: 50,
flexDirection: 'row'
},
listContainer: {
flex: 1
},
closeButton: {
position: 'absolute',
left: 0,
top: 10,
color: COLOR_WHITE
}
});
const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 };
export default class ReactionsModal extends React.PureComponent {
static propTypes = {
isVisible: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
reactions: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
])
}
renderItem = (item) => {
const { user, customEmojis, baseUrl } = this.props;
const count = item.usernames.length;
let usernames = item.usernames.slice(0, 3)
.map(username => (username.value === user.username ? I18n.t('you') : username.value)).join(', ');
if (count > 3) {
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
} else {
usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
}
return (
<View style={styles.itemContainer}>
<View style={styles.emojiContainer}>
<Emoji
content={item.emoji}
standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle}
customEmojis={customEmojis}
baseUrl={baseUrl}
/>
</View>
<View style={styles.peopleItemContainer}>
<Text style={styles.reactCount}>
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
</Text>
<Text style={styles.peopleReacted}>{ usernames }</Text>
</View>
</View>
);
}
render() {
const {
isVisible, close, reactions
} = this.props;
return (
<Modal
isVisible={isVisible}
onBackdropPress={close}
onBackButtonPress={close}
backdropOpacity={0.9}
>
<TouchableWithoutFeedback onPress={close}>
<View style={styles.titleContainer}>
<CustomIcon
style={styles.closeButton}
name='cross'
size={20}
onPress={close}
/>
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
</View>
</TouchableWithoutFeedback>
<View style={styles.listContainer}>
<FlatList
data={reactions}
renderItem={({ item }) => this.renderItem(item)}
keyExtractor={item => item.emoji}
/>
</View>
</Modal>
);
}
}

View File

@ -0,0 +1,58 @@
import React from 'react';
import { View, Text } from 'react-native';
import removeMarkdown from 'remove-markdown';
import { emojify } from 'react-emojione';
import PropTypes from 'prop-types';
import { CustomIcon } from '../../lib/Icons';
import DisclosureIndicator from '../DisclosureIndicator';
import styles from './styles';
const RepliedThread = React.memo(({
tmid, tmsg, isHeader, isTemp, fetchThreadName
}) => {
if (!tmid || !isHeader || 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>
);
}, (prevProps, nextProps) => {
if (prevProps.tmid !== nextProps.tmid) {
return false;
}
if (prevProps.tmsg !== nextProps.tmsg) {
return false;
}
if (prevProps.isHeader !== nextProps.isHeader) {
return false;
}
if (prevProps.isTemp !== nextProps.isTemp) {
return false;
}
return true;
});
RepliedThread.propTypes = {
tmid: PropTypes.string,
tmsg: PropTypes.string,
isHeader: PropTypes.bool,
isTemp: PropTypes.bool,
fetchThreadName: PropTypes.func
};
RepliedThread.displayName = 'MessageRepliedThread';
export default RepliedThread;

View File

@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
@ -69,98 +70,130 @@ const styles = StyleSheet.create({
}
});
const onPress = (attachment, baseUrl, user) => {
let url = attachment.title_link || attachment.author_link;
if (!url) {
return;
const Title = React.memo(({ attachment, timeFormat }) => {
if (!attachment.author_name) {
return null;
}
if (attachment.type === 'file') {
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
}
openLink(url);
};
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
return (
<View style={styles.authorContainer}>
{attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null}
{time ? <Text style={styles.time}>{ time }</Text> : null}
</View>
);
}, () => true);
const Reply = ({
attachment, timeFormat, baseUrl, customEmojis, user, index
const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, useMarkdown
}) => {
const text = attachment.text || attachment.title;
if (!text) {
return null;
}
return (
<Markdown
msg={text}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
useMarkdown={useMarkdown}
/>
);
}, (prevProps, nextProps) => {
if (prevProps.attachment.text !== nextProps.attachment.text) {
return false;
}
if (prevProps.attachment.title !== nextProps.attachment.title) {
return false;
}
return true;
});
const Fields = React.memo(({ attachment }) => {
if (!attachment.fields) {
return null;
}
return (
<View style={styles.fieldsContainer}>
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={styles.fieldTitle}>{field.title}</Text>
<Text style={styles.fieldValue}>{field.value}</Text>
</View>
))}
</View>
);
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields));
const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown
}) => {
if (!attachment) {
return null;
}
const renderAuthor = () => (
attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null
);
const renderTime = () => {
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
return time ? <Text style={styles.time}>{ time }</Text> : null;
};
const renderTitle = () => {
if (!attachment.author_name) {
return null;
const onPress = () => {
let url = attachment.title_link || attachment.author_link;
if (!url) {
return;
}
return (
<View style={styles.authorContainer}>
{renderAuthor()}
{renderTime()}
</View>
);
};
const renderText = () => {
const text = attachment.text || attachment.title;
if (text) {
return (
<Markdown
msg={text}
customEmojis={customEmojis}
baseUrl={baseUrl}
username={user.username}
/>
);
if (attachment.type === 'file') {
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
}
};
const renderFields = () => {
if (!attachment.fields) {
return null;
}
return (
<View style={styles.fieldsContainer}>
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={styles.fieldTitle}>{field.title}</Text>
<Text style={styles.fieldValue}>{field.value}</Text>
</View>
))}
</View>
);
openLink(url);
};
return (
<Touchable
onPress={() => onPress(attachment, baseUrl, user)}
onPress={onPress}
style={[styles.button, index > 0 && styles.marginTop]}
background={Touchable.Ripple('#fff')}
>
<View style={styles.attachmentContainer}>
{renderTitle()}
{renderText()}
{renderFields()}
<Title attachment={attachment} timeFormat={timeFormat} />
<Description
attachment={attachment}
timeFormat={timeFormat}
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji}
useMarkdown={useMarkdown}
/>
<Fields attachment={attachment} />
</View>
</Touchable>
);
};
}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment));
Reply.propTypes = {
attachment: PropTypes.object.isRequired,
timeFormat: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
index: PropTypes.number
attachment: PropTypes.object,
timeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number,
useMarkdown: PropTypes.bool,
getCustomEmoji: PropTypes.func
};
Reply.displayName = 'MessageReply';
Title.propTypes = {
attachment: PropTypes.object,
timeFormat: PropTypes.string
};
Title.displayName = 'MessageReplyTitle';
Description.propTypes = {
attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
getCustomEmoji: PropTypes.func
};
Description.displayName = 'MessageReplyDescription';
Fields.propTypes = {
attachment: PropTypes.object
};
Fields.displayName = 'MessageReplyFields';
export default Reply;

View File

@ -0,0 +1,46 @@
import React from 'react';
import { View, Text } from 'react-native';
import PropTypes from 'prop-types';
import { formatLastMessage, formatMessageCount } from './utils';
import styles from './styles';
import { CustomIcon } from '../../lib/Icons';
import { THREAD } from './constants';
const Thread = React.memo(({
msg, tcount, tlm, customThreadTimeFormat
}) => {
if (!tlm) {
return null;
}
const time = formatLastMessage(tlm, customThreadTimeFormat);
const buttonText = formatMessageCount(tcount, THREAD);
return (
<View style={styles.buttonContainer}>
<View
style={[styles.button, styles.smallButton]}
testID={`message-thread-button-${ msg }`}
>
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
<Text style={styles.buttonText}>{buttonText}</Text>
</View>
<Text style={styles.time}>{time}</Text>
</View>
);
}, (prevProps, nextProps) => {
if (prevProps.tcount !== nextProps.tcount) {
return false;
}
return true;
});
Thread.propTypes = {
msg: PropTypes.string,
tcount: PropTypes.string,
tlm: PropTypes.string,
customThreadTimeFormat: PropTypes.string
};
Thread.displayName = 'MessageThread';
export default Thread;

View File

@ -57,14 +57,22 @@ const UrlImage = React.memo(({ image, user, baseUrl }) => {
}
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
});
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
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>
));
), (prevProps, nextProps) => {
if (prevProps.title !== nextProps.title) {
return false;
}
if (prevProps.description !== nextProps.description) {
return false;
}
return true;
});
const Url = React.memo(({
url, index, user, baseUrl
@ -89,16 +97,28 @@ const Url = React.memo(({
);
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
const Urls = React.memo(({ urls, user, baseUrl }) => {
if (!urls || urls.length === 0) {
return null;
}
return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
));
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls));
UrlImage.propTypes = {
image: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string
};
UrlImage.displayName = 'MessageUrlImage';
UrlContent.propTypes = {
title: PropTypes.string,
description: PropTypes.string
};
UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = {
url: PropTypes.object.isRequired,
@ -106,5 +126,13 @@ Url.propTypes = {
user: PropTypes.object,
baseUrl: PropTypes.string
};
Url.displayName = 'MessageUrl';
export default Url;
Urls.propTypes = {
urls: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string
};
Urls.displayName = 'MessageUrls';
export default Urls;

View File

@ -30,28 +30,11 @@ const styles = StyleSheet.create({
}
});
export default class User extends React.PureComponent {
static propTypes = {
timeFormat: PropTypes.string.isRequired,
username: PropTypes.string,
alias: PropTypes.string,
ts: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string
]),
temp: PropTypes.bool
}
render() {
const {
username, alias, ts, temp, timeFormat
} = this.props;
const extraStyle = {};
if (temp) {
extraStyle.opacity = 0.3;
}
const User = React.memo(({
isHeader, useRealName, author, alias, ts, timeFormat
}) => {
if (isHeader) {
const username = (useRealName && author.name) || author.username;
const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
const time = moment(ts).format(timeFormat);
@ -67,4 +50,17 @@ export default class User extends React.PureComponent {
</View>
);
}
}
return null;
});
User.propTypes = {
isHeader: PropTypes.bool,
useRealName: PropTypes.bool,
author: PropTypes.object,
alias: PropTypes.string,
ts: PropTypes.instanceOf(Date),
timeFormat: PropTypes.string
};
User.displayName = 'MessageUser';
export default User;

View File

@ -1,14 +1,14 @@
import React from 'react';
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 { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
import { isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/webm', 'video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -32,77 +32,46 @@ const styles = StyleSheet.create({
}
});
export default class Video extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
customEmojis: PropTypes.object.isRequired
const Video = React.memo(({
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
}) => {
if (!baseUrl) {
return null;
}
state = { isVisible: false };
get uri() {
const { baseUrl, user, file } = this.props;
const { video_url } = file;
return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
}
toggleModal = () => {
this.setState(prevState => ({
isVisible: !prevState.isVisible
}));
}
open = () => {
const { file } = this.props;
const onPress = () => {
if (isTypeSupported(file.video_type)) {
return this.toggleModal();
return onOpenFileModal(file);
}
openLink(this.uri);
}
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
openLink(uri);
};
render() {
const { isVisible } = this.state;
const {
baseUrl, user, customEmojis, file
} = this.props;
const { description } = file;
return (
<React.Fragment>
<Touchable
onPress={onPress}
style={styles.button}
background={Touchable.Ripple('#fff')}
>
<CustomIcon
name='play'
size={54}
style={styles.image}
/>
</Touchable>
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
</React.Fragment>
);
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file));
if (!baseUrl) {
return null;
}
Video.propTypes = {
file: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
useMarkdown: PropTypes.bool,
onOpenFileModal: PropTypes.func,
getCustomEmoji: PropTypes.func
};
return (
[
<View key='button'>
<Touchable
onPress={this.open}
style={styles.button}
background={Touchable.Ripple('#fff')}
>
<CustomIcon
name='play'
size={54}
style={styles.image}
/>
</Touchable>
<Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />
</View>,
<Modal
key='modal'
isVisible={isVisible}
style={styles.modal}
supportedOrientations={['portrait', 'landscape']}
onBackButtonPress={() => this.toggleModal()}
>
<VideoPlayer
source={{ uri: this.uri }}
onBack={this.toggleModal}
disableVolume
/>
</Modal>
]
);
}
}
export default Video;

View File

@ -0,0 +1,2 @@
export const DISCUSSION = 'discussion';
export const THREAD = 'thread';

View File

@ -1,30 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ViewPropTypes } from 'react-native';
import { connect } from 'react-redux';
import equal from 'deep-equal';
import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message';
import {
errorActionsShow as errorActionsShowAction,
toggleReactionPicker as toggleReactionPickerAction,
replyBroadcast as replyBroadcastAction
} from '../../actions/messages';
import { vibrate } from '../../utils/vibration';
import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getCustomEmoji } from './utils';
import messagesStatus from '../../constants/messagesStatus';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis,
Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
Message_TimeFormat: state.settings.Message_TimeFormat,
editingMessage: state.messages.message,
useRealName: state.settings.UI_Use_Real_Name
}), dispatch => ({
errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
replyBroadcast: message => dispatch(replyBroadcastAction(message)),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message))
}))
export default class MessageContainer extends React.Component {
static propTypes = {
item: PropTypes.object.isRequired,
@ -33,31 +15,28 @@ export default class MessageContainer extends React.Component {
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
customTimeFormat: PropTypes.string,
timeFormat: PropTypes.string,
customThreadTimeFormat: PropTypes.string,
style: ViewPropTypes.style,
style: PropTypes.any,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
previousItem: PropTypes.object,
_updatedAt: PropTypes.instanceOf(Date),
// redux
baseUrl: PropTypes.string,
customEmojis: PropTypes.object,
Message_GroupingPeriod: PropTypes.number,
Message_TimeFormat: PropTypes.string,
editingMessage: PropTypes.object,
useRealName: PropTypes.bool,
useMarkdown: PropTypes.bool,
status: PropTypes.number,
navigation: PropTypes.object,
// methods - props
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
// methods - redux
onThreadPress: PropTypes.func,
errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func,
fetchThreadName: PropTypes.func
fetchThreadName: PropTypes.func,
onOpenFileModal: PropTypes.func,
onReactionLongPress: PropTypes.func
}
static defaultProps = {
@ -67,21 +46,11 @@ export default class MessageContainer extends React.Component {
broadcast: false
}
constructor(props) {
super(props);
this.state = { reactionsModal: false };
this.closeReactions = this.closeReactions.bind(this);
}
shouldComponentUpdate(nextProps, nextState) {
const { reactionsModal } = this.state;
shouldComponentUpdate(nextProps) {
const {
status, editingMessage, item, _updatedAt, navigation
status, item, _updatedAt
} = this.props;
if (reactionsModal !== nextState.reactionsModal) {
return true;
}
if (status !== nextProps.status) {
return true;
}
@ -89,65 +58,64 @@ export default class MessageContainer extends React.Component {
return true;
}
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.toISOString() !== nextProps._updatedAt.toISOString();
}
onPress = debounce(() => {
const { item } = this.props;
KeyboardUtils.dismiss();
if ((item.tlm || item.tmid)) {
this.onThreadPress();
}
}, 300, true);
onLongPress = () => {
const { onLongPress } = this.props;
onLongPress(this.parseMessage());
const { archived, onLongPress } = this.props;
if (this.isInfo || this.hasError || archived) {
return;
}
if (onLongPress) {
onLongPress(this.parseMessage());
}
}
onErrorPress = () => {
const { errorActionsShow } = this.props;
errorActionsShow(this.parseMessage());
if (errorActionsShow) {
errorActionsShow(this.parseMessage());
}
}
onReactionPress = (emoji) => {
const { onReactionPress, item } = this.props;
onReactionPress(emoji, item._id);
if (onReactionPress) {
onReactionPress(emoji, item._id);
}
}
onReactionLongPress = () => {
this.setState({ reactionsModal: true });
vibrate();
const { onReactionLongPress, item } = this.props;
if (onReactionLongPress) {
onReactionLongPress(item);
}
}
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'
});
if (onDiscussionPress) {
onDiscussionPress(item);
}
}, 1000, true)
get timeFormat() {
const { customTimeFormat, Message_TimeFormat } = this.props;
return customTimeFormat || Message_TimeFormat;
}
closeReactions = () => {
this.setState({ reactionsModal: false });
onThreadPress = () => {
const { onThreadPress, item } = this.props;
if (onThreadPress) {
onThreadPress(item);
}
}
isHeader = () => {
get isHeader() {
const {
item, previousItem, broadcast, Message_GroupingPeriod
} = this.props;
@ -163,7 +131,7 @@ export default class MessageContainer extends React.Component {
return true;
}
isThreadReply = () => {
get isThreadReply() {
const {
item, previousItem
} = this.props;
@ -173,7 +141,7 @@ export default class MessageContainer extends React.Component {
return false;
}
isThreadSequential = () => {
get isThreadSequential() {
const {
item, previousItem
} = this.props;
@ -183,6 +151,21 @@ export default class MessageContainer extends React.Component {
return false;
}
get isInfo() {
const { item } = this.props;
return SYSTEM_MESSAGES.includes(item.t);
}
get isTemp() {
const { item } = this.props;
return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR;
}
get hasError() {
const { item } = this.props;
return item.status === messagesStatus.ERROR;
}
parseMessage = () => {
const { item } = this.props;
return JSON.parse(JSON.stringify(item));
@ -190,23 +173,26 @@ export default class MessageContainer extends React.Component {
toggleReactionPicker = () => {
const { toggleReactionPicker } = this.props;
toggleReactionPicker(this.parseMessage());
if (toggleReactionPicker) {
toggleReactionPicker(this.parseMessage());
}
}
replyBroadcast = () => {
const { replyBroadcast } = this.props;
replyBroadcast(this.parseMessage());
if (replyBroadcast) {
replyBroadcast(this.parseMessage());
}
}
render() {
const { reactionsModal } = this.state;
const {
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast, fetchThreadName, customThreadTimeFormat
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown
} = this.props;
const {
_id, msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels
} = item;
const isEditing = editingMessage._id === item._id;
return (
<Message
id={_id}
@ -214,26 +200,18 @@ export default class MessageContainer extends React.Component {
author={u}
ts={ts}
type={t}
status={status}
attachments={attachments}
urls={urls}
reactions={reactions}
alias={alias}
editing={isEditing}
header={this.isHeader()}
isThreadReply={this.isThreadReply()}
isThreadSequential={this.isThreadSequential()}
avatar={avatar}
user={user}
edited={editedBy && !!editedBy.username}
timeFormat={this.timeFormat}
timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style}
archived={archived}
broadcast={broadcast}
baseUrl={baseUrl}
customEmojis={customEmojis}
reactionsModal={reactionsModal}
useRealName={useRealName}
role={role}
drid={drid}
@ -243,16 +221,27 @@ export default class MessageContainer extends React.Component {
tcount={tcount}
tlm={tlm}
tmsg={tmsg}
useMarkdown={useMarkdown}
fetchThreadName={fetchThreadName}
closeReactions={this.closeReactions}
mentions={mentions}
channels={channels}
isEdited={editedBy && !!editedBy.username}
isHeader={this.isHeader}
isThreadReply={this.isThreadReply}
isThreadSequential={this.isThreadSequential}
isInfo={this.isInfo}
isTemp={this.isTemp}
hasError={this.hasError}
onErrorPress={this.onErrorPress}
onPress={this.onPress}
onLongPress={this.onLongPress}
onReactionLongPress={this.onReactionLongPress}
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
toggleReactionPicker={this.toggleReactionPicker}
onDiscussionPress={this.onDiscussionPress}
onThreadPress={this.onThreadPress}
onOpenFileModal={onOpenFileModal}
getCustomEmoji={getCustomEmoji}
/>
);
}

View File

@ -18,8 +18,7 @@ export default StyleSheet.create({
paddingVertical: 4,
width: '100%',
paddingHorizontal: 14,
flexDirection: 'column',
flex: 1
flexDirection: 'column'
},
messageContent: {
flex: 1,
@ -32,8 +31,8 @@ export default StyleSheet.create({
marginLeft: 0
},
flex: {
flexDirection: 'row',
flex: 1
flexDirection: 'row'
// flex: 1
},
text: {
fontSize: 16,
@ -46,9 +45,6 @@ export default StyleSheet.create({
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
editing: {
backgroundColor: '#fff5df'
},
customEmoji: {
width: 20,
height: 20
@ -161,7 +157,7 @@ export default StyleSheet.create({
justifyContent: 'flex-start'
},
imageContainer: {
flex: 1,
// flex: 1,
flexDirection: 'column',
borderRadius: 4
},
@ -173,6 +169,9 @@ export default StyleSheet.create({
borderColor: COLOR_BORDER,
borderWidth: 1
},
imagePressed: {
opacity: 0.5
},
inlineImage: {
width: 300,
height: 300,
@ -220,7 +219,7 @@ export default StyleSheet.create({
},
repliedThread: {
flexDirection: 'row',
flex: 1,
// flex: 1,
alignItems: 'center',
marginTop: 6,
marginBottom: 12

View File

@ -0,0 +1,116 @@
import moment from 'moment';
import I18n from '../../i18n';
import database from '../../lib/realm';
import { DISCUSSION } from './constants';
export const formatLastMessage = (lm, customFormat) => {
if (customFormat) {
return moment(lm).format(customFormat);
}
return lm ? moment(lm).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null;
};
export const 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;
};
export const BUTTON_HIT_SLOP = {
top: 4, right: 4, bottom: 4, left: 4
};
export const SYSTEM_MESSAGES = [
'r',
'au',
'ru',
'ul',
'uj',
'ut',
'rm',
'user-muted',
'user-unmuted',
'message_pinned',
'subscription-role-added',
'subscription-role-removed',
'room_changed_description',
'room_changed_announcement',
'room_changed_topic',
'room_changed_privacy',
'message_snippeted',
'thread-created'
];
export const getInfoMessage = ({
type, role, msg, author
}) => {
const { username } = author;
if (type === 'rm') {
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') {
return I18n.t('Message_pinned');
} else if (type === 'ul') {
return I18n.t('Has_left_the_channel');
} else if (type === 'ru') {
return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
} else if (type === 'au') {
return I18n.t('User_added_by', { userAdded: msg, userBy: username });
} else if (type === 'user-muted') {
return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
} else if (type === 'user-unmuted') {
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
} else if (type === 'subscription-role-added') {
return `${ msg } was set ${ role } by ${ username }`;
} else if (type === 'subscription-role-removed') {
return `${ msg } is no longer ${ role } by ${ username }`;
} else if (type === 'room_changed_description') {
return I18n.t('Room_changed_description', { description: msg, userBy: username });
} else if (type === 'room_changed_announcement') {
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
} else if (type === 'room_changed_topic') {
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
} else if (type === 'room_changed_privacy') {
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
} else if (type === 'message_snippeted') {
return I18n.t('Created_snippet');
}
return '';
};
export const getCustomEmoji = (content) => {
// search by name
const data = database.objects('customEmojis').filtered('name == $0', content);
if (data.length) {
return data[0];
}
// searches by alias
// RealmJS doesn't support IN operator: https://github.com/realm/realm-js/issues/450
const emojis = database.objects('customEmojis');
const findByAlias = emojis.find((emoji) => {
if (emoji.aliases.length && emoji.aliases.findIndex(alias => alias === content) !== -1) {
return true;
}
return false;
});
return findByAlias;
};

View File

@ -308,7 +308,6 @@ export default {
This_room_is_blocked: 'Dieser Raum ist gesperrt',
This_room_is_read_only: 'Dieser Raum kann nur gelesen werden',
Timezone: 'Zeitzone',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'Thema',
Topic: 'Thema',
Try_again: 'Versuchen Sie es nochmal',

View File

@ -81,6 +81,7 @@ export default {
Add_Reaction: 'Add Reaction',
Add_Server: 'Add Server',
Add_user: 'Add user',
Admin_Panel: 'Admin Panel',
Alert: 'Alert',
alert: 'alert',
alerts: 'alerts',
@ -147,13 +148,15 @@ export default {
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?',
edit: 'edit',
erasing_room: 'erasing room',
edited: 'edited',
Edit: 'Edit',
Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email',
email: 'e-mail',
Enable_markdown: 'Enable markdown',
Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel',
erasing_room: 'erasing room',
Error_uploading: 'Error uploading',
Favorites: 'Favorites',
Files: 'Files',
@ -203,6 +206,7 @@ export default {
message: 'message',
messages: 'messages',
Messages: 'Messages',
Message_Reported: 'Message reported',
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
Microphone_Permission: 'Microphone Permission',
Mute: 'Mute',
@ -266,6 +270,7 @@ export default {
replies: 'replies',
reply: 'reply',
Reply: 'Reply',
Report: 'Report',
Resend: 'Resend',
Reset_password: 'Reset password',
resetting_password: 'resetting password',
@ -324,7 +329,6 @@ export default {
Thread: 'Thread',
Threads: 'Threads',
Timezone: 'Timezone',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'topic',
Topic: 'Topic',
Try_again: 'Try again',

View File

@ -309,7 +309,6 @@ export default {
This_room_is_blocked: 'Cette canal est bloquée',
This_room_is_read_only: 'Cette canal est en lecture seule',
Timezone: 'Fuseau horaire',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'sujet',
Topic: 'Sujet',
Try_again: 'Réessayer',

View File

@ -154,11 +154,13 @@ export default {
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?',
edit: 'editar',
edited: 'editado',
erasing_room: 'apagando sala',
Edit: 'Editar',
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
Enable_markdown: 'Habilitar markdown',
Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo',

View File

@ -311,7 +311,6 @@ export default {
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',

View File

@ -270,7 +270,6 @@ export default {
This_room_is_blocked: 'Этот канал заблокирован',
This_room_is_read_only: 'Этот канал доступен только для чтения',
Timezone: 'Часовой пояс',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'топик',
Topic: 'Топик',
Try_again: 'Попробуйте еще раз',

View File

@ -305,7 +305,6 @@ export default {
This_room_is_blocked: '这个房间被锁了',
This_room_is_read_only: '这个房间是只读的',
Timezone: '时区',
Toggle_Drawer: 'Toggle_Drawer',
topic: '主题',
Topic: '主题',
Try_again: '再试一次',

View File

@ -5,6 +5,7 @@ import {
import { Provider } from 'react-redux';
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
import { Linking } from 'react-native';
import firebase from 'react-native-firebase';
import { appInit } from './actions';
import { deepLinkingOpen } from './actions/deepLinking';
@ -20,16 +21,14 @@ import Navigation from './lib/Navigation';
import Sidebar from './views/SidebarView';
import ProfileView from './views/ProfileView';
import SettingsView from './views/SettingsView';
import AdminPanelView from './views/AdminPanelView';
import RoomActionsView from './views/RoomActionsView';
import RoomInfoView from './views/RoomInfoView';
import RoomInfoEditView from './views/RoomInfoEditView';
import RoomMembersView from './views/RoomMembersView';
import RoomFilesView from './views/RoomFilesView';
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 MessagesView from './views/MessagesView';
import SelectedUsersView from './views/SelectedUsersView';
import CreateChannelView from './views/CreateChannelView';
import LegalView from './views/LegalView';
@ -108,13 +107,10 @@ const ChatsStack = createStackNavigator({
RoomInfoView,
RoomInfoEditView,
RoomMembersView,
RoomFilesView,
MentionedMessagesView,
StarredMessagesView,
SearchMessagesView,
PinnedMessagesView,
SelectedUsersView,
ThreadMessagesView
ThreadMessagesView,
MessagesView
}, {
defaultNavigationOptions: defaultHeader
});
@ -151,6 +147,12 @@ const SettingsStack = createStackNavigator({
defaultNavigationOptions: defaultHeader
});
const AdminPanelStack = createStackNavigator({
AdminPanelView
}, {
defaultNavigationOptions: defaultHeader
});
SettingsStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
if (navigation.state.index > 0) {
@ -164,7 +166,8 @@ SettingsStack.navigationOptions = ({ navigation }) => {
const ChatsDrawer = createDrawerNavigator({
ChatsStack,
ProfileStack,
SettingsStack
SettingsStack,
AdminPanelStack
}, {
contentComponent: Sidebar
});
@ -202,6 +205,28 @@ const App = createAppContainer(createSwitchNavigator(
}
));
// gets the current screen from navigation state
const getActiveRouteName = (navigationState) => {
if (!navigationState) {
return null;
}
const route = navigationState.routes[navigationState.index];
// dive into nested navigators
if (route.routes) {
return getActiveRouteName(route);
}
return route.routeName;
};
const onNavigationStateChange = (prevState, currentState) => {
const currentScreen = getActiveRouteName(currentState);
const prevScreen = getActiveRouteName(prevState);
if (prevScreen !== currentScreen) {
firebase.analytics().setCurrentScreen(currentScreen);
}
};
export default class Root extends React.Component {
constructor(props) {
super(props);
@ -242,6 +267,7 @@ export default class Root extends React.Component {
ref={(navigatorRef) => {
Navigation.setTopLevelNavigator(navigatorRef);
}}
onNavigationStateChange={onNavigationStateChange}
/>
</Provider>
);

View File

@ -3,7 +3,6 @@ import semver from 'semver';
import reduxStore from '../createStore';
import database from '../realm';
import * as actions from '../../actions';
import log from '../../utils/log';
const getUpdatedSince = () => {
@ -17,7 +16,7 @@ const create = (customEmojis) => {
try {
database.create('customEmojis', emoji, true);
} catch (e) {
log('getEmojis create', e);
// log('getEmojis create', e);
}
});
}
@ -40,7 +39,6 @@ export default async function() {
database.write(() => {
create(emojis);
});
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(result.emojis)));
});
} else {
const params = {};
@ -68,17 +66,14 @@ export default async function() {
database.delete(emojiRecord);
}
} catch (e) {
log('getEmojis delete', e);
log('err_get_emojis_delete', e);
}
});
}
const allEmojis = database.objects('customEmojis');
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(allEmojis)));
})
);
}
} catch (e) {
log('getCustomEmojis', e);
log('err_get_custom_emojis', e);
}
}

View File

@ -16,7 +16,7 @@ const create = (permissions) => {
try {
database.create('permissions', permission, true);
} catch (e) {
log('getPermissions create', e);
log('err_get_permissions_create', e);
}
});
}
@ -63,7 +63,7 @@ export default async function() {
database.delete(permission);
}
} catch (e) {
log('getPermissions delete', e);
log('err_get_permissions_delete', e);
}
});
}
@ -71,6 +71,6 @@ export default async function() {
);
}
} catch (e) {
log('getPermissions', e);
log('err_get_permissions', e);
}
}

View File

@ -20,12 +20,12 @@ export default async function() {
try {
database.create('roles', role, true);
} catch (e) {
log('getRoles create', e);
log('err_get_roles_create', e);
}
}));
});
}
} catch (e) {
log('getRoles', e);
log('err_get_roles', e);
}
}

View File

@ -11,7 +11,7 @@ function updateServer(param) {
try {
database.databases.serversDB.create('servers', { id: reduxStore.getState().server.server, ...param }, true);
} catch (e) {
log('updateServer', e);
log('err_get_settings_update_server', e);
}
});
}
@ -34,7 +34,7 @@ export default async function() {
try {
database.create('settings', { ...setting, _updatedAt: new Date() }, true);
} catch (e) {
log('create settings', e);
log('err_get_settings_create', e);
}
if (setting._id === 'Site_Name') {
@ -52,6 +52,6 @@ export default async function() {
updateServer.call(this, { iconURL });
}
} catch (e) {
log('getSettings', e);
log('err_get_settings', e);
}
}

View File

@ -0,0 +1,15 @@
export default function(message) {
if (/image/.test(message.type)) {
return { image_url: message.url };
}
if (/audio/.test(message.type)) {
return { audio_url: message.url };
}
if (/video/.test(message.type)) {
return { video_url: message.url };
}
return {
title_link: message.url,
type: 'file'
};
}

View File

@ -27,9 +27,8 @@ export const merge = (subscription, room) => {
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 }));
subscription.muted = room.muted.filter(muted => !!muted);
} else {
subscription.muted = [];
}

View File

@ -33,7 +33,7 @@ export default (msg) => {
// msg.reactions = Object.keys(msg.reactions).map(key => ({ emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
// }
if (!Array.isArray(msg.reactions)) {
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames }));
}
msg.urls = msg.urls ? parseUrls(msg.urls) : [];
msg._updatedAt = new Date();

View File

@ -1,5 +1,3 @@
import { Answers } from 'react-native-fabric';
export default fn => (...params) => {
try {
fn(...params);
@ -8,7 +6,6 @@ export default fn => (...params) => {
if (typeof error !== 'object') {
error = { error };
}
Answers.logCustom('error', error);
if (__DEV__) {
alert(error);
}

View File

@ -52,7 +52,7 @@ export default function loadMessagesForRoom(...args) {
database.create('threadMessages', message, true);
}
} catch (e) {
log('loadMessagesForRoom -> create messages', e);
log('err_load_messages_for_room_create', e);
}
}));
return resolve(data);
@ -61,7 +61,7 @@ export default function loadMessagesForRoom(...args) {
return resolve([]);
}
} catch (e) {
log('loadMessagesForRoom', e);
log('err_load_messages_for_room', e);
reject(e);
}
});

View File

@ -45,7 +45,7 @@ export default function loadMissedMessages(...args) {
database.create('threadMessages', message, true);
}
} catch (e) {
log('loadMissedMessages -> create messages', e);
log('err_load_missed_messages_create', e);
}
}));
});
@ -65,14 +65,14 @@ export default function loadMissedMessages(...args) {
});
});
} catch (e) {
log('loadMissedMessages -> delete message', e);
log('err_load_missed_messages_delete', e);
}
});
}
}
resolve();
} catch (e) {
log('loadMissedMessages', e);
log('err_load_missed_messages', e);
reject(e);
}
});

View File

@ -34,7 +34,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
message.rid = tmid;
database.create('threadMessages', message, true);
} catch (e) {
log('loadThreadMessages -> create messages', e);
log('err_load_thread_messages_create', e);
}
}));
return resolve(data);
@ -43,7 +43,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
return resolve([]);
}
} catch (e) {
log('loadThreadMessages', e);
log('err_load_thread_messages', e);
reject(e);
}
});

View File

@ -18,6 +18,6 @@ export default async function readMessages(rid) {
});
return data;
} catch (e) {
log('readMessages', e);
log('err_read_messages', e);
}
}

View File

@ -51,7 +51,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
try {
database.create('uploads', fileInfo, true);
} catch (e) {
return log('sendFileMessage -> create uploads 1', e);
return log('err_send_file_message_create_upload_1', e);
}
});
@ -69,7 +69,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
try {
database.create('uploads', fileInfo, true);
} catch (e) {
return log('sendFileMessage -> create uploads 2', e);
return log('err_send_file_message_create_upload_2', e);
}
});
});
@ -95,7 +95,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
try {
database.delete(upload);
} catch (e) {
log('sendFileMessage -> delete uploads', e);
log('err_send_file_message_delete_upload', e);
}
});
} catch (e) {
@ -104,7 +104,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
try {
database.create('uploads', fileInfo, true);
} catch (err) {
log('sendFileMessage -> create uploads 3', err);
log('err_send_file_message_create_upload_3', err);
}
});
}

View File

@ -66,6 +66,6 @@ export default async function(rid, msg, tmid) {
});
}
} catch (e) {
log('sendMessage', e);
log('err_send_message', e);
}
}

View File

@ -63,7 +63,7 @@ export default function subscribeRoom({ rid }) {
typingTimeouts[username] = null;
}
} catch (error) {
console.log('TCL: removeUserTyping -> error', error);
log('err_remove_user_typing', error);
}
};
@ -85,7 +85,7 @@ export default function subscribeRoom({ rid }) {
removeUserTyping(username);
}, 10000);
} catch (error) {
console.log('TCL: addUserTyping -> error', error);
log('err_add_user_typing', error);
}
}
};
@ -125,7 +125,7 @@ export default function subscribeRoom({ rid }) {
const read = debounce(() => {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room._id) {
if (room && room._id) {
this.readMessages(rid);
}
}, 300);
@ -198,7 +198,7 @@ export default function subscribeRoom({ rid }) {
try {
promises = this.sdk.subscribeRoom(rid);
} catch (e) {
log('subscribeRoom', e);
log('err_subscribe_room', e);
}
return {

View File

@ -59,7 +59,7 @@ export default async function subscribeRooms() {
database.delete(subscription);
});
} catch (e) {
log('handleStreamMessageReceived -> subscriptions removed', e);
log('err_stream_msg_received_sub_removed', e);
}
} else {
const rooms = database.objects('rooms').filtered('_id == $0', data.rid);
@ -70,7 +70,7 @@ export default async function subscribeRooms() {
database.delete(rooms);
});
} catch (e) {
log('handleStreamMessageReceived -> subscriptions updated', e);
log('err_stream_msg_received_sub_updated', e);
}
}
}
@ -83,7 +83,7 @@ export default async function subscribeRooms() {
database.create('subscriptions', tmp, true);
});
} catch (e) {
log('handleStreamMessageReceived -> rooms updated', e);
log('err_stream_msg_received_room_updated', e);
}
} else if (type === 'inserted') {
try {
@ -91,7 +91,7 @@ export default async function subscribeRooms() {
database.create('rooms', data, true);
});
} catch (e) {
log('handleStreamMessageReceived -> rooms inserted', e);
log('err_stream_msg_received_room_inserted', e);
}
}
}
@ -116,7 +116,7 @@ export default async function subscribeRooms() {
database.create('messages', message, true);
});
} catch (e) {
log('handleStreamMessageReceived -> message', e);
log('err_stream_msg_received_message', e);
}
});
}
@ -146,7 +146,7 @@ export default async function subscribeRooms() {
try {
await this.sdk.subscribeNotifyUser();
} catch (e) {
log('subscribeRooms', e);
log('err_subscribe_rooms', e);
}
return {

View File

@ -43,18 +43,11 @@ const roomsSchema = {
primaryKey: '_id',
properties: {
_id: 'string',
name: 'string?',
broadcast: { type: 'bool', optional: true }
}
};
const userMutedInRoomSchema = {
name: 'usersMuted',
primaryKey: 'value',
properties: {
value: 'string'
}
};
const subscriptionSchema = {
name: 'subscriptions',
primaryKey: '_id',
@ -85,7 +78,7 @@ const subscriptionSchema = {
archived: { type: 'bool', optional: true },
joinCodeRequired: { type: 'bool', optional: true },
notifications: { type: 'bool', optional: true },
muted: { type: 'list', objectType: 'usersMuted' },
muted: 'string[]',
broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true },
@ -99,8 +92,7 @@ const usersSchema = {
properties: {
_id: 'string',
username: 'string',
name: { type: 'string', optional: true },
avatarVersion: { type: 'int', optional: true }
name: { type: 'string', optional: true }
}
};
@ -155,21 +147,13 @@ const url = {
}
};
const messagesReactionsUsernamesSchema = {
name: 'messagesReactionsUsernames',
primaryKey: 'value',
properties: {
value: 'string'
}
};
const messagesReactionsSchema = {
name: 'messagesReactions',
primaryKey: '_id',
properties: {
_id: 'string',
emoji: 'string',
usernames: { type: 'list', objectType: 'messagesReactionsUsernames' }
usernames: 'string[]'
}
};
@ -211,7 +195,9 @@ const messagesSchema = {
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]'
replies: 'string[]',
mentions: { type: 'list', objectType: 'users' },
channels: { type: 'list', objectType: 'rooms' }
}
};
@ -359,9 +345,7 @@ const schema = [
frequentlyUsedEmojiSchema,
customEmojisSchema,
messagesReactionsSchema,
messagesReactionsUsernamesSchema,
rolesSchema,
userMutedInRoomSchema,
uploadsSchema
];
@ -374,9 +358,9 @@ class DB {
schema: [
serversSchema
],
schemaVersion: 6,
schemaVersion: 8,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 8) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
@ -431,16 +415,11 @@ class DB {
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,
schema,
schemaVersion: 9,
schemaVersion: 11,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
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;
}
newRealm.delete(newSubs);
const newMessages = newRealm.objects('messages');
newRealm.delete(newMessages);
const newThreads = newRealm.objects('threads');
@ -449,8 +428,6 @@ class DB {
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');

View File

@ -40,9 +40,12 @@ import { roomsRequest } from '../actions/rooms';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
export const MARKDOWN_KEY = 'RC_MARKDOWN_KEY';
const returnAnArray = obj => obj || [];
const MIN_ROCKETCHAT_VERSION = '0.70.0';
const STATUSES = ['offline', 'online', 'away', 'busy'];
const RocketChat = {
TOKEN_KEY,
subscribeRooms,
@ -95,7 +98,7 @@ const RocketChat = {
return result;
}
} catch (e) {
log('getServerInfo', e);
log('err_get_server_info', e);
}
return {
success: false,
@ -168,14 +171,7 @@ const RocketChat = {
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);
this.getUserPresence();
},
connect({ server, user }) {
database.setActiveDB(server);
@ -213,6 +209,10 @@ const RocketChat = {
this.sdk.onStreamData('connected', () => {
reduxStore.dispatch(connectSuccess());
const { isAuthenticated } = reduxStore.getState().login;
if (isAuthenticated) {
this.getUserPresence();
}
});
this.sdk.onStreamData('close', () => {
@ -220,6 +220,25 @@ const RocketChat = {
});
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
this.sdk.onStreamData('stream-notify-logged', protectedFunction((ddpMessage) => {
const { eventName } = ddpMessage.fields;
if (eventName === 'user-status') {
const userStatus = ddpMessage.fields.args[0];
const [id, username, status] = userStatus;
if (username) {
database.memoryDatabase.write(() => {
try {
database.memoryDatabase.create('activeUsers', {
id, username, status: STATUSES[status]
}, true);
} catch (error) {
console.log(error);
}
});
}
}
}));
},
register(credentials) {
@ -386,7 +405,7 @@ const RocketChat = {
database.create('messages', message, true);
});
} catch (e) {
log('resendMessage error', e);
log('err_resend_message', e);
}
}
},
@ -472,19 +491,6 @@ const RocketChat = {
return setting;
});
},
parseEmojis: emojis => emojis.reduce((ret, item) => {
ret[item.name] = item.extension;
item.aliases.forEach((alias) => {
ret[alias.value] = item.extension;
});
return ret;
}, {}),
_prepareEmojis(emojis) {
emojis.forEach((emoji) => {
emoji.aliases = emoji.aliases.map(alias => ({ value: alias }));
});
return emojis;
},
deleteMessage(message) {
const { _id, rid } = message;
// RC 0.48.0
@ -511,6 +517,9 @@ const RocketChat = {
// RC 0.59.0
return this.sdk.post('chat.pinMessage', { messageId: message._id });
},
reportMessage(messageId) {
return this.sdk.post('chat.reportMessage', { messageId, description: 'Message reported by user' });
},
getRoom(rid) {
const [result] = database.objects('subscriptions').filtered('rid = $0', rid);
if (!result) {
@ -518,12 +527,12 @@ const RocketChat = {
}
return Promise.resolve(result);
},
async getPermalink(message) {
async getPermalinkMessage(message) {
let room;
try {
room = await RocketChat.getRoom(message.rid);
} catch (e) {
log('Rocketchat.getPermalink', e);
log('err_get_permalink', e);
return null;
}
const { server } = reduxStore.getState().server;
@ -534,6 +543,15 @@ const RocketChat = {
}[room.t];
return `${ server }/${ roomType }/${ room.name }?msg=${ message._id }`;
},
getPermalinkChannel(channel) {
const { server } = reduxStore.getState().server;
const roomType = {
p: 'group',
c: 'channel',
d: 'direct'
}[channel.t];
return `${ server }/${ roomType }/${ channel.name }`;
},
subscribe(...args) {
return this.sdk.subscribe(...args);
},
@ -695,6 +713,13 @@ const RocketChat = {
// RC 0.51.0
return this.sdk.methodCall('setAvatarFromService', data, contentType, service);
},
async getUseMarkdown() {
const useMarkdown = await AsyncStorage.getItem(MARKDOWN_KEY);
if (useMarkdown === null) {
return true;
}
return JSON.parse(useMarkdown);
},
async getSortPreferences() {
const prefs = await AsyncStorage.getItem(SORT_PREFS_KEY);
return JSON.parse(prefs);
@ -769,9 +794,9 @@ const RocketChat = {
toggleFollowMessage(mid, follow) {
// RC 1.0
if (follow) {
return this.sdk.methodCall('followMessage', { mid });
return this.sdk.post('chat.followMessage', { mid });
}
return this.sdk.methodCall('unfollowMessage', { mid });
return this.sdk.post('chat.unfollowMessage', { mid });
},
getThreadsList({ rid, count, offset }) {
// RC 1.0
@ -784,6 +809,42 @@ const RocketChat = {
return this.sdk.get('chat.syncThreadsList', {
rid, updatedSince
});
},
async getUserPresence() {
const serverVersion = reduxStore.getState().server.version;
// if server is lower than 1.1.0
if (semver.lt(semver.coerce(serverVersion), '1.1.0')) {
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
this.activeUsersSubTimeout = setTimeout(() => {
this.sdk.subscribe('activeUsers');
}, 5000);
} else {
const params = {};
if (this.lastUserPresenceFetch) {
params.from = this.lastUserPresenceFetch.toISOString();
}
// RC 1.1.0
const result = await this.sdk.get('users.presence', params);
if (result.success) {
this.lastUserPresenceFetch = new Date();
database.memoryDatabase.write(() => {
result.users.forEach((item) => {
try {
item.id = item._id;
database.memoryDatabase.create('activeUsers', item, true);
} catch (error) {
console.log(error);
}
});
});
this.sdk.subscribe('stream-notify-logged', 'user-status');
}
}
}
};

3
app/lib/utils.js Normal file
View File

@ -0,0 +1,3 @@
export const formatAttachmentUrl = (attachmentUrl, userId, token, server) => (
encodeURI(attachmentUrl.includes('http') ? attachmentUrl : `${ server }${ attachmentUrl }?rc_uid=${ userId }&rc_token=${ token }`)
);

View File

@ -1,13 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ViewPropTypes } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import scrollPersistTaps from '../utils/scrollPersistTaps';
export default class KeyboardView extends React.PureComponent {
static propTypes = {
style: ViewPropTypes.style,
contentContainerStyle: ViewPropTypes.style,
style: PropTypes.any,
contentContainerStyle: PropTypes.any,
keyboardVerticalOffset: PropTypes.number,
scrollEnabled: PropTypes.bool,
children: PropTypes.oneOfType([

View File

@ -46,6 +46,12 @@ export default class RoomItem extends React.Component {
avatarSize: 48
}
// Making jest happy: https://github.com/facebook/react-native/issues/22175
// eslint-disable-next-line no-useless-constructor
constructor(props) {
super(props);
}
shouldComponentUpdate(nextProps) {
const { lastMessage, _updatedAt } = this.props;
const oldlastMessage = lastMessage;

View File

@ -1,7 +1,5 @@
import React from 'react';
import {
Text, View, StyleSheet, ViewPropTypes
} from 'react-native';
import { Text, View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import Avatar from '../containers/Avatar';
@ -70,7 +68,7 @@ UserItem.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired,
onLongPress: PropTypes.func,
style: ViewPropTypes.style,
style: PropTypes.any,
icon: PropTypes.string
};

View File

@ -1,17 +0,0 @@
import * as types from '../constants/types';
const initialState = {
customEmojis: {}
};
export default function customEmojis(state = initialState.customEmojis, action) {
if (action.type === types.SET_CUSTOM_EMOJIS) {
return {
...state,
...action.payload
};
}
return state;
}

View File

@ -8,8 +8,8 @@ import server from './server';
import selectedUsers from './selectedUsers';
import createChannel from './createChannel';
import app from './app';
import customEmojis from './customEmojis';
import sortPreferences from './sortPreferences';
import markdown from './markdown';
export default combineReducers({
settings,
@ -21,6 +21,6 @@ export default combineReducers({
createChannel,
app,
rooms,
customEmojis,
sortPreferences
sortPreferences,
markdown
});

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