Merge 1.13.0 into Master (#936)
* fix last messages (#239) * fix last messages * Room actions (#231) * Layout * Empty starred list * Favorite room * Pinned messages * fix last messages * fix date on pinned messages * fix package * [NEW] OAuth (#241) * Layout * tmp * test iscordova * Webview redirecting * Open and Close login actions * Login services saved on redux * OAuth Github * Server regex fix * OAuth modal style * - Twitter login - Remove services from redux - Open login saga fix * - Facebook login - Fixed user agent - Reactions fix - Message url unique key fix * Google login * Email keyboard removed from messagebox * - Login buttons refactored - RoomList header * Layout improvements * Meteor login redirect_uri changed * fix * Random credentialToken state * [NEW] Room actions: Mentioned messages and Room Members (#242) * Mentioned messages * Starred and pinned actions debounce * Room members * Open room on member touch * [WIP] Improves (#245) * hotfix for ios * hotfix for ios * Update config.yml * Workaround for RN 0.54 on iOS (#246) * Update iOS to RN 0.54 (#248) * Update iOS to RN 0.54 * [WIP] Audio message functionality (#247) * [NEW] Add module react-native-audio * [WIP] Audio message basic UI * [NEW] Record audio message * Use cordova repository to get certificates * Icon 1024 * [NEW] Room actions: block user, snippet messages, room files and leave room (#250) * - Block user - Load room members async - fixed reactive change of room's read only flag * Snippet messages * - Room files - Dismiss Video component on back button press - Improvements on Image component * Improvement on Video component * Leave room * Missing message types * lint * Reactotron working (#249) * [NEW] Room info and Room info edit (#254) * - Block user - Load room members async - fixed reactive change of room's read only flag * Snippet messages * - Room files - Dismiss Video component on back button press - Improvements on Image component * Improvement on Video component * Leave room * Missing message types * lint * - Room info (read only) - Missing message types * Room info scroll * - Tap on room header opens room info - Layout tweaks * - Room info edit - iOS Toast fixed * - Style not implemented actions as disabled * Edit room permission * - Save all room settings in a single call - Implemented roomType and readOnly * - Allow reacting when room is read only * Message type added: room_changed_privacy * Erase room * Created TextInput and SwitchContainer components for reuse and readability * - hasPermission method * - Archive/Unarchive room - Set Join Code * Twitter keyboard type on iOS * Archived room * reactWhenReadOnly permission on message * Active users refactored * User roles * - Subscribe to roles (in order to get role description info: e.g. 'core-team' to 'Rocket.Chat Team') - Save roles to realm (for offline access) - Save roles to redux (and get data from realm on app init) * Lint * code style * password show/hide feature * fix show/hide password * password show/hide * Crashlytics (#258) * Fabric iOS * Fabric configured on iOS and Android * login tracked * more logs * fix reaction * CI fix * Bug fixes (#261) * Layout fixes * RoomsListView's SafeAreaView * Unhandled promise rejection fix * Prevent navigation from opening scenes twice * Create channel fixes * Create LICENSE * Beta (#265) * Fabric iOS * Fabric configured on iOS and Android * - react-native-fabric configured - login tracked * README updated * Run scripts from README updated * README scripts * get rooms and messages by rest * user status * more improves * more improves * send pong on timeout * fix some methods * more tests * rest messages * Room actions (#266) * Toggle notifications * Search messages * Invite users * Mute/Unmute users in room * rocket.cat messages * Room topic layout fixed * Starred messages loading onEndReached * Room actions onEndReached * Unnecessary login request * Login loading * Login services fixed * User presence layout * ïmproves on room actions view * Removed unnecessary data from SelectedUsersView * load few messages on open room, search message improve * fix loading messages forever * Removed state from search * Custom message time format * secureTextEntry layout * Reduce android app size * Roles subscription fix * Public routes navigation * fix reconnect * - New login/register, login, register * proguard * Login flux * App init/restore * Android layout fixes * Multiple meteor connection requests fixed * Nested attachments * Nested attachments * fix check status * New login layout (#269) * Public routes navigation * New login/register, login, register * Multiple meteor connection requests fixed * Nested attachments * Button component * TextInput android layout fixed * Register fixed * Thinner close modal button * Requests /me after login only one time * Static images moved * fix reconnect * fix ddp * fix custom emoji * New message layout (#273) * Grouping messages * Message layout * Users typing animation * Image attachment layout * Fabric and image fix (#284) * Fixed images not showing * Keyboard libs updated * Fabric fix and location removed (#286) * Proguard disabled * message with list + links fixed (#288) * Better image cache component (#292) * react-native-img-cache removed * Improve list render * Support <http://link/Text> inside markdown * Deep linking (#291) * deep linking * Basic deep link working * Deep link routing * Multiple servers working * Send user to the room * Avatar initials and room type icon (#298) * Deep linking fix and more (#294) * Fix - Any https link was deep linking to RocketChat * Keyboard dismiss after add new server * Room info bug fix * Opacity animation * Navigation when adding server fixed * Throttle for unnecessary render on receiving several messages * Search inputs without autocorrect and autocapitalize * Search messages fixed * Messagebox unnecessary render and spotlight fixed * react-native-keyboard-input updated * Lint * Tests updated * Update all dependencies (#299) * Update react-navigation to the latest version 🚀 (#293) * fix(package): update react-navigation to version 2.0.0 * Code updated to support breaking changes of react-navigation * Detox tests E2E (#283) * RoomsListView re-render (#304) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> - [x] Removed unnecessary re-renders on RoomsListView * [NEW] Broadcast channels (#301) * Broadcast channels * e2e tests * New markdown (#306) Our current markdown is causing a lot of issues on Android devices, since it wraps everything inside a Text component. On Android, Text doesn't support View as a child. This PR adds react-native-markdown-renderer, that uses View as wrapper and may be better. * Fixed audio recording issues (#310) * Fix for "java.lang.IllegalArgumentException: unexpected url" (#313) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> User was able to add an invalid instance of Rocket.Chat by pressing submit button instead of "Connect" button. <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * I18n (#312) * Unread and date separator layout improved (#319) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> - [x] Unread and date separator layout - [x] "Start of conversation"/"Loading messages" label ![screen shot 2018-05-30 at 18 10 43](https://user-images.githubusercontent.com/804994/40747867-0424964a-6435-11e8-9293-31cc43c110ab.png) ![screen shot 2018-05-30 at 18 09 05](https://user-images.githubusercontent.com/804994/40747868-04484784-6435-11e8-8c31-92e0776276f0.png) <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * [FIX] iOS Universal links (#318) * [NEW] Drawer (#322) * [FIX] invalid user muted value * Ddp fixes (#324) * [NEW] User Profile (#323) * Drawer layout * Drawer changes * Profile * Profile avatar * Set language * Tests * Custom fields * Readme updated * fix invalid user muted value * Fix for "Cannot add a child that doesn't have a YogaNode to a parent without a measure function! (Trying to add a 'RCTVirtualText' to a 'RCTView')" * Settings/Permissions improvements (#325) * Changed the way we read RocketChat settings since setting.type won't be returned from server anymore * Permissions * Unnecessary action sheet render * Update gradle and targetSdkVersion (#328) * Changed the way we read RocketChat settings since setting.type won't be returned from server anymore * Permissions * Unnecessary action sheet render * Update gradle * Switched testServer to use blob * RoomsListHeader search fixed * Runs loadMessagesForRoom only if room has at least 20 rows * - Logout if user's token expired - Removed update avatar logic - Profile dialog border on android * - Animations disabled - CircleCI set * Tests updated * "eventType argument is required" fix * Switch push notification lib (#346) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #342 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * Allow x-instance-id and X-Instance-ID header (#354) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #137 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> Some server configurations may send x-instance-id header with different case. * Image upload improvements (#368) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> - [x] Crop image - [x] Type image description (like web) - [x] Show upload progress - [x] "Try again" in case of error - [x] Cancel upload while in progress - [x] [Android] Zoom on photos <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> ![image](https://user-images.githubusercontent.com/804994/42526934-a12da304-844d-11e8-8668-f3d69369726a.png) ![image](https://user-images.githubusercontent.com/804994/42527829-297945fe-8450-11e8-9f0e-9e668dd33043.png) * [NEW] Room Loading(#372) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * [FIX] Empty room name for livechat (#375) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #320 Closes #209 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * [NEW] Reply preview (#374) * Updated to React Native 0.56 * Reply Preview * [FIX] Close websocket (#379) * Fixed a bug when closing websocket * removeListener fixed * [I18N] Russian translation (#381) [I18N] Russian translation file * [NEW] Icon (#383) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> ![image](https://user-images.githubusercontent.com/804994/43228416-d8af49d6-9037-11e8-8830-a1803932c7fd.png) * [FIX] Android 8 notifications (#382) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #380 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * Added CocoaPods to manage react-native-image-crop-picker (#373) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> react-native-image-crop-picker raised an error when uploading to TestFlight. The lib highly recommends CocoaPods for production builds. * Added single-server to readme (#390) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #386 Closes #295 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * Improve RoomsList render time (#384) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> - [x] Added FlatList.getItemLayout() to improve list render time - [x] Some texts were breaking lines at sidebar - [x] Removed onPress from links at RoomsListView - [x] Added eslint rule to prevent unused styles - [x] Fixed auto focus bug at CreateChannel and NewServer - [x] Fix change server bug - [x] Fixed a bug when resuming in ListServer - [x] I18n fixed - [x] Fixed a bug on actionsheet ref not being created - [x] Reply wasn't showing on Android - [x] Use Notification.Builder.setColor/getColor only after Android SDK 23 - [x] Listen to app state only when inside app - [x] Switched register push token position in order to improve login performance - [x] When deep link changes server, it doesn't refresh rooms list - [x] Added SafeAreaView in all views to improve iPhone X experience - [x] Subpath regex #388 * [NEW] Empty room background (#412) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #398 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> ![aug-09-2018 11-35-32](https://user-images.githubusercontent.com/804994/43906080-cbfadf92-9bc8-11e8-9ac9-44f43d3af023.gif) ![aug-09-2018 11-35-16](https://user-images.githubusercontent.com/804994/43906082-cc19411c-9bc8-11e8-9892-c65c86951a91.gif) ![image](https://user-images.githubusercontent.com/804994/43911366-ad830cd0-9bd5-11e8-8913-6a7e87a2206c.png) * Add roadmap (#406) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #45 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * [NEW] Onboarding (#407) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #392 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> ![aug-07-2018 17-03-50](https://user-images.githubusercontent.com/804994/43799447-f62074dc-9a63-11e8-8aac-bf2c4c5a8a2b.gif) ![aug-07-2018 17-03-35](https://user-images.githubusercontent.com/804994/43799446-f5f84a70-9a63-11e8-8947-265113ae9bf4.gif) ![aug-07-2018 17-03-13](https://user-images.githubusercontent.com/804994/43799445-f5d70ee6-9a63-11e8-94a9-f49c7d69fbba.gif) * [NEW] Updated Logo on Splash screen (#409) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> Closes #399 <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> ![aug-07-2018 17-39-44](https://user-images.githubusercontent.com/804994/43801415-739a0cca-9a69-11e8-8bec-d65f751e6a28.gif) ![aug-07-2018 17-31-12](https://user-images.githubusercontent.com/804994/43801416-73d19bd6-9a69-11e8-90ac-bbc7ddeed938.gif) * [FIX] Only single attachment rendered (#417) * [NEW] Rooms list layout (#413) * RoomsListView layout * Rooms list layout * Sort component * Header icons * Default header colors * Add server dropdown * Close sort dropdown if server dropdown will open * UserItem * Room type icon * Search working * Tests updated * Android layout * Using realm queries instead of array iterates * Animation duration * Fixed render bug * [NEW] Create channel layout (#420) * RoomsListView layout * Rooms list layout * Sort component * Header icons * Default header colors * Add server dropdown * Close sort dropdown if server dropdown will open * UserItem * Room type icon * Search working * Tests updated * Android layout * Using realm queries instead of array iterates * Animation duration * Fixed render bug * - NewMessageView - backButtonTitle always empty - SearchBox created * New create channel layout * Search refactored * loginSuccess dismiss modal * Tests working * [FIX] Open unsupported videos on browser (#422) * 1.1 * Sort/group rooms local only (#425) * Update android api from ci * Sort local only * [FIX] Missing current server (#427) * server.current removed * Increased area of touch on header * Hide search when sort dropdown is tapped * default server icon url * 1.1.1 * [NEW] Experimental Icon (#430) * [NEW] Message layout (#426) * message container/component * Separator component * Reply * Url * tests updated * Minor changes * Audio component * Broadcast button * Minor touches * Reply preview * Edited * Minor bug fixes * - Update roadmap - Bump version to 1.2 * Onboarding styles fix * [FIX] Drawer navigation won't refresh chats (#432) * Avoid errors on Audio/Image/Video (#443) * Bump version to 1.2.1 (#444) * Stop supporting Android 4.4 and lower (#447) * Several fixes for 1.2.1 (#448) * Fix user.roles * Better onLongPress handle on messages * Indicator position * Fix role undefined in system messages * Add baseUrl in case of file attachments * Join room fixed * RoomView params * Broadcast fixes * Add server layout changes * Use native images * Subscribe to not joined channels * Fix alerts without i18n * Tests updated * Bump version to 1.2.2 (#449) * [NEW] Use community JSC for Android (#450) * [NEW] Use community JSC for Android * Quick fix on unread chats * [NEW] Show app version (#454) * [NEW] Portuguese translation (#452) * [NEW] Portuguese translation * Remove servers from sidebar * Update dependencies (#431) * Update dependencies * Lint and test * Added react-native fork * rn 57 * Lint and tests updated * Update xcode on circleci * Use legacy build system * Update tests * Use inline requires (#459) * Update dependencies * Lint and test * Added react-native fork * rn 57 * Lint and tests updated * Update xcode on circleci * Use legacy build system * Update tests * Inline requires * Fix eslint and remove temp gradle * Unnecessary renders * Update isNotch and Readme * Tests updated * Bump version to 1.3.0 (#461) * Better touch handling on rooms list (#462) * Use react-native-gesture-handler at RoomItem * Fixed info message author * Edit message render improvement * Fix ws to http replace * Bump version to 1.3.1 (#463) * Composer layout tweaked (#464) * Composer layout tweaked * Fix localization error * Bump version to 1.3.2 * [FIX] Handle deleted messages (#466) * [FIX] Handle deleted messages * Fix rest error * Fix some connection issues * [FIX] Search rooms (#468) * Bump version to 1.3.3 (#469) * Connecting to DDP badge (#471) * Display custom fields on user info (#476) * Render custom fields on user info * renderCustomFields fix * Display custom fields in user info * Fix lint error * [FIX] DDP badge wasn't hiding on fast connections (#477) * Use Rocket.Chat JS SDK (#481) * JS SDK * API working * Multiple servers * Bump version to 1.4.0 (#482) * [FIX] 2FA and LDAP (#488) * [FIX] Unread rooms group order (#487) * Use grouping setting on temp messages (#486) * [FIX] Delete room error (#485) * Rename to Rocket.Chat Experimental (#483) * Update dependencies (#484) * Bump version to 1.4.0 (#482) * test * one more test * Fix build * Regression: Wait for unmount to delete database after logout (#489) * Bump version to 1.4.1 (#490) * Regression: Crash on Android search (#492) * Bump version to 1.4.2 (#493) * Update Rocket.Chat.js.SDK (#494) * Bump version to v1.4.3 (#495) * [FIX] OAuth (#496) * Smaller header icons inside the room (#499) * [FIX] Logout (#497) * [FIX] Logout * Removed realm instances on rooms list * Bump version to 1.4.4 (#498) * Update navigation library (#501) * v2 * Working on Android 0.57.3 * Drawer working * Removing v1 navigator * - Splash screen - Icons changed * Deeplink * Remove EventEmitter from CreateChannelView * Android search * Android notifications * OAuth * Fix search props * Lint and tests fixed * Fix android build * Improvements on iPhone X* usage * Fix detox * Fix android build * Room.f added to RoomView.shouldComponentUpdate * Animations on RoomsListView and RoomView * Fix topbar buttons on Android * Bump version to 1.5.0 (#503) * Check $FABRIC_KEY availability in CircleCI (#506) * Check $FABRIC_KEY in CircleCI * Remove config scripts * Check $FABRIC_KEY availability in CircleCI for iOS (#507) * [I18n] Add Simplified Chinese(zh-CN) locale (#505) * [FIX] iOS pop gesture not working properly (#509) * Check if lastMessage has an attachment and show "User sent an attachment" at RoomsList (#510) * [FIX] Messages not being loaded properly (#513) * Fetch avatar initials from server (#512) * Fix iOS pop gesture and open sidemenu gesture (#511) * Bump version to 1.5.1 (#516) * [NEW] Room header layout (#521) * Clear iOS notification on resume/open (#520) * [FIX] Flashing avatars on Android after #512 (#519) * [FIX] App connects to previous server instead of the recent added (#518) * [FIX] Room view header crashes when destructuring reducer (#523) * [FIX] Dismiss keyboard on room close (#530) * [FIX] Composer composer's send icon slowness (#528) * [WIP] New Authentication layout (#536) New Authentication layout * Regression: Resend messages with error (#532) * DDP Connection badge animation changed (#533) * [FIX] Upload buttons on Android (#541) * Bump version to 1.6.0 (#543) * I18n: Add missing translation of simplified Chinese (#539) * Update dependencies (#544) * AndroidManifest changes * Regression: Deep linking stopped working after react-native-navigation update (#549) * [FIX] Android stuck on splash screen after hardware back button is pressed (#550) * [FIX] Android stuck on splash screen after hardware button is pressed * Fix empty user at asyncstorage * Remove unused subscribe * [FIX] x-instance-id header prop is case insensitive (#551) * Bump version to 1.6.1 (#553) * [FIX] x-instance-id header prop is case insensitive * Use Rest API calls (#558) * Chats: Don't show group header if none of the filters is selected (#560) * [CHORE] Update Xcode image version on CircleCI (#561) * Bump version to 1.7.0 (#562) * [FIX] Load messages on notification tap (#564) * Use Rest API pt 2 (#568) * Room files * Pinned messages * Starred messages * Mentioned messages * Search messages * Bug fixes * Profile * Livechat * Block/unblock user * Erase room * Archive room * Remove unused method * Bug fix * [CHORE] Add hold step on CircleCI before TestFlight (#572) * [FIX] GET /info to check if it's a valid server instead of x-instance-id (#573) * Bump version to 1.7.1 (#574) * Unnecessary re-renders removed (#570) * shouldComponentUpdate * Rooms list shouldcomponentupdate * RoomView shouldComponentUpdate * Messagebox and Message shouldComponentUpdate * EmojiPicker shouldComponentUpdate * RoomActions shouldComponentUpdate * Room info shouldComponentUpdate * Update RNN * Use only one Flatlist if none group filter is selected * Update fix * shouldComponentUpdate * Bug fixes * ListView changes * Bug fix * render list bug fix * Changes on public channels * - RoomView saga leak removed - Join room e2e tests added * Rest versions * Method call versions * Min RocketChat version alert * Update dependencies (#587) * [FIX] Better message actions (#567) * [FIX] Back button press on message actions (#592) * Bump version to 1.8.0 (#595) * [FIX] LDAP login (#596) * Create class to manage navigation (#594) * Add Navigation class * Place Drawer.js logic inside of Navigation * Load less views at startup * [FIX] v1.8.0 (#599) * Downgrade react-native-fast-image * Update iOS permission usage descriptions * [FIX] Delete upload item * Update JS SDK version (#602) * Add Icons class (#611) Creates Icons class to manage when to load icons from native side or react-native-vector-icons. It also fixes `react-native run-android` #517 * Updating room indicator (#609) Shows "Updating..." when requesting rooms from Rest API. * [FIX] Load avatar on servers that prevent unauthenticated avatar access (#604) App would show an empty space on servers that require authentication on avatar access * [FIX] 2FA login in a server with LDAP enabled (#612) * [FIX] Start loop searching for rooms updates only when connection goes down and SDK has userId (#613) * Allow to create empty channel (#615) * [FIX] Reply title should break text (#616) * Bump version to 1.9.0 (#617) * [FIX] SDK issues (#621) * Remove listeners from room * Properly close connections on change server * Minor layout change on connecting badge * [CHORE] Add TestFlight invite and update Readme (#623) * [FIX] npm -> yarn dependencies migration (#622) * I18n: Add French (#629) * [FIX] Remove rooms listener (#630) * [CHORE] Update issue template (#638) * I18n: Add German (#641) * Bump version to 1.10.0 (#644) * [FIX] Prevent mass is typing dispatchs (#651) * [FIX] Handle database errors properly (#650) * [FIX] Change actions labels (#654) * [FIX] Room members filter (#655) * [FIX] uploadProgress is not a function (#656) * [FIX] Slow messagebox (#658) * Remove drawer (#653) * Remove drawer (layout needs to be changed in future releases, though) * Don't navigate outside on logout if there's other logged server * Update react-native-navigation * Message button (#660) * Remove touchable opacity when scrolling messages * Tap on disable messages closes keyboard * Unify vibration * Vibrate only on Android * [FIX] Fetch rooms date (#662) * [FIX] Select emoji error (#666) * Update Realm to 2.24 (#667) * Update React Native to 0.58.6 (#668) * [FIX] Fix some language issues in German language (#664) * New icons (#643) * New Icons * Remove unused assets * Change send icon * Layout tweaks * Refactor Status * Styles changed * User layout fix * Separator layout changes * Sidebar status layout fix * Fix Message.onLongPress issue * Fix code markdown Closes https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/625 * Status lint * Fix tests * Navigation debounce * RoomActions icons * Space between components * Group text * Update tests * [CHORE] Remove .debug suffix on Android (#681) * [FIX] Fix null native Messagebox component object (#680) * Fix null native Messagebox component object * [iOS] Fix header alignment * Remove unused files * Switch to react-navigation (#687) * Update readme (#714) * Bump to 1.10.1 (#731) * [FIX] Deep linking between multiple logged servers (#730) * Fix handle invisible status (#692) * I18n: Add Portuguese (Portugal) (#722) * [FIX] Show ActivityIndicator in RoomMembersView (#686) * Bump version to 1.11.0 (#761) * Migrate from GCM to FCM (#760) * [NEW] Scrollable room name feature (#756) * [NEW] Scroll down floating button (#735) * [CHORE] Added Storybook documentation (#757) * Use FlatList in RoomView (#762) * [FIX] iOS requiring location permission (#768) * Room item layout (#771) * [NEW] Draft message per room (#772) * [FIX] Add Realm.safeAddListener (#785) * [CHORE] Remove tvOS target (#779) * [NEW] Discussions (#696) * Bump version to 1.12.0 (#804) * [NEW] Threads (#798) * RoomsListView improvements (#819) * [FIX] Giphy not showing (#810) * [FIX] Apply emojify on empty texts (#824) * Lock drawer when stack is not on root screen (#825) * Room item layout (#835) * [FIX] Threads (#838) Closes #826 Closes #827 Closes #828 Closes #829 Closes #830 Closes #831 Closes #832 Closes #833 * [FIX] Smaller thread title (#846) * [FIX] Smaller thread title * Remove markdown notation from thread title * On message press debounce * Align vertical thread title * [Regression] Search stopped working on Android after LastMessage refactor (#851) * Load legal pages from web (#849) * Update fetch permissions api (#850) * Update custom emojis endpoint (#852) * Update emoji endpoint * Use React.memo on Markdown * Support RC versions lower than 0.75.0 * Realm migration * Fetch roles from rest api (#853) * Fetch roles from rest api * Fix RoomInfoView role get * Remove roles from redux * Bump version to 1.13 (#857) * Active users improvements (#855) * Remove connection badge (#862) * Connecting indicator on RoomsListView header * Connecting indicator on RoomView header * Remove ConnectionBadge * Show updating on RoomView load messages * Update dependencies (#863) * Minor updates * Update jsc-android * Update react-native-modal * Minor updates * Update react-native-fast-image * Minor dev updates * Few major updates * Update react-native-keyboard-aware-scroll-view * Update pods * Update android-support * Update tests * Remove duplicated getRoleDescription function (#866) * [FIX] Load local URL image (#871) * [FIX] Toggle/follow thread icon (#867) * Tweaks on sequential threads messages layout (#858) * Tweaks on sequential threads messages * Update tests * Fix quote * Prevent from deleting thread start message when positioned inside the thread * Remove thread listener from RightButtons * Fix error on thread start parse * Stop parsing threads on render * Check replied thread only if necessary * Fix messages don't displaying * Fix threads e2e * RoomsListView.updateState slice * Stop fetching hidden messages on threads * Set initialNumToRender to 5 * [FIX] Check if room is mounted before setting state (#864) * Tweaks on sequential threads messages * Update tests * Fix quote * Prevent from deleting thread start message when positioned inside the thread * Remove thread listener from RightButtons * Fix error on thread start parse * Stop parsing threads on render * Check replied thread only if necessary * Fix messages don't displaying * Fix threads e2e * RoomsListView.updateState slice * Stop fetching hidden messages on threads * Check if RoomView is mounted before rendering * Refactor navigation events on RoomsListView * Fix lint * Fix listener * [FIX] Typing not getting cleared after popping a room (#873) * [CHORE] Remove e2e tests from CI (#875) * [FIX] Remove listeners on RoomView header unmount (#874)
This commit is contained in:
parent
7303bddaef
commit
fe46929238
|
@ -266,9 +266,9 @@ workflows:
|
|||
build-and-test:
|
||||
jobs:
|
||||
- lint-testunit
|
||||
- e2e-test:
|
||||
requires:
|
||||
- lint-testunit
|
||||
# - e2e-test:
|
||||
# requires:
|
||||
# - lint-testunit
|
||||
|
||||
- ios-build:
|
||||
requires:
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"expoServerPort": null,
|
||||
"packagerPort": null,
|
||||
"packagerPid": null
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"hostType": "tunnel",
|
||||
"lanType": "ip",
|
||||
"dev": true,
|
||||
"strict": false,
|
||||
"minify": false,
|
||||
"urlType": "exp",
|
||||
"urlRandomness": null
|
||||
}
|
21
README.md
21
README.md
|
@ -214,3 +214,24 @@ $ detox build --configuration ios.sim.release
|
|||
```bash
|
||||
$ detox test --configuration ios.sim.release
|
||||
```
|
||||
|
||||
## Storybook
|
||||
- Open index.js
|
||||
|
||||
- Uncomment following line
|
||||
|
||||
```bash
|
||||
import './storybook';
|
||||
```
|
||||
|
||||
- Comment out following lines
|
||||
```bash
|
||||
import './app/ReactotronConfig';
|
||||
import { AppRegistry } from 'react-native';
|
||||
import App from './app/index';
|
||||
import { name as appName } from './app.json';
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
```
|
||||
|
||||
- Start your application again
|
|
@ -0,0 +1,26 @@
|
|||
export default class Realm {
|
||||
schema = [];
|
||||
|
||||
data = [];
|
||||
|
||||
constructor(params) {
|
||||
require('lodash').each(params.schema, (schema) => {
|
||||
this.data[schema.name] = [];
|
||||
this.data[schema.name].filtered = () => this.data[schema.name];
|
||||
});
|
||||
this.schema = params.schema;
|
||||
}
|
||||
|
||||
objects(schemaName) {
|
||||
return this.data[schemaName];
|
||||
}
|
||||
|
||||
write = (fn) => {
|
||||
fn();
|
||||
}
|
||||
|
||||
create(schemaName, data) {
|
||||
this.data[schemaName].push(data);
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import {View} from 'react-native';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { createStore, combineReducers } from 'redux';
|
||||
|
||||
const reducers = combineReducers({login:() => ({user: {}}), settings:() => ({}), meteor: () => ({ connected: true })});
|
||||
const store = createStore(reducers);
|
||||
|
||||
import React from 'react';
|
||||
import RoomItem from '../app/presentation/RoomItem';
|
||||
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
const date = '2017-10-10T10:00:00Z';
|
||||
const onPress = () => {};
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" baseUrl="baseUrl" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render unread', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1} /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render unread +999', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="d" _updatedAt={date} name="name" unread={1000} /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render no icon', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="X" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render private group', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="g" _updatedAt={date} name="private-group" /> </View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render channel', () => {
|
||||
expect(renderer.create(<Provider store={store}><View><RoomItem onPress={onPress} type="c" _updatedAt={date} name="general" /></View></Provider>).toJSON()).toMatchSnapshot();
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`render channel 1`] = `
|
||||
<View>
|
||||
View
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`render no icon 1`] = `
|
||||
<View>
|
||||
View
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`render private group 1`] = `
|
||||
<View>
|
||||
View
|
||||
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`render unread +999 1`] = `
|
||||
<View>
|
||||
View
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`render unread 1`] = `
|
||||
<View>
|
||||
View
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<View>
|
||||
View
|
||||
</View>
|
||||
`;
|
File diff suppressed because it is too large
Load Diff
|
@ -103,7 +103,7 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "1.10.0"
|
||||
versionName "1.13.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
@ -180,7 +180,22 @@ repositories {
|
|||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'org.webkit:android-jsc:r225067'
|
||||
force 'org.webkit:android-jsc:r241213'
|
||||
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-tasks') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-stats') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-basement') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,21 +214,23 @@ dependencies {
|
|||
implementation project(':react-native-video')
|
||||
implementation project(':react-native-vector-icons')
|
||||
implementation project(':rn-fetch-blob')
|
||||
implementation project(':@remobile/react-native-toast')
|
||||
implementation project(':react-native-toast')
|
||||
implementation project(':react-native-fast-image')
|
||||
implementation project(':realm')
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "com.android.support:appcompat-v7:27.1.1"
|
||||
implementation "com.android.support:support-v4:27.1.1"
|
||||
implementation 'com.android.support:customtabs:27.1.1'
|
||||
implementation 'com.android.support:design:27.1.1'
|
||||
implementation 'org.webkit:android-jsc-cppruntime:+'
|
||||
implementation "com.android.support:appcompat-v7:28.0.0"
|
||||
implementation "com.android.support:support-v4:28.0.0"
|
||||
implementation 'com.android.support:customtabs:28.0.0'
|
||||
implementation 'com.android.support:design:28.0.0'
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation 'com.facebook.fresco:fresco:1.10.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:1.10.0'
|
||||
implementation 'com.facebook.fresco:animated-webp:1.10.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:1.10.0'
|
||||
implementation 'com.google.android.gms:play-services-gcm:16.1.0'
|
||||
implementation "com.google.firebase:firebase-core:16.0.1"
|
||||
implementation "com.google.firebase:firebase-messaging:17.3.4"
|
||||
implementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') {
|
||||
transitive = true;
|
||||
}
|
||||
|
@ -225,3 +242,6 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
|||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true
|
|
@ -0,0 +1,245 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "673693445664",
|
||||
"firebase_url": "https://rocketchat-9e9be.firebaseio.com",
|
||||
"project_id": "rocketchat-9e9be",
|
||||
"storage_bucket": "rocketchat-9e9be.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:6ef4638e500ec958",
|
||||
"android_client_info": {
|
||||
"package_name": "RocketChat"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:16da2e50aff9f0c9",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.android"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-hrjftksij02vqtd467ln2cubvu48ft5j.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android",
|
||||
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-k0mvosdjoe5dbvqce3b377ckabb5dgu8.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android",
|
||||
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 2,
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.ios",
|
||||
"app_store_id": "1148741252"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:1551054db195f705",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.android.dev"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-t5aeku0oie010npd40a0tgn27c418vk7.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android.dev",
|
||||
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-iml14ln4vccuu7liclrpt2k671fkjs38.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android.dev",
|
||||
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 2,
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.ios",
|
||||
"app_store_id": "1148741252"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:8be27b1f7c42a2ed",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.reactnative"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:64932c99863e2838",
|
||||
"android_client_info": {
|
||||
"package_name": "com.konecty.rocket.chat"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-3ajben08beuco6eout3kpod2gbbm8fij.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.konecty.rocket.chat",
|
||||
"certificate_hash": "cd5806ba3f0141d0f2e47acfe64a485f575108ab"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 2,
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.ios",
|
||||
"app_store_id": "1148741252"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
|
@ -9,15 +9,6 @@
|
|||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.C2D_MESSAGE"
|
||||
android:protectionLevel="signature" />
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="21"
|
||||
android:targetSdkVersion="27" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
|
@ -45,8 +36,6 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
||||
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="673693445664\0"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
|
@ -4,12 +4,11 @@ buildscript {
|
|||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
// mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
// classpath 'com.android.tools.build:gradle:2.2.3'
|
||||
classpath 'com.android.tools.build:gradle:3.1.0'
|
||||
classpath 'com.google.gms:google-services:4.0.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -19,12 +18,8 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
// mavenCentral()
|
||||
google()
|
||||
jcenter()
|
||||
// maven {
|
||||
// url 'https://maven.google.com'
|
||||
// }
|
||||
maven { url "https://jitpack.io" }
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
|
|
|
@ -11,8 +11,8 @@ include ':react-native-device-info'
|
|||
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
|
||||
include ':react-native-gesture-handler'
|
||||
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
|
||||
include ':@remobile/react-native-toast'
|
||||
project(':@remobile/react-native-toast').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-toast/android')
|
||||
include ':react-native-toast'
|
||||
project(':react-native-toast').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-toast/android')
|
||||
include ':rn-fetch-blob'
|
||||
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
|
||||
include ':react-native-image-crop-picker'
|
||||
|
|
|
@ -26,18 +26,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
|||
'OPEN_SEARCH_HEADER',
|
||||
'CLOSE_SEARCH_HEADER'
|
||||
]);
|
||||
export const ROOM = createRequestTypes('ROOM', [
|
||||
'ADD_USER_TYPING',
|
||||
'REMOVE_USER_TYPING',
|
||||
'SOMEONE_TYPING',
|
||||
'OPEN',
|
||||
'CLOSE',
|
||||
'LEAVE',
|
||||
'ERASE',
|
||||
'USER_TYPING',
|
||||
'MESSAGE_RECEIVED',
|
||||
'SET_LAST_OPEN'
|
||||
]);
|
||||
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']);
|
||||
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']);
|
||||
export const MESSAGES = createRequestTypes('MESSAGES', [
|
||||
...defaultTypes,
|
||||
|
@ -75,8 +64,6 @@ export const SERVER = createRequestTypes('SERVER', [
|
|||
]);
|
||||
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
|
||||
export const LOGOUT = 'LOGOUT'; // logout is always success
|
||||
export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET']);
|
||||
export const ROLES = createRequestTypes('ROLES', ['SET']);
|
||||
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
|
||||
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
||||
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function setActiveUser(data) {
|
||||
return {
|
||||
type: types.ACTIVE_USERS.SET,
|
||||
data
|
||||
};
|
||||
}
|
|
@ -1,25 +1,5 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function messagesRequest(room) {
|
||||
return {
|
||||
type: types.MESSAGES.REQUEST,
|
||||
room
|
||||
};
|
||||
}
|
||||
|
||||
export function messagesSuccess() {
|
||||
return {
|
||||
type: types.MESSAGES.SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
export function messagesFailure(err) {
|
||||
return {
|
||||
type: types.MESSAGES.FAILURE,
|
||||
err
|
||||
};
|
||||
}
|
||||
|
||||
export function actionsShow(actionMessage) {
|
||||
return {
|
||||
type: types.MESSAGES.ACTIONS_SHOW,
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function setRoles(data) {
|
||||
return {
|
||||
type: types.ROLES.SET,
|
||||
data
|
||||
};
|
||||
}
|
|
@ -1,40 +1,5 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
|
||||
export function removeUserTyping(username) {
|
||||
return {
|
||||
type: types.ROOM.REMOVE_USER_TYPING,
|
||||
username
|
||||
};
|
||||
}
|
||||
|
||||
export function someoneTyping(data) {
|
||||
return {
|
||||
type: types.ROOM.SOMEONE_TYPING,
|
||||
...data
|
||||
};
|
||||
}
|
||||
|
||||
export function addUserTyping(username) {
|
||||
return {
|
||||
type: types.ROOM.ADD_USER_TYPING,
|
||||
username
|
||||
};
|
||||
}
|
||||
|
||||
export function openRoom(room) {
|
||||
return {
|
||||
type: types.ROOM.OPEN,
|
||||
room
|
||||
};
|
||||
}
|
||||
|
||||
export function closeRoom() {
|
||||
return {
|
||||
type: types.ROOM.CLOSE
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveRoom(rid, t) {
|
||||
return {
|
||||
type: types.ROOM.LEAVE,
|
||||
|
@ -51,23 +16,10 @@ export function eraseRoom(rid, t) {
|
|||
};
|
||||
}
|
||||
|
||||
export function userTyping(status = true) {
|
||||
export function userTyping(rid, status = true) {
|
||||
return {
|
||||
type: types.ROOM.USER_TYPING,
|
||||
rid,
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
export function roomMessageReceived(message) {
|
||||
return {
|
||||
type: types.ROOM.MESSAGE_RECEIVED,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
export function setLastOpen(date = new Date()) {
|
||||
return {
|
||||
type: types.ROOM.SET_LAST_OPEN,
|
||||
date
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { SERVER } from './actionsTypes';
|
||||
|
||||
export function selectServerRequest(server) {
|
||||
export function selectServerRequest(server, version, fetchVersion = true) {
|
||||
return {
|
||||
type: SERVER.SELECT_REQUEST,
|
||||
server
|
||||
server,
|
||||
version,
|
||||
fetchVersion
|
||||
};
|
||||
}
|
||||
|
||||
export function selectServerSuccess(server) {
|
||||
export function selectServerSuccess(server, version) {
|
||||
return {
|
||||
type: SERVER.SELECT_SUCCESS,
|
||||
server
|
||||
server,
|
||||
version
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import { isIOS } from '../utils/deviceInfo';
|
||||
|
||||
export const COLOR_DANGER = '#f5455c';
|
||||
export const COLOR_BUTTON_PRIMARY = '#1d74f5';
|
||||
export const COLOR_TEXT = '#292E35';
|
||||
export const COLOR_SEPARATOR = '#CBCED1';
|
||||
export const COLOR_SUCCESS = '#2de0a5';
|
||||
export const COLOR_PRIMARY = '#1d74f5';
|
||||
export const COLOR_WHITE = '#fff';
|
||||
export const COLOR_BUTTON_PRIMARY = COLOR_PRIMARY;
|
||||
export const COLOR_TITLE = '#0C0D0F';
|
||||
export const COLOR_TEXT = '#2F343D';
|
||||
export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
|
||||
export const COLOR_SEPARATOR = '#A7A7AA';
|
||||
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
|
||||
export const COLOR_BORDER = '#e1e5e8';
|
||||
export const COLOR_UNREAD = '#e1e5e8';
|
||||
export const STATUS_COLORS = {
|
||||
online: '#2de0a5',
|
||||
busy: COLOR_DANGER,
|
||||
|
@ -11,6 +19,6 @@ export const STATUS_COLORS = {
|
|||
offline: '#cbced1'
|
||||
};
|
||||
|
||||
export const HEADER_BACKGROUND = isIOS ? '#FFF' : '#2F343D';
|
||||
export const HEADER_TITLE = isIOS ? '#0C0D0F' : '#FFF';
|
||||
export const HEADER_BACK = isIOS ? '#1d74f5' : '#FFF';
|
||||
export const HEADER_BACKGROUND = isIOS ? '#f8f8f8' : '#2F343D';
|
||||
export const HEADER_TITLE = isIOS ? COLOR_TITLE : COLOR_WHITE;
|
||||
export const HEADER_BACK = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
export default [
|
||||
'add-user-to-any-c-room',
|
||||
'add-user-to-any-p-room',
|
||||
'add-user-to-joined-room',
|
||||
'archive-room',
|
||||
'delete-c',
|
||||
'delete-message',
|
||||
'delete-p',
|
||||
'edit-message',
|
||||
'edit-room',
|
||||
'force-delete-message',
|
||||
'mute-user',
|
||||
'set-react-when-readonly',
|
||||
'set-readonly',
|
||||
'unarchive-room',
|
||||
'view-broadcast-member-list'
|
||||
];
|
|
@ -14,12 +14,6 @@ export default {
|
|||
CROWD_Enable: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Layout_Privacy_Policy: {
|
||||
type: 'valueAsString'
|
||||
},
|
||||
Layout_Terms_of_Service: {
|
||||
type: 'valueAsString'
|
||||
},
|
||||
LDAP_Enable: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
|
@ -61,5 +55,8 @@ export default {
|
|||
},
|
||||
Assets_favicon_512: {
|
||||
type: null
|
||||
},
|
||||
Threads_enabled: {
|
||||
type: null
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,73 +1,69 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, ViewPropTypes } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
export default class Avatar extends PureComponent {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
style: ViewPropTypes.style,
|
||||
text: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
borderRadius: PropTypes.number,
|
||||
type: PropTypes.string,
|
||||
children: PropTypes.object,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
})
|
||||
const Avatar = React.memo(({
|
||||
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token
|
||||
}) => {
|
||||
const avatarStyle = {
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius
|
||||
};
|
||||
|
||||
if (!text && !avatar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
size: 25,
|
||||
type: 'd',
|
||||
borderRadius: 4
|
||||
const room = type === 'd' ? text : `@${ text }`;
|
||||
|
||||
// Avoid requesting several sizes by having only two sizes on cache
|
||||
const uriSize = size === 100 ? 100 : 50;
|
||||
|
||||
let avatarAuthURLFragment = '';
|
||||
if (userId && token) {
|
||||
avatarAuthURLFragment = `&rc_token=${ token }&rc_uid=${ userId }`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
text, size, baseUrl, borderRadius, style, avatar, type, children, user
|
||||
} = this.props;
|
||||
const uri = avatar || `${ baseUrl }/avatar/${ room }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`;
|
||||
|
||||
const avatarStyle = {
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius
|
||||
};
|
||||
const image = (
|
||||
<FastImage
|
||||
style={avatarStyle}
|
||||
source={{
|
||||
uri,
|
||||
priority: FastImage.priority.high
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!text && !avatar) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={[avatarStyle, style]}>
|
||||
{image}
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const room = type === 'd' ? text : `@${ text }`;
|
||||
Avatar.propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
style: ViewPropTypes.style,
|
||||
text: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
borderRadius: PropTypes.number,
|
||||
type: PropTypes.string,
|
||||
children: PropTypes.object,
|
||||
userId: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
};
|
||||
|
||||
// Avoid requesting several sizes by having only two sizes on cache
|
||||
const uriSize = size === 100 ? 100 : 50;
|
||||
Avatar.defaultProps = {
|
||||
text: '',
|
||||
size: 25,
|
||||
type: 'd',
|
||||
borderRadius: 4
|
||||
};
|
||||
|
||||
let avatarAuthURLFragment = '';
|
||||
if (user && user.id && user.token) {
|
||||
avatarAuthURLFragment = `&rc_token=${ user.token }&rc_uid=${ user.id }`;
|
||||
}
|
||||
|
||||
const uri = avatar || `${ baseUrl }/avatar/${ room }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`;
|
||||
|
||||
const image = (
|
||||
<FastImage
|
||||
style={avatarStyle}
|
||||
source={{
|
||||
uri,
|
||||
priority: FastImage.priority.high
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[avatarStyle, style]}>
|
||||
{image}
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Avatar;
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import {
|
||||
Text, StyleSheet, ActivityIndicator, Animated, Easing
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import I18n from '../i18n';
|
||||
import debounce from '../utils/debounce';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: 41,
|
||||
backgroundColor: '#F7F8FA',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
elevation: 4
|
||||
},
|
||||
text: {
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
textConnecting: {
|
||||
color: '#9EA2A8'
|
||||
},
|
||||
containerConnected: {
|
||||
backgroundColor: '#2de0a5'
|
||||
},
|
||||
containerOffline: {
|
||||
backgroundColor: '#f5455c'
|
||||
},
|
||||
activityIndicator: {
|
||||
marginRight: 15
|
||||
}
|
||||
});
|
||||
|
||||
const ANIMATION_DURATION = 300;
|
||||
|
||||
@connect(state => ({
|
||||
connecting: state.meteor.connecting,
|
||||
connected: state.meteor.connected,
|
||||
disconnected: !state.meteor.connecting && !state.meteor.connected
|
||||
}))
|
||||
class ConnectionBadge extends Component {
|
||||
static propTypes = {
|
||||
connecting: PropTypes.bool,
|
||||
connected: PropTypes.bool,
|
||||
disconnected: PropTypes.bool
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.animatedValue = new Animated.Value(0);
|
||||
if (props.connecting) {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { connected, disconnected } = this.props;
|
||||
this.show(connected || disconnected);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
animate = debounce((toValue, autoHide) => {
|
||||
Animated.timing(
|
||||
this.animatedValue,
|
||||
{
|
||||
toValue,
|
||||
duration: ANIMATION_DURATION,
|
||||
easing: Easing.ease,
|
||||
useNativeDriver: true
|
||||
},
|
||||
).start(() => {
|
||||
if (toValue === 1 && autoHide) {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
this.timeout = setTimeout(() => {
|
||||
this.hide();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
|
||||
show = (autoHide) => {
|
||||
this.animate(1, autoHide);
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
this.animate(0);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { connecting, connected } = this.props;
|
||||
|
||||
const translateY = this.animatedValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-42, 0]
|
||||
});
|
||||
|
||||
if (connecting) {
|
||||
return (
|
||||
<Animated.View style={[styles.container, { transform: [{ translateY }] }]}>
|
||||
<ActivityIndicator color='#9EA2A8' style={styles.activityIndicator} />
|
||||
<Text style={[styles.text, styles.textConnecting]}>{I18n.t('Connecting')}</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
} else if (connected) {
|
||||
return (
|
||||
<Animated.View style={[styles.container, styles.containerConnected, { transform: [{ translateY }] }]}>
|
||||
<Text style={styles.text}>{I18n.t('Connected')}</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, styles.containerOffline, { transform: [{ translateY }] }]}>
|
||||
<Text style={styles.text}>{I18n.t('Offline')}</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ConnectionBadge;
|
|
@ -10,7 +10,7 @@ import TabBar from './TabBar';
|
|||
import EmojiCategory from './EmojiCategory';
|
||||
import styles from './styles';
|
||||
import categories from './categories';
|
||||
import database from '../../lib/realm';
|
||||
import database, { safeAddListener } from '../../lib/realm';
|
||||
import { emojisByCategory } from '../../emojis';
|
||||
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
|
||||
|
||||
|
@ -45,8 +45,8 @@ export default class EmojiPicker extends Component {
|
|||
this.updateFrequentlyUsed();
|
||||
this.updateCustomEmojis();
|
||||
requestAnimationFrame(() => this.setState({ show: true }));
|
||||
this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
|
||||
this.customEmojis.addListener(this.updateCustomEmojis);
|
||||
safeAddListener(this.frequentlyUsed, this.updateFrequentlyUsed);
|
||||
safeAddListener(this.customEmojis, this.updateCustomEmojis);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import { COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
export default StyleSheet.create({
|
||||
background: {
|
||||
backgroundColor: '#fff'
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
container: {
|
||||
flex: 1
|
||||
|
@ -27,7 +28,7 @@ export default StyleSheet.create({
|
|||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
backgroundColor: '#007aff',
|
||||
backgroundColor: COLOR_PRIMARY,
|
||||
bottom: 0
|
||||
},
|
||||
tabLine: {
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text } from 'react-native';
|
||||
import HeaderButtons, { HeaderButton, Item } from 'react-navigation-header-buttons';
|
||||
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors';
|
||||
|
||||
const color = isIOS ? '#1D74F5' : '#FFF';
|
||||
const color = isIOS ? COLOR_PRIMARY : COLOR_WHITE;
|
||||
export const headerIconSize = 23;
|
||||
|
||||
const CustomHeaderButton = React.memo(props => (
|
||||
<HeaderButton {...props} IconComponent={CustomIcon} iconSize={23} color={color} />
|
||||
<HeaderButton {...props} IconComponent={CustomIcon} iconSize={headerIconSize} color={color} />
|
||||
));
|
||||
|
||||
export const CustomHeaderButtons = React.memo(props => (
|
||||
|
@ -59,5 +60,3 @@ LegalButton.propTypes = {
|
|||
};
|
||||
|
||||
export { Item };
|
||||
|
||||
export default () => <Text>a</Text>;
|
||||
|
|
|
@ -198,6 +198,11 @@ export default class MessageActions extends React.Component {
|
|||
if (this.isRoomReadOnly()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent from deleting thread start message when positioned inside the thread
|
||||
if (props.tmid && props.tmid === props.actionMessage._id) {
|
||||
return false;
|
||||
}
|
||||
const deleteOwn = this.isOwn(props);
|
||||
const { Message_AllowDeleting: isDeleteAllowed, Message_AllowDeleting_BlockDeleteInMinutes } = this.props;
|
||||
if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import styles from './styles';
|
|||
import I18n from '../../i18n';
|
||||
import { isIOS, isAndroid } from '../../utils/deviceInfo';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_SUCCESS, COLOR_DANGER } from '../../constants/colors';
|
||||
|
||||
export const _formatTime = function(seconds) {
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
|
@ -130,7 +131,7 @@ export default class extends React.PureComponent {
|
|||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color='#f5455c'
|
||||
color={COLOR_DANGER}
|
||||
name='cross'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
|
@ -143,7 +144,7 @@ export default class extends React.PureComponent {
|
|||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color='#2de0a5'
|
||||
color={COLOR_SUCCESS}
|
||||
name='check'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
|
|
|
@ -6,17 +6,21 @@ import { connect } from 'react-redux';
|
|||
|
||||
import Markdown from '../message/Markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_TEXT_DESCRIPTION, COLOR_WHITE
|
||||
} from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
backgroundColor: '#fff'
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
messageContainer: {
|
||||
flex: 1,
|
||||
marginHorizontal: 10,
|
||||
backgroundColor: '#F3F4F5',
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 4
|
||||
|
@ -26,15 +30,17 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center'
|
||||
},
|
||||
username: {
|
||||
color: '#1D74F5',
|
||||
color: COLOR_PRIMARY,
|
||||
fontSize: 16,
|
||||
fontWeight: '500'
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
time: {
|
||||
color: '#9EA2A8',
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
marginLeft: 5
|
||||
marginLeft: 6,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
},
|
||||
close: {
|
||||
marginRight: 10
|
||||
|
@ -79,7 +85,7 @@ export default class ReplyPreview extends Component {
|
|||
</View>
|
||||
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
|
||||
</View>
|
||||
<CustomIcon name='cross' color='#9ea2a8' size={20} style={styles.close} onPress={this.close} />
|
||||
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@ import Button from '../Button';
|
|||
import I18n from '../../i18n';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const cancelButtonColor = '#f7f8fa';
|
||||
const cancelButtonColor = COLOR_BACKGROUND_CONTAINER;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modal: {
|
||||
|
@ -25,11 +26,13 @@ const styles = StyleSheet.create({
|
|||
paddingTop: 16
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorTitle,
|
||||
...sharedStyles.textBold
|
||||
},
|
||||
container: {
|
||||
height: 430,
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: COLOR_WHITE,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
scrollView: {
|
||||
|
@ -46,7 +49,7 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
backgroundColor: '#f7f8fa'
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER
|
||||
},
|
||||
button: {
|
||||
marginBottom: 0
|
||||
|
@ -149,15 +152,15 @@ export default class UploadModal extends Component {
|
|||
underlayColor={cancelButtonColor}
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: '#1d74f5' }]}>{I18n.t('Cancel')}</Text>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: COLOR_PRIMARY }]}>{I18n.t('Cancel')}</Text>
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
onPress={this.submit}
|
||||
style={[styles.androidButton, { backgroundColor: '#1d74f5' }]}
|
||||
underlayColor='#1d74f5'
|
||||
style={[styles.androidButton, { backgroundColor: COLOR_PRIMARY }]}
|
||||
underlayColor={COLOR_PRIMARY}
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: '#fff' }]}>{I18n.t('Send')}</Text>
|
||||
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: COLOR_WHITE }]}>{I18n.t('Send')}</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -31,12 +31,11 @@ import I18n from '../../i18n';
|
|||
import ReplyPreview from './ReplyPreview';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { COLOR_PRIMARY, COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
|
||||
|
||||
const MENTIONS_TRACKING_TYPE_USERS = '@';
|
||||
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
|
||||
|
||||
const BLUE_COLOR = '#1D74F5';
|
||||
|
||||
const onlyUnique = function onlyUnique(value, index, self) {
|
||||
return self.indexOf(({ _id }) => value._id === _id) === index;
|
||||
};
|
||||
|
@ -49,25 +48,7 @@ const imagePickerConfig = {
|
|||
cropperCancelText: I18n.t('Cancel')
|
||||
};
|
||||
|
||||
@connect(state => ({
|
||||
roomType: state.room.t,
|
||||
message: state.messages.message,
|
||||
replyMessage: state.messages.replyMessage,
|
||||
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg,
|
||||
editing: state.messages.editing,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
user: {
|
||||
id: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
token: state.login.user && state.login.user.token
|
||||
}
|
||||
}), dispatch => ({
|
||||
editCancel: () => dispatch(editCancelAction()),
|
||||
editRequest: message => dispatch(editRequestAction(message)),
|
||||
typing: status => dispatch(userTypingAction(status)),
|
||||
closeReply: () => dispatch(replyCancelAction())
|
||||
}))
|
||||
export default class MessageBox extends Component {
|
||||
class MessageBox extends Component {
|
||||
static propTypes = {
|
||||
rid: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
|
@ -75,12 +56,15 @@ export default class MessageBox extends Component {
|
|||
replyMessage: PropTypes.object,
|
||||
replying: PropTypes.bool,
|
||||
editing: PropTypes.bool,
|
||||
threadsEnabled: PropTypes.bool,
|
||||
isFocused: PropTypes.bool,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
}),
|
||||
roomType: PropTypes.string,
|
||||
tmid: PropTypes.string,
|
||||
editCancel: PropTypes.func.isRequired,
|
||||
editRequest: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
|
@ -109,12 +93,38 @@ export default class MessageBox extends Component {
|
|||
this.text = '';
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { rid, tmid } = this.props;
|
||||
let msg;
|
||||
if (tmid) {
|
||||
const thread = database.objectForPrimaryKey('threads', tmid);
|
||||
if (thread) {
|
||||
msg = thread.draftMessage;
|
||||
}
|
||||
} else {
|
||||
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||
if (room) {
|
||||
msg = room.draftMessage;
|
||||
}
|
||||
}
|
||||
if (msg) {
|
||||
this.setInput(msg);
|
||||
this.setShowSend(true);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { message, replyMessage } = this.props;
|
||||
if (message !== nextProps.message && nextProps.message.msg) {
|
||||
const { message, replyMessage, isFocused } = this.props;
|
||||
if (!isFocused) {
|
||||
return;
|
||||
}
|
||||
if (!equal(message, nextProps.message) && nextProps.message.msg) {
|
||||
this.setInput(nextProps.message.msg);
|
||||
if (this.text) {
|
||||
this.setShowSend(true);
|
||||
}
|
||||
this.focus();
|
||||
} else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
|
||||
} else if (!equal(replyMessage, nextProps.replyMessage)) {
|
||||
this.focus();
|
||||
} else if (!nextProps.message) {
|
||||
this.clearInput();
|
||||
|
@ -126,8 +136,11 @@ export default class MessageBox extends Component {
|
|||
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
|
||||
} = this.state;
|
||||
const {
|
||||
roomType, replying, editing
|
||||
roomType, replying, editing, isFocused
|
||||
} = this.props;
|
||||
if (!isFocused) {
|
||||
return false;
|
||||
}
|
||||
if (nextProps.roomType !== roomType) {
|
||||
return true;
|
||||
}
|
||||
|
@ -241,7 +254,7 @@ export default class MessageBox extends Component {
|
|||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={BLUE_COLOR}
|
||||
color={COLOR_PRIMARY}
|
||||
name='cross'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
|
@ -258,7 +271,7 @@ export default class MessageBox extends Component {
|
|||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={BLUE_COLOR}
|
||||
color={COLOR_PRIMARY}
|
||||
name='emoji'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
|
@ -273,7 +286,7 @@ export default class MessageBox extends Component {
|
|||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={BLUE_COLOR}
|
||||
color={COLOR_PRIMARY}
|
||||
name='keyboard'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
|
@ -294,7 +307,7 @@ export default class MessageBox extends Component {
|
|||
accessibilityLabel={I18n.t('Send message')}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name='send1' size={23} color={BLUE_COLOR} />
|
||||
<CustomIcon name='send1' size={23} color={COLOR_PRIMARY} />
|
||||
</BorderlessButton>
|
||||
);
|
||||
return icons;
|
||||
|
@ -308,7 +321,7 @@ export default class MessageBox extends Component {
|
|||
accessibilityLabel={I18n.t('Send audio message')}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name='mic' size={23} color={BLUE_COLOR} />
|
||||
<CustomIcon name='mic' size={23} color={COLOR_PRIMARY} />
|
||||
</BorderlessButton>
|
||||
);
|
||||
icons.push(
|
||||
|
@ -320,7 +333,7 @@ export default class MessageBox extends Component {
|
|||
accessibilityLabel={I18n.t('Message actions')}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name='plus' size={23} color={BLUE_COLOR} />
|
||||
<CustomIcon name='plus' size={23} color={COLOR_PRIMARY} />
|
||||
</BorderlessButton>
|
||||
);
|
||||
return icons;
|
||||
|
@ -446,13 +459,13 @@ export default class MessageBox extends Component {
|
|||
}
|
||||
|
||||
handleTyping = (isTyping) => {
|
||||
const { typing } = this.props;
|
||||
const { typing, rid } = this.props;
|
||||
if (!isTyping) {
|
||||
if (this.typingTimeout) {
|
||||
clearTimeout(this.typingTimeout);
|
||||
this.typingTimeout = false;
|
||||
}
|
||||
typing(false);
|
||||
typing(rid, false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -461,7 +474,7 @@ export default class MessageBox extends Component {
|
|||
}
|
||||
|
||||
this.typingTimeout = setTimeout(() => {
|
||||
typing(true);
|
||||
typing(rid, true);
|
||||
this.typingTimeout = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
@ -487,7 +500,7 @@ export default class MessageBox extends Component {
|
|||
}
|
||||
|
||||
sendImageMessage = async(file) => {
|
||||
const { rid } = this.props;
|
||||
const { rid, tmid } = this.props;
|
||||
|
||||
this.setState({ file: { isVisible: false } });
|
||||
const fileInfo = {
|
||||
|
@ -499,7 +512,7 @@ export default class MessageBox extends Component {
|
|||
path: file.path
|
||||
};
|
||||
try {
|
||||
await RocketChat.sendFileMessage(rid, fileInfo);
|
||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
|
||||
} catch (e) {
|
||||
log('sendImageMessage', e);
|
||||
}
|
||||
|
@ -545,14 +558,14 @@ export default class MessageBox extends Component {
|
|||
}
|
||||
|
||||
finishAudioMessage = async(fileInfo) => {
|
||||
const { rid } = this.props;
|
||||
const { rid, tmid } = this.props;
|
||||
|
||||
this.setState({
|
||||
recording: false
|
||||
});
|
||||
if (fileInfo) {
|
||||
try {
|
||||
await RocketChat.sendFileMessage(rid, fileInfo);
|
||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
|
||||
} catch (e) {
|
||||
if (e && e.error === 'error-file-too-large') {
|
||||
return Alert.alert(I18n.t(e.error));
|
||||
|
@ -578,31 +591,42 @@ export default class MessageBox extends Component {
|
|||
if (message.trim() === '') {
|
||||
return;
|
||||
}
|
||||
// if is editing a message
|
||||
|
||||
const {
|
||||
editing, replying
|
||||
} = this.props;
|
||||
|
||||
// Edit
|
||||
if (editing) {
|
||||
const { _id, rid } = editingMessage;
|
||||
editRequest({ _id, msg: message, rid });
|
||||
|
||||
// Reply
|
||||
} else if (replying) {
|
||||
const {
|
||||
user, replyMessage, roomType, closeReply
|
||||
} = this.props;
|
||||
const permalink = await this.getPermalink(replyMessage);
|
||||
let msg = `[ ](${ permalink }) `;
|
||||
const { replyMessage, closeReply, threadsEnabled } = this.props;
|
||||
|
||||
// if original message wasn't sent by current user and neither from a direct room
|
||||
if (user.username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) {
|
||||
msg += `@${ replyMessage.u.username } `;
|
||||
// Thread
|
||||
if (threadsEnabled && replyMessage.mention) {
|
||||
onSubmit(message, replyMessage._id);
|
||||
|
||||
// Legacy reply or quote (quote is a reply without mention)
|
||||
} else {
|
||||
const { user, roomType } = this.props;
|
||||
const permalink = await this.getPermalink(replyMessage);
|
||||
let msg = `[ ](${ permalink }) `;
|
||||
|
||||
// if original message wasn't sent by current user and neither from a direct room
|
||||
if (user.username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) {
|
||||
msg += `@${ replyMessage.u.username } `;
|
||||
}
|
||||
|
||||
msg = `${ msg } ${ message }`;
|
||||
onSubmit(msg);
|
||||
}
|
||||
|
||||
msg = `${ msg } ${ message }`;
|
||||
onSubmit(msg);
|
||||
closeReply();
|
||||
|
||||
// Normal message
|
||||
} else {
|
||||
// if is submiting a new message
|
||||
onSubmit(message);
|
||||
}
|
||||
this.clearInput();
|
||||
|
@ -648,7 +672,7 @@ export default class MessageBox extends Component {
|
|||
onPress={() => this.onPressMention(item)}
|
||||
>
|
||||
<Text style={styles.fixedMentionAvatar}>{item.username}</Text>
|
||||
<Text>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
|
||||
<Text style={styles.mentionText}>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
||||
|
@ -691,7 +715,7 @@ export default class MessageBox extends Component {
|
|||
{trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
|
||||
? [
|
||||
this.renderMentionEmoji(item),
|
||||
<Text key='mention-item-name'>:{ item.name || item }:</Text>
|
||||
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
|
||||
]
|
||||
: [
|
||||
<Avatar
|
||||
|
@ -701,9 +725,10 @@ export default class MessageBox extends Component {
|
|||
size={30}
|
||||
type={item.username ? 'd' : 'c'}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>,
|
||||
<Text key='mention-item-name'>{ item.username || item.name }</Text>
|
||||
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
|
||||
]
|
||||
}
|
||||
</TouchableOpacity>
|
||||
|
@ -782,7 +807,7 @@ export default class MessageBox extends Component {
|
|||
underlineColorAndroid='transparent'
|
||||
defaultValue=''
|
||||
multiline
|
||||
placeholderTextColor='#9ea2a8'
|
||||
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
|
||||
testID='messagebox-input'
|
||||
/>
|
||||
{this.rightButtons}
|
||||
|
@ -820,3 +845,26 @@ export default class MessageBox extends Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: state.messages.message,
|
||||
replyMessage: state.messages.replyMessage,
|
||||
replying: state.messages.replying,
|
||||
editing: state.messages.editing,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
threadsEnabled: state.settings.Threads_enabled,
|
||||
user: {
|
||||
id: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
token: state.login.user && state.login.user.token
|
||||
}
|
||||
});
|
||||
|
||||
const dispatchToProps = ({
|
||||
editCancel: () => editCancelAction(),
|
||||
editRequest: message => editRequestAction(message),
|
||||
typing: (rid, status) => userTypingAction(rid, status),
|
||||
closeReply: () => replyCancelAction()
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);
|
||||
|
|
|
@ -1,29 +1,33 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BORDER, COLOR_SEPARATOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE
|
||||
} from '../../constants/colors';
|
||||
|
||||
const MENTION_HEIGHT = 50;
|
||||
|
||||
export default StyleSheet.create({
|
||||
textBox: {
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: COLOR_WHITE,
|
||||
flex: 0,
|
||||
alignItems: 'center',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#D8D8D8',
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: COLOR_SEPARATOR,
|
||||
zIndex: 2
|
||||
},
|
||||
composer: {
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: COLOR_WHITE,
|
||||
flexDirection: 'column',
|
||||
borderTopColor: '#e1e5e8',
|
||||
borderTopWidth: 1
|
||||
borderTopColor: COLOR_SEPARATOR,
|
||||
borderTopWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
textArea: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexGrow: 0,
|
||||
backgroundColor: '#fff'
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
textBoxInput: {
|
||||
textAlignVertical: 'center',
|
||||
|
@ -37,7 +41,8 @@ export default StyleSheet.create({
|
|||
paddingRight: 0,
|
||||
fontSize: 17,
|
||||
letterSpacing: 0,
|
||||
color: '#2f343d'
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
editing: {
|
||||
backgroundColor: '#fff5df'
|
||||
|
@ -53,9 +58,9 @@ export default StyleSheet.create({
|
|||
},
|
||||
mentionItem: {
|
||||
height: MENTION_HEIGHT,
|
||||
backgroundColor: '#F7F8FA',
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#ECECEC',
|
||||
borderTopColor: COLOR_BORDER,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 5
|
||||
|
@ -72,18 +77,25 @@ export default StyleSheet.create({
|
|||
textAlign: 'center'
|
||||
},
|
||||
fixedMentionAvatar: {
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
width: 46
|
||||
width: 46,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textBold,
|
||||
...sharedStyles.textColorNormal
|
||||
},
|
||||
mentionText: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorNormal
|
||||
},
|
||||
emojiKeyboardContainer: {
|
||||
flex: 1,
|
||||
borderTopColor: '#ECECEC',
|
||||
borderTopColor: COLOR_BORDER,
|
||||
borderTopWidth: 1
|
||||
},
|
||||
iphoneXArea: {
|
||||
height: 50,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: COLOR_WHITE,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Image, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { COLOR_TEXT_DESCRIPTION } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
style: {
|
||||
marginRight: 7,
|
||||
marginTop: 3,
|
||||
tintColor: '#9EA2A8'
|
||||
tintColor: COLOR_TEXT_DESCRIPTION,
|
||||
color: COLOR_TEXT_DESCRIPTION
|
||||
},
|
||||
discussion: {
|
||||
marginRight: 6
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -15,6 +21,11 @@ const RoomTypeIcon = React.memo(({ type, size, style }) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (type === 'discussion') {
|
||||
// FIXME: These are temporary only. We should have all room icons on <Customicon />, but our design team is still working on this.
|
||||
return <CustomIcon name='chat' size={13} style={[styles.style, styles.discussion]} />;
|
||||
}
|
||||
|
||||
if (type === 'c') {
|
||||
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size }]} />;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
|||
import I18n from '../i18n';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -28,7 +29,8 @@ const styles = StyleSheet.create({
|
|||
fontSize: 17,
|
||||
marginLeft: 8,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
paddingBottom: 0,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -4,28 +4,16 @@ import { connect } from 'react-redux';
|
|||
import { ViewPropTypes } from 'react-native';
|
||||
|
||||
import Status from './Status';
|
||||
import database, { safeAddListener } from '../../lib/realm';
|
||||
|
||||
@connect((state, ownProps) => {
|
||||
if (state.login.user && ownProps.id === state.login.user.id) {
|
||||
return {
|
||||
status: state.login.user && state.login.user.status,
|
||||
offline: !state.meteor.connected
|
||||
};
|
||||
}
|
||||
|
||||
const user = state.activeUsers[ownProps.id];
|
||||
return {
|
||||
status: (user && user.status) || 'offline'
|
||||
};
|
||||
})
|
||||
|
||||
@connect(state => ({
|
||||
offline: !state.meteor.connected
|
||||
}))
|
||||
export default class StatusContainer extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// id is a prop, but it's used only inside @connect to find for current status
|
||||
id: PropTypes.string, // eslint-disable-line
|
||||
id: PropTypes.string,
|
||||
style: ViewPropTypes.style,
|
||||
size: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
offline: PropTypes.bool
|
||||
};
|
||||
|
||||
|
@ -33,12 +21,32 @@ export default class StatusContainer extends React.PureComponent {
|
|||
size: 16
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.user = database.memoryDatabase.objects('activeUsers').filtered('id == $0', props.id);
|
||||
this.state = {
|
||||
user: this.user[0] || {}
|
||||
};
|
||||
safeAddListener(this.user, this.updateState);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.user.removeAllListeners();
|
||||
}
|
||||
|
||||
get status() {
|
||||
const { offline, status } = this.props;
|
||||
if (offline) {
|
||||
const { user } = this.state;
|
||||
const { offline } = this.props;
|
||||
if (offline || !user) {
|
||||
return 'offline';
|
||||
}
|
||||
return status;
|
||||
return user.status || 'offline';
|
||||
}
|
||||
|
||||
updateState = () => {
|
||||
if (this.user.length) {
|
||||
this.setState({ user: this.user[0] });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -3,13 +3,13 @@ import { StatusBar as StatusBarRN } from 'react-native';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { HEADER_BACKGROUND } from '../constants/colors';
|
||||
import { HEADER_BACKGROUND, COLOR_WHITE } from '../constants/colors';
|
||||
|
||||
const HEADER_BAR_STYLE = isIOS ? 'dark-content' : 'light-content';
|
||||
|
||||
const StatusBar = React.memo(({ light }) => {
|
||||
if (light) {
|
||||
return <StatusBarRN backgroundColor='#FFF' barStyle='dark-content' animated />;
|
||||
return <StatusBarRN backgroundColor={COLOR_WHITE} barStyle='dark-content' animated />;
|
||||
}
|
||||
return <StatusBarRN backgroundColor={HEADER_BACKGROUND} barStyle={HEADER_BAR_STYLE} animated />;
|
||||
});
|
||||
|
|
|
@ -6,7 +6,9 @@ import PropTypes from 'prop-types';
|
|||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_DANGER, COLOR_TEXT } from '../constants/colors';
|
||||
import {
|
||||
COLOR_DANGER, COLOR_TEXT_DESCRIPTION, COLOR_TEXT, COLOR_BORDER
|
||||
} from '../constants/colors';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -15,22 +17,21 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
label: {
|
||||
marginBottom: 10,
|
||||
color: COLOR_TEXT,
|
||||
fontSize: 14,
|
||||
fontWeight: '700'
|
||||
...sharedStyles.textSemibold,
|
||||
...sharedStyles.textColorNormal
|
||||
},
|
||||
input: {
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorNormal,
|
||||
height: 48,
|
||||
fontSize: 17,
|
||||
color: '#9EA2A8',
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
borderWidth: 1.5,
|
||||
borderWidth: 1,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'white',
|
||||
borderColor: '#E7EBF2'
|
||||
borderColor: COLOR_BORDER
|
||||
},
|
||||
inputIconLeft: {
|
||||
paddingLeft: 45
|
||||
|
@ -59,10 +60,10 @@ const styles = StyleSheet.create({
|
|||
right: 15
|
||||
},
|
||||
icon: {
|
||||
color: '#2F343D'
|
||||
color: COLOR_TEXT
|
||||
},
|
||||
password: {
|
||||
color: '#9ea2a8'
|
||||
color: COLOR_TEXT_DESCRIPTION
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -144,7 +145,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
testID={testID}
|
||||
accessibilityLabel={placeholder}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor='#9ea2a8'
|
||||
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
|
||||
contentDescription={placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
|
|
|
@ -6,11 +6,13 @@ import {
|
|||
import Video from 'react-native-video';
|
||||
import Slider from 'react-native-slider';
|
||||
import moment from 'moment';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
audioContainer: {
|
||||
|
@ -18,35 +20,42 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: 56,
|
||||
backgroundColor: '#f7f8fa',
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
marginBottom: 10
|
||||
marginBottom: 6
|
||||
},
|
||||
playPauseButton: {
|
||||
width: 56,
|
||||
marginHorizontal: 10,
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
playPauseImage: {
|
||||
color: '#1D74F5'
|
||||
color: COLOR_PRIMARY
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
marginRight: 10
|
||||
flex: 1
|
||||
},
|
||||
duration: {
|
||||
marginRight: 16,
|
||||
marginHorizontal: 12,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#54585e'
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
thumbStyle: {
|
||||
width: 12,
|
||||
height: 12
|
||||
},
|
||||
trackStyle: {
|
||||
height: 2
|
||||
}
|
||||
});
|
||||
|
||||
const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
|
||||
const BUTTON_HIT_SLOP = {
|
||||
top: 12, right: 12, bottom: 12, left: 12
|
||||
};
|
||||
|
||||
export default class Audio extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -93,30 +102,30 @@ export default class Audio extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
onLoad(data) {
|
||||
onLoad = (data) => {
|
||||
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
|
||||
}
|
||||
|
||||
onProgress(data) {
|
||||
onProgress = (data) => {
|
||||
const { duration } = this.state;
|
||||
if (data.currentTime <= duration) {
|
||||
this.setState({ currentTime: data.currentTime });
|
||||
}
|
||||
}
|
||||
|
||||
onEnd() {
|
||||
onEnd = () => {
|
||||
this.setState({ paused: true, currentTime: 0 });
|
||||
requestAnimationFrame(() => {
|
||||
this.player.seek(0);
|
||||
});
|
||||
}
|
||||
|
||||
getDuration() {
|
||||
getDuration = () => {
|
||||
const { duration } = this.state;
|
||||
return formatTime(duration);
|
||||
}
|
||||
|
||||
togglePlayPause() {
|
||||
togglePlayPause = () => {
|
||||
const { paused } = this.state;
|
||||
this.setState({ paused: !paused });
|
||||
}
|
||||
|
@ -148,16 +157,18 @@ export default class Audio extends React.Component {
|
|||
paused={paused}
|
||||
repeat={false}
|
||||
/>
|
||||
<BorderlessButton
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={() => this.togglePlayPause()}
|
||||
onPress={this.togglePlayPause}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
{
|
||||
paused
|
||||
? <CustomIcon name='play' size={30} style={styles.playPauseImage} />
|
||||
: <CustomIcon name='pause' size={30} style={styles.playPauseImage} />
|
||||
? <CustomIcon name='play' size={36} style={styles.playPauseImage} />
|
||||
: <CustomIcon name='pause' size={36} style={styles.playPauseImage} />
|
||||
}
|
||||
</BorderlessButton>
|
||||
</Touchable>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={currentTime}
|
||||
|
@ -169,10 +180,11 @@ export default class Audio extends React.Component {
|
|||
easing: Easing.linear,
|
||||
delay: 0
|
||||
}}
|
||||
thumbTintColor='#1d74f5'
|
||||
minimumTrackTintColor='#1d74f5'
|
||||
thumbTintColor={COLOR_PRIMARY}
|
||||
minimumTrackTintColor={COLOR_PRIMARY}
|
||||
onValueChange={value => this.setState({ currentTime: value })}
|
||||
thumbStyle={styles.thumbStyle}
|
||||
trackStyle={styles.trackStyle}
|
||||
/>
|
||||
<Text style={styles.duration}>{this.getDuration()}</Text>
|
||||
</View>,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import PhotoModal from './PhotoModal';
|
||||
import Markdown from './Markdown';
|
||||
|
@ -36,7 +36,7 @@ export default class extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
onPressButton() {
|
||||
onPressButton = () => {
|
||||
this.setState({
|
||||
modalVisible: true
|
||||
});
|
||||
|
@ -58,7 +58,7 @@ export default class extends Component {
|
|||
render() {
|
||||
const { modalVisible, isPressed } = this.state;
|
||||
const { baseUrl, file, user } = this.props;
|
||||
const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
const img = file.image_url.includes('http') ? file.image_url : `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
|
||||
if (!img) {
|
||||
return null;
|
||||
|
@ -66,20 +66,21 @@ export default class extends Component {
|
|||
|
||||
return (
|
||||
[
|
||||
<RectButton
|
||||
<Touchable
|
||||
key='image'
|
||||
onPress={() => this.onPressButton()}
|
||||
onActiveStateChange={this.isPressed}
|
||||
onPress={this.onPressButton}
|
||||
style={styles.imageContainer}
|
||||
underlayColor='#fff'
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<FastImage
|
||||
style={[styles.image, isPressed && { opacity: 0.5 }]}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{this.getDescription()}
|
||||
</RectButton>,
|
||||
<React.Fragment>
|
||||
<FastImage
|
||||
style={[styles.image, isPressed && { opacity: 0.5 }]}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{this.getDescription()}
|
||||
</React.Fragment>
|
||||
</Touchable>,
|
||||
<PhotoModal
|
||||
key='modal'
|
||||
title={file.title}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Text, Image, Platform } from 'react-native';
|
||||
import { Text, Image } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { emojify } from 'react-emojione';
|
||||
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
|
||||
|
@ -14,116 +14,90 @@ const formatText = text => text.replace(
|
|||
(match, url, title) => `[${ title }](${ url })`
|
||||
);
|
||||
|
||||
const codeFontFamily = Platform.select({
|
||||
ios: { fontFamily: 'Courier New' },
|
||||
android: { fontFamily: 'monospace' }
|
||||
});
|
||||
|
||||
export default class Markdown extends React.Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { msg } = this.props;
|
||||
return nextProps.msg !== msg;
|
||||
const Markdown = React.memo(({
|
||||
msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
|
||||
}) => {
|
||||
if (!msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
msg, customEmojis, style, rules, baseUrl, username, edited
|
||||
} = this.props;
|
||||
if (!msg) {
|
||||
return null;
|
||||
}
|
||||
let m = formatText(msg);
|
||||
let m = formatText(msg);
|
||||
if (m) {
|
||||
m = emojify(m, { output: 'unicode' });
|
||||
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
rules={{
|
||||
paragraph: (node, children) => (
|
||||
// eslint-disable-next-line
|
||||
<Text key={node.key} style={styles.paragraph}>
|
||||
{children}
|
||||
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
|
||||
</Text>
|
||||
),
|
||||
mention: (node) => {
|
||||
const { content, key } = node;
|
||||
let mentionStyle = styles.mention;
|
||||
if (content === 'all' || content === 'here') {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
...styles.mentionAll
|
||||
};
|
||||
} else if (content === username) {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
...styles.mentionLoggedUser
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Text style={mentionStyle} key={key}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
hashtag: node => (
|
||||
<Text key={node.key} style={styles.mention}>
|
||||
#{node.content}
|
||||
</Text>
|
||||
),
|
||||
emoji: (node) => {
|
||||
if (node.children && node.children.length && node.children[0].content) {
|
||||
const { content } = node.children[0];
|
||||
const emojiExtension = customEmojis[content];
|
||||
if (emojiExtension) {
|
||||
const emoji = { extension: emojiExtension, content };
|
||||
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
|
||||
}
|
||||
return <Text key={node.key}>:{content}:</Text>;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hardbreak: () => null,
|
||||
blocklink: () => null,
|
||||
image: node => (
|
||||
<Image key={node.key} style={styles.inlineImage} source={{ uri: node.attributes.src }} />
|
||||
),
|
||||
...rules
|
||||
}}
|
||||
style={{
|
||||
paragraph: styles.paragraph,
|
||||
text: {
|
||||
color: '#0C0D0F',
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.1
|
||||
},
|
||||
codeInline: {
|
||||
...codeFontFamily,
|
||||
borderWidth: 1,
|
||||
backgroundColor: '#f8f8f8',
|
||||
borderRadius: 4
|
||||
},
|
||||
codeBlock: {
|
||||
...codeFontFamily,
|
||||
backgroundColor: '#f8f8f8',
|
||||
borderColor: '#cccccc',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
padding: 4
|
||||
},
|
||||
link: {
|
||||
color: '#1D74F5'
|
||||
},
|
||||
...style
|
||||
}}
|
||||
plugins={[
|
||||
new PluginContainer(MarkdownFlowdock),
|
||||
new PluginContainer(MarkdownEmojiPlugin)
|
||||
]}
|
||||
>{m}
|
||||
</MarkdownRenderer>
|
||||
);
|
||||
}
|
||||
}
|
||||
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
|
||||
if (numberOfLines > 0) {
|
||||
m = m.replace(/[\n]+/g, '\n').trim();
|
||||
}
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
rules={{
|
||||
paragraph: (node, children) => (
|
||||
// eslint-disable-next-line
|
||||
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
|
||||
</Text>
|
||||
),
|
||||
mention: (node) => {
|
||||
const { content, key } = node;
|
||||
let mentionStyle = styles.mention;
|
||||
if (content === 'all' || content === 'here') {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
...styles.mentionAll
|
||||
};
|
||||
} else if (content === username) {
|
||||
mentionStyle = {
|
||||
...mentionStyle,
|
||||
...styles.mentionLoggedUser
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Text style={mentionStyle} key={key}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
hashtag: node => (
|
||||
<Text key={node.key} style={styles.mention}>
|
||||
#{node.content}
|
||||
</Text>
|
||||
),
|
||||
emoji: (node) => {
|
||||
if (node.children && node.children.length && node.children[0].content) {
|
||||
const { content } = node.children[0];
|
||||
const emojiExtension = customEmojis[content];
|
||||
if (emojiExtension) {
|
||||
const emoji = { extension: emojiExtension, content };
|
||||
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
|
||||
}
|
||||
return <Text key={node.key}>:{content}:</Text>;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hardbreak: () => null,
|
||||
blocklink: () => null,
|
||||
image: node => (
|
||||
<Image key={node.key} style={styles.inlineImage} source={{ uri: node.attributes.src }} />
|
||||
),
|
||||
...rules
|
||||
}}
|
||||
style={{
|
||||
paragraph: styles.paragraph,
|
||||
text: styles.text,
|
||||
codeInline: styles.codeInline,
|
||||
codeBlock: styles.codeBlock,
|
||||
link: styles.link,
|
||||
...style
|
||||
}}
|
||||
plugins={[
|
||||
new PluginContainer(MarkdownFlowdock),
|
||||
new PluginContainer(MarkdownEmojiPlugin)
|
||||
]}
|
||||
>{m}
|
||||
</MarkdownRenderer>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.msg === nextProps.msg);
|
||||
|
||||
Markdown.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
|
@ -132,5 +106,8 @@ Markdown.propTypes = {
|
|||
customEmojis: PropTypes.object.isRequired,
|
||||
style: PropTypes.any,
|
||||
rules: PropTypes.object,
|
||||
edited: PropTypes.bool
|
||||
edited: PropTypes.bool,
|
||||
numberOfLines: PropTypes.number
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
|
|
|
@ -5,9 +5,9 @@ import {
|
|||
} from 'react-native';
|
||||
import moment from 'moment';
|
||||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||
import {
|
||||
State, RectButton, LongPressGestureHandler, BorderlessButton
|
||||
} from 'react-native-gesture-handler';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { emojify } from 'react-emojione';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
|
||||
import Image from './Image';
|
||||
import User from './User';
|
||||
|
@ -23,6 +23,10 @@ import styles from './styles';
|
|||
import I18n from '../../i18n';
|
||||
import messagesStatus from '../../constants/messagesStatus';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_DANGER } from '../../constants/colors';
|
||||
import debounce from '../../utils/debounce';
|
||||
import DisclosureIndicator from '../DisclosureIndicator';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const SYSTEM_MESSAGES = [
|
||||
'r',
|
||||
|
@ -30,6 +34,7 @@ const SYSTEM_MESSAGES = [
|
|||
'ru',
|
||||
'ul',
|
||||
'uj',
|
||||
'ut',
|
||||
'rm',
|
||||
'user-muted',
|
||||
'user-unmuted',
|
||||
|
@ -40,7 +45,8 @@ const SYSTEM_MESSAGES = [
|
|||
'room_changed_announcement',
|
||||
'room_changed_topic',
|
||||
'room_changed_privacy',
|
||||
'message_snippeted'
|
||||
'message_snippeted',
|
||||
'thread-created'
|
||||
];
|
||||
|
||||
const getInfoMessage = ({
|
||||
|
@ -51,6 +57,8 @@ const getInfoMessage = ({
|
|||
return I18n.t('Message_removed');
|
||||
} else if (type === 'uj') {
|
||||
return I18n.t('Has_joined_the_channel');
|
||||
} else if (type === 'ut') {
|
||||
return I18n.t('Has_joined_the_conversation');
|
||||
} else if (type === 'r') {
|
||||
return I18n.t('Room_name_changed', { name: msg, userBy: username });
|
||||
} else if (type === 'message_pinned') {
|
||||
|
@ -82,12 +90,16 @@ const getInfoMessage = ({
|
|||
}
|
||||
return '';
|
||||
};
|
||||
const BUTTON_HIT_SLOP = {
|
||||
top: 4, right: 4, bottom: 4, left: 4
|
||||
};
|
||||
|
||||
export default class Message extends PureComponent {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
msg: PropTypes.string,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
|
@ -108,6 +120,8 @@ export default class Message extends PureComponent {
|
|||
reactionsModal: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
header: PropTypes.bool,
|
||||
isThreadReply: PropTypes.bool,
|
||||
isThreadSequential: PropTypes.bool,
|
||||
avatar: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.oneOfType([
|
||||
|
@ -124,14 +138,23 @@ export default class Message extends PureComponent {
|
|||
PropTypes.object
|
||||
]),
|
||||
useRealName: PropTypes.bool,
|
||||
dcount: PropTypes.number,
|
||||
dlm: PropTypes.instanceOf(Date),
|
||||
tmid: PropTypes.string,
|
||||
tcount: PropTypes.number,
|
||||
tlm: PropTypes.instanceOf(Date),
|
||||
tmsg: PropTypes.string,
|
||||
// methods
|
||||
closeReactions: PropTypes.func,
|
||||
onErrorPress: PropTypes.func,
|
||||
onLongPress: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
onReactionPress: PropTypes.func,
|
||||
onDiscussionPress: PropTypes.func,
|
||||
onThreadPress: PropTypes.func,
|
||||
replyBroadcast: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
fetchThreadName: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -143,9 +166,14 @@ export default class Message extends PureComponent {
|
|||
onLongPress: () => {}
|
||||
}
|
||||
|
||||
onPress = () => {
|
||||
onPress = debounce(() => {
|
||||
KeyboardUtils.dismiss();
|
||||
}
|
||||
|
||||
const { onThreadPress, tlm, tmid } = this.props;
|
||||
if ((tlm || tmid) && onThreadPress) {
|
||||
onThreadPress();
|
||||
}
|
||||
}, 300, true)
|
||||
|
||||
onLongPress = () => {
|
||||
const { archived, onLongPress } = this.props;
|
||||
|
@ -155,6 +183,32 @@ export default class Message extends PureComponent {
|
|||
onLongPress();
|
||||
}
|
||||
|
||||
formatLastMessage = (lm) => {
|
||||
const { customThreadTimeFormat } = this.props;
|
||||
if (customThreadTimeFormat) {
|
||||
return moment(lm).format(customThreadTimeFormat);
|
||||
}
|
||||
return lm ? moment(lm).calendar(null, {
|
||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'h:mm A',
|
||||
lastWeek: 'dddd',
|
||||
sameElse: 'MMM D'
|
||||
}) : null;
|
||||
}
|
||||
|
||||
formatMessageCount = (count, type) => {
|
||||
const discussion = type === 'discussion';
|
||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||
if (count === 1) {
|
||||
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
|
||||
} else if (count > 1 && count < 1000) {
|
||||
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
} else if (count > 999) {
|
||||
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
isInfoMessage = () => {
|
||||
const { type } = this.props;
|
||||
return SYSTEM_MESSAGES.includes(type);
|
||||
|
@ -180,20 +234,21 @@ export default class Message extends PureComponent {
|
|||
return status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
renderAvatar = () => {
|
||||
renderAvatar = (small = false) => {
|
||||
const {
|
||||
header, avatar, author, baseUrl, user
|
||||
} = this.props;
|
||||
if (header) {
|
||||
return (
|
||||
<Avatar
|
||||
style={styles.avatar}
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={36}
|
||||
borderRadius={4}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
avatar={avatar}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -223,10 +278,25 @@ export default class Message extends PureComponent {
|
|||
if (this.isInfoMessage()) {
|
||||
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
|
||||
}
|
||||
|
||||
const {
|
||||
customEmojis, msg, baseUrl, user, edited
|
||||
customEmojis, msg, baseUrl, user, edited, tmid
|
||||
} = this.props;
|
||||
return <Markdown msg={msg} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} edited={edited} />;
|
||||
|
||||
if (tmid && !msg) {
|
||||
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
msg={msg}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
edited={edited}
|
||||
numberOfLines={tmid ? 1 : 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderAttachment() {
|
||||
|
@ -254,13 +324,13 @@ export default class Message extends PureComponent {
|
|||
}
|
||||
|
||||
renderUrl = () => {
|
||||
const { urls } = this.props;
|
||||
const { urls, user, baseUrl } = this.props;
|
||||
if (urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urls.map((url, index) => (
|
||||
<Url url={url} key={url.url} index={index} />
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -270,9 +340,9 @@ export default class Message extends PureComponent {
|
|||
}
|
||||
const { onErrorPress } = this.props;
|
||||
return (
|
||||
<BorderlessButton onPress={onErrorPress} style={styles.errorButton}>
|
||||
<CustomIcon name='circle-cross' color='red' size={20} />
|
||||
</BorderlessButton>
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton}>
|
||||
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -281,31 +351,27 @@ export default class Message extends PureComponent {
|
|||
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
|
||||
const underlayColor = reacted ? '#fff' : '#e1e5e8';
|
||||
return (
|
||||
<LongPressGestureHandler
|
||||
<Touchable
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
onLongPress={onReactionLongPress}
|
||||
key={reaction.emoji}
|
||||
onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && onReactionLongPress()}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<RectButton
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && { backgroundColor: '#e8f2ff' }]}
|
||||
activeOpacity={0.8}
|
||||
underlayColor={underlayColor}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
customEmojis={customEmojis}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</RectButton>
|
||||
</LongPressGestureHandler>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
customEmojis={customEmojis}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -317,18 +383,18 @@ export default class Message extends PureComponent {
|
|||
return (
|
||||
<View style={styles.reactionsContainer}>
|
||||
{reactions.map(this.renderReaction)}
|
||||
<RectButton
|
||||
<Touchable
|
||||
onPress={toggleReactionPicker}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={styles.reactionButton}
|
||||
activeOpacity={0.8}
|
||||
underlayColor='#e1e5e8'
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={styles.reactionContainer}>
|
||||
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
|
||||
</View>
|
||||
</RectButton>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -337,23 +403,176 @@ export default class Message extends PureComponent {
|
|||
const { broadcast, replyBroadcast } = this.props;
|
||||
if (broadcast && !this.isOwn()) {
|
||||
return (
|
||||
<RectButton
|
||||
onPress={replyBroadcast}
|
||||
style={styles.broadcastButton}
|
||||
activeOpacity={0.5}
|
||||
underlayColor='#fff'
|
||||
>
|
||||
<CustomIcon name='back' size={20} style={styles.broadcastButtonIcon} />
|
||||
<Text style={styles.broadcastButtonText}>{I18n.t('Reply')}</Text>
|
||||
</RectButton>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={replyBroadcast}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={styles.button}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderDiscussion = () => {
|
||||
const {
|
||||
msg, dcount, dlm, onDiscussionPress
|
||||
} = this.props;
|
||||
const time = this.formatLastMessage(dlm);
|
||||
const buttonText = this.formatMessageCount(dcount, 'discussion');
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
||||
<Text style={styles.text}>{msg}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onDiscussionPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderThread = () => {
|
||||
const {
|
||||
tcount, tlm, onThreadPress, msg
|
||||
} = this.props;
|
||||
|
||||
if (!tlm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const time = this.formatLastMessage(tlm);
|
||||
const buttonText = this.formatMessageCount(tcount, 'thread');
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onThreadPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
testID={`message-thread-button-${ msg }`}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderRepliedThread = () => {
|
||||
const {
|
||||
tmid, tmsg, header, fetchThreadName
|
||||
} = this.props;
|
||||
if (!tmid || !header || this.isTemp()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tmsg) {
|
||||
fetchThreadName(tmid);
|
||||
return null;
|
||||
}
|
||||
|
||||
let msg = emojify(tmsg, { output: 'unicode' });
|
||||
msg = removeMarkdown(msg);
|
||||
|
||||
return (
|
||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} />
|
||||
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
|
||||
<DisclosureIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderInner = () => {
|
||||
const { type } = this.props;
|
||||
if (type === 'discussion-created') {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderDiscussion()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderContent()}
|
||||
{this.renderAttachment()}
|
||||
{this.renderUrl()}
|
||||
{this.renderThread()}
|
||||
{this.renderReactions()}
|
||||
{this.renderBroadcastReply()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessage = () => {
|
||||
const { header, isThreadReply, isThreadSequential } = this.props;
|
||||
|
||||
if (isThreadReply || isThreadSequential || this.isInfoMessage()) {
|
||||
const thread = isThreadReply ? this.renderRepliedThread() : null;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{thread}
|
||||
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
|
||||
{this.renderAvatar(true)}
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
header && styles.messageContentWithHeader,
|
||||
this.hasError() && header && styles.messageContentWithHeader,
|
||||
this.hasError() && !header && styles.messageContentWithError,
|
||||
this.isTemp() && styles.temp
|
||||
]}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</View>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
{this.renderAvatar()}
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
header && styles.messageContentWithHeader,
|
||||
this.hasError() && header && styles.messageContentWithHeader,
|
||||
this.hasError() && !header && styles.messageContentWithError,
|
||||
this.isTemp() && styles.temp
|
||||
]}
|
||||
>
|
||||
{this.renderInner()}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
editing, style, header, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
|
||||
editing, style, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const accessibilityLabel = I18n.t('Message_accessibility', { user: author.username, time: moment(ts).format(timeFormat), message: msg });
|
||||
|
||||
|
@ -365,28 +584,10 @@ export default class Message extends PureComponent {
|
|||
onPress={this.onPress}
|
||||
>
|
||||
<View
|
||||
style={[styles.container, header && styles.marginBottom, editing && styles.editing, style]}
|
||||
style={[styles.container, editing && styles.editing, style]}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
<View style={styles.flex}>
|
||||
{this.renderAvatar()}
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
header && styles.messageContentWithHeader,
|
||||
this.hasError() && header && styles.messageContentWithHeader,
|
||||
this.hasError() && !header && styles.messageContentWithError,
|
||||
this.isTemp() && styles.temp
|
||||
]}
|
||||
>
|
||||
{this.renderUsername()}
|
||||
{this.renderContent()}
|
||||
{this.renderAttachment()}
|
||||
{this.renderUrl()}
|
||||
{this.renderReactions()}
|
||||
{this.renderBroadcastReply()}
|
||||
</View>
|
||||
</View>
|
||||
{this.renderMessage()}
|
||||
{reactionsModal
|
||||
? (
|
||||
<ReactionsModal
|
||||
|
|
|
@ -8,6 +8,9 @@ import Modal from 'react-native-modal';
|
|||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import { responsive } from 'react-native-responsive-ui';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imageWrapper: {
|
||||
flex: 1
|
||||
|
@ -18,16 +21,16 @@ const styles = StyleSheet.create({
|
|||
marginVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: '#ffffff',
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: '600'
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
description: {
|
||||
color: '#ffffff',
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 14,
|
||||
fontWeight: '500'
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
indicatorContainer: {
|
||||
alignItems: 'center',
|
||||
|
|
|
@ -8,6 +8,8 @@ import Modal from 'react-native-modal';
|
|||
import Emoji from './Emoji';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
|
@ -16,18 +18,20 @@ const styles = StyleSheet.create({
|
|||
paddingVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: '#ffffff',
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: '600'
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactCount: {
|
||||
color: '#dddddd',
|
||||
fontSize: 10
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 13,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
peopleReacted: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '500'
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
peopleItemContainer: {
|
||||
flex: 1,
|
||||
|
@ -51,7 +55,7 @@ const styles = StyleSheet.create({
|
|||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 10,
|
||||
color: '#ffffff'
|
||||
color: COLOR_WHITE
|
||||
}
|
||||
});
|
||||
const standardEmojiStyle = { fontSize: 20 };
|
||||
|
|
|
@ -2,19 +2,23 @@ import React from 'react';
|
|||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 15,
|
||||
marginTop: 6,
|
||||
alignSelf: 'flex-end',
|
||||
backgroundColor: '#f3f4f5',
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1,
|
||||
borderRadius: 4
|
||||
},
|
||||
attachmentContainer: {
|
||||
|
@ -30,16 +34,16 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
author: {
|
||||
flex: 1,
|
||||
color: '#0C0D0F',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginRight: 10
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
color: '#9ea2a8',
|
||||
marginLeft: 5
|
||||
marginLeft: 10,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
},
|
||||
fieldsContainer: {
|
||||
flex: 1,
|
||||
|
@ -51,7 +55,14 @@ const styles = StyleSheet.create({
|
|||
padding: 10
|
||||
},
|
||||
fieldTitle: {
|
||||
fontWeight: 'bold'
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
fieldValue: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
marginTop: {
|
||||
marginTop: 4
|
||||
|
@ -86,7 +97,7 @@ const Reply = ({
|
|||
};
|
||||
|
||||
const renderTitle = () => {
|
||||
if (!(attachment.author_icon || attachment.author_name || attachment.ts)) {
|
||||
if (!attachment.author_name) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
@ -121,7 +132,7 @@ const Reply = ({
|
|||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text style={styles.fieldTitle}>{field.title}</Text>
|
||||
<Text>{field.value}</Text>
|
||||
<Text style={styles.fieldValue}>{field.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
@ -129,18 +140,17 @@ const Reply = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<RectButton
|
||||
<Touchable
|
||||
onPress={() => onPress(attachment, baseUrl, user)}
|
||||
style={[styles.button, index > 0 && styles.marginTop]}
|
||||
activeOpacity={0.5}
|
||||
underlayColor='#fff'
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<View style={styles.attachmentContainer}>
|
||||
{renderTitle()}
|
||||
{renderText()}
|
||||
{renderFields()}
|
||||
</View>
|
||||
</RectButton>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,9 +2,14 @@ import React from 'react';
|
|||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import openLink from '../../utils/openLink';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY
|
||||
} from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
|
@ -14,8 +19,8 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#F3F4F5',
|
||||
borderColor: '#F3F4F5',
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1
|
||||
},
|
||||
textContainer: {
|
||||
|
@ -26,14 +31,14 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'flex-start'
|
||||
},
|
||||
title: {
|
||||
fontWeight: '500',
|
||||
color: '#1D74F5',
|
||||
fontSize: 16
|
||||
color: COLOR_PRIMARY,
|
||||
fontSize: 16,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
description: {
|
||||
marginTop: 5,
|
||||
fontSize: 16,
|
||||
color: '#0C0D0F'
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
marginTop: {
|
||||
marginTop: 4
|
||||
|
@ -46,32 +51,60 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const onPress = (url) => {
|
||||
openLink(url);
|
||||
};
|
||||
const Url = ({ url, index }) => {
|
||||
const UrlImage = React.memo(({ image, user, baseUrl }) => {
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
|
||||
});
|
||||
|
||||
const UrlContent = React.memo(({ title, description }) => (
|
||||
<View style={styles.textContainer}>
|
||||
{title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null}
|
||||
{description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null}
|
||||
</View>
|
||||
));
|
||||
|
||||
const Url = React.memo(({
|
||||
url, index, user, baseUrl
|
||||
}) => {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onPress = () => openLink(url.url);
|
||||
|
||||
return (
|
||||
<RectButton
|
||||
onPress={() => onPress(url.url)}
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={[styles.button, index > 0 && styles.marginTop, styles.container]}
|
||||
activeOpacity={0.5}
|
||||
underlayColor='#fff'
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
{url.image ? <FastImage source={{ uri: url.image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} /> : null}
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={styles.title} numberOfLines={2}>{url.title}</Text>
|
||||
<Text style={styles.description} numberOfLines={2}>{url.description}</Text>
|
||||
</View>
|
||||
</RectButton>
|
||||
<React.Fragment>
|
||||
<UrlImage image={url.image} user={user} baseUrl={baseUrl} />
|
||||
<UrlContent title={url.title} description={url.description} />
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
);
|
||||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
|
||||
|
||||
UrlImage.propTypes = {
|
||||
image: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
|
||||
UrlContent.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string
|
||||
};
|
||||
|
||||
Url.propTypes = {
|
||||
url: PropTypes.object.isRequired,
|
||||
index: PropTypes.number
|
||||
index: PropTypes.number,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
|
||||
export default Url;
|
||||
|
|
|
@ -3,18 +3,20 @@ import PropTypes from 'prop-types';
|
|||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import moment from 'moment';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import messageStyles from './styles';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 2
|
||||
alignItems: 'center'
|
||||
},
|
||||
username: {
|
||||
color: '#0C0D0F',
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
lineHeight: 22
|
||||
lineHeight: 22,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
|
@ -23,16 +25,8 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
alias: {
|
||||
fontSize: 14,
|
||||
color: '#9EA2A8',
|
||||
paddingLeft: 6,
|
||||
lineHeight: 16
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
color: '#9EA2A8',
|
||||
paddingLeft: 10,
|
||||
fontWeight: '300',
|
||||
lineHeight: 16
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -58,18 +52,18 @@ export default class User extends React.PureComponent {
|
|||
extraStyle.opacity = 0.3;
|
||||
}
|
||||
|
||||
const aliasUsername = alias ? (<Text style={styles.alias}>@{username}</Text>) : null;
|
||||
const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
|
||||
const time = moment(ts).format(timeFormat);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.username}>
|
||||
<Text style={styles.username} numberOfLines={1}>
|
||||
{alias || username}
|
||||
{aliasUsername}
|
||||
</Text>
|
||||
{aliasUsername}
|
||||
</View>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
<Text style={messageStyles.time}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { StyleSheet, View } from 'react-native';
|
||||
import Modal from 'react-native-modal';
|
||||
import VideoPlayer from 'react-native-video-controls';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
|
@ -19,7 +19,7 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 4,
|
||||
height: 150,
|
||||
backgroundColor: '#1f2329',
|
||||
marginBottom: 10,
|
||||
marginBottom: 6,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
@ -48,13 +48,13 @@ export default class Video extends React.PureComponent {
|
|||
return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
|
||||
toggleModal() {
|
||||
toggleModal = () => {
|
||||
this.setState(prevState => ({
|
||||
isVisible: !prevState.isVisible
|
||||
}));
|
||||
}
|
||||
|
||||
open() {
|
||||
open = () => {
|
||||
const { file } = this.props;
|
||||
if (isTypeSupported(file.video_type)) {
|
||||
return this.toggleModal();
|
||||
|
@ -76,18 +76,17 @@ export default class Video extends React.PureComponent {
|
|||
return (
|
||||
[
|
||||
<View key='button'>
|
||||
<RectButton
|
||||
<Touchable
|
||||
onPress={this.open}
|
||||
style={styles.button}
|
||||
onPress={() => this.open()}
|
||||
activeOpacity={0.5}
|
||||
underlayColor='#fff'
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<CustomIcon
|
||||
name='play'
|
||||
size={54}
|
||||
style={styles.image}
|
||||
/>
|
||||
</RectButton>
|
||||
</Touchable>
|
||||
<Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />
|
||||
</View>,
|
||||
<Modal
|
||||
|
@ -99,7 +98,7 @@ export default class Video extends React.PureComponent {
|
|||
>
|
||||
<VideoPlayer
|
||||
source={{ uri: this.uri }}
|
||||
onBack={() => this.toggleModal()}
|
||||
onBack={this.toggleModal}
|
||||
disableVolume
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
replyBroadcast as replyBroadcastAction
|
||||
} from '../../actions/messages';
|
||||
import { vibrate } from '../../utils/vibration';
|
||||
import debounce from '../../utils/debounce';
|
||||
|
||||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
|
@ -27,15 +28,14 @@ import { vibrate } from '../../utils/vibration';
|
|||
export default class MessageContainer extends React.Component {
|
||||
static propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
reactions: PropTypes.any.isRequired,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired
|
||||
}),
|
||||
customTimeFormat: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
style: ViewPropTypes.style,
|
||||
status: PropTypes.number,
|
||||
archived: PropTypes.bool,
|
||||
broadcast: PropTypes.bool,
|
||||
previousItem: PropTypes.object,
|
||||
|
@ -47,13 +47,17 @@ export default class MessageContainer extends React.Component {
|
|||
Message_TimeFormat: PropTypes.string,
|
||||
editingMessage: PropTypes.object,
|
||||
useRealName: PropTypes.bool,
|
||||
status: PropTypes.number,
|
||||
navigation: PropTypes.object,
|
||||
// methods - props
|
||||
onLongPress: PropTypes.func,
|
||||
onReactionPress: PropTypes.func,
|
||||
onDiscussionPress: PropTypes.func,
|
||||
// methods - redux
|
||||
errorActionsShow: PropTypes.func,
|
||||
replyBroadcast: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
fetchThreadName: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -72,7 +76,7 @@ export default class MessageContainer extends React.Component {
|
|||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { reactionsModal } = this.state;
|
||||
const {
|
||||
status, reactions, broadcast, _updatedAt, editingMessage, item
|
||||
status, editingMessage, item, _updatedAt, navigation
|
||||
} = this.props;
|
||||
|
||||
if (reactionsModal !== nextState.reactionsModal) {
|
||||
|
@ -81,27 +85,20 @@ export default class MessageContainer extends React.Component {
|
|||
if (status !== nextProps.status) {
|
||||
return true;
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
if (!!_updatedAt ^ !!nextProps._updatedAt) {
|
||||
if (item.tmsg !== nextProps.item.tmsg) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(reactions, nextProps.reactions)) {
|
||||
return true;
|
||||
}
|
||||
if (broadcast !== nextProps.broadcast) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(editingMessage, nextProps.editingMessage)) {
|
||||
|
||||
if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
|
||||
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
|
||||
return true;
|
||||
} else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString();
|
||||
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
|
||||
}
|
||||
|
||||
|
||||
onLongPress = () => {
|
||||
const { onLongPress } = this.props;
|
||||
onLongPress(this.parseMessage());
|
||||
|
@ -117,12 +114,30 @@ export default class MessageContainer extends React.Component {
|
|||
onReactionPress(emoji, item._id);
|
||||
}
|
||||
|
||||
|
||||
onReactionLongPress = () => {
|
||||
this.setState({ reactionsModal: true });
|
||||
vibrate();
|
||||
}
|
||||
|
||||
onDiscussionPress = () => {
|
||||
const { onDiscussionPress, item } = this.props;
|
||||
onDiscussionPress(item);
|
||||
}
|
||||
|
||||
onThreadPress = debounce(() => {
|
||||
const { navigation, item } = this.props;
|
||||
if (item.tmid) {
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
|
||||
});
|
||||
} else if (item.tlm) {
|
||||
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item._id, name: title, t: 'thread'
|
||||
});
|
||||
}
|
||||
}, 1000, true)
|
||||
|
||||
get timeFormat() {
|
||||
const { customTimeFormat, Message_TimeFormat } = this.props;
|
||||
return customTimeFormat || Message_TimeFormat;
|
||||
|
@ -141,12 +156,33 @@ export default class MessageContainer extends React.Component {
|
|||
&& (previousItem.u.username === item.u.username)
|
||||
&& !(previousItem.groupable === false || item.groupable === false || broadcast === true)
|
||||
&& (item.ts - previousItem.ts < Message_GroupingPeriod * 1000)
|
||||
&& (previousItem.tmid === item.tmid)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isThreadReply = () => {
|
||||
const {
|
||||
item, previousItem
|
||||
} = this.props;
|
||||
if (previousItem && item.tmid && (previousItem.tmid !== item.tmid) && (previousItem._id !== item.tmid)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isThreadSequential = () => {
|
||||
const {
|
||||
item, previousItem
|
||||
} = this.props;
|
||||
if (previousItem && item.tmid && ((previousItem.tmid === item.tmid) || (previousItem._id === item.tmid))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
parseMessage = () => {
|
||||
const { item } = this.props;
|
||||
return JSON.parse(JSON.stringify(item));
|
||||
|
@ -165,14 +201,15 @@ export default class MessageContainer extends React.Component {
|
|||
render() {
|
||||
const { reactionsModal } = this.state;
|
||||
const {
|
||||
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast
|
||||
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast, fetchThreadName, customThreadTimeFormat
|
||||
} = this.props;
|
||||
const {
|
||||
msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role
|
||||
_id, msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg
|
||||
} = item;
|
||||
const isEditing = editingMessage._id === item._id;
|
||||
return (
|
||||
<Message
|
||||
id={_id}
|
||||
msg={msg}
|
||||
author={u}
|
||||
ts={ts}
|
||||
|
@ -184,10 +221,13 @@ export default class MessageContainer extends React.Component {
|
|||
alias={alias}
|
||||
editing={isEditing}
|
||||
header={this.isHeader()}
|
||||
isThreadReply={this.isThreadReply()}
|
||||
isThreadSequential={this.isThreadSequential()}
|
||||
avatar={avatar}
|
||||
user={user}
|
||||
edited={editedBy && !!editedBy.username}
|
||||
timeFormat={this.timeFormat}
|
||||
customThreadTimeFormat={customThreadTimeFormat}
|
||||
style={style}
|
||||
archived={archived}
|
||||
broadcast={broadcast}
|
||||
|
@ -196,6 +236,14 @@ export default class MessageContainer extends React.Component {
|
|||
reactionsModal={reactionsModal}
|
||||
useRealName={useRealName}
|
||||
role={role}
|
||||
drid={drid}
|
||||
dcount={dcount}
|
||||
dlm={dlm}
|
||||
tmid={tmid}
|
||||
tcount={tcount}
|
||||
tlm={tlm}
|
||||
tmsg={tmsg}
|
||||
fetchThreadName={fetchThreadName}
|
||||
closeReactions={this.closeReactions}
|
||||
onErrorPress={this.onErrorPress}
|
||||
onLongPress={this.onLongPress}
|
||||
|
@ -203,6 +251,8 @@ export default class MessageContainer extends React.Component {
|
|||
onReactionPress={this.onReactionPress}
|
||||
replyBroadcast={this.replyBroadcast}
|
||||
toggleReactionPicker={this.toggleReactionPicker}
|
||||
onDiscussionPress={this.onDiscussionPress}
|
||||
onThreadPress={this.onThreadPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import { StyleSheet, Platform } from 'react-native';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER
|
||||
} from '../../constants/colors';
|
||||
|
||||
const codeFontFamily = Platform.select({
|
||||
ios: { fontFamily: 'Courier New' },
|
||||
android: { fontFamily: 'monospace' }
|
||||
});
|
||||
|
||||
export default StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
container: {
|
||||
paddingVertical: 5,
|
||||
paddingVertical: 4,
|
||||
width: '100%',
|
||||
paddingHorizontal: 15,
|
||||
paddingHorizontal: 14,
|
||||
flexDirection: 'column',
|
||||
transform: [{ scaleY: -1 }],
|
||||
flex: 1
|
||||
},
|
||||
messageContent: {
|
||||
|
@ -26,10 +35,16 @@ export default StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
flex: 1
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
textInfo: {
|
||||
fontStyle: 'italic',
|
||||
color: '#a0a0a0',
|
||||
fontSize: 16
|
||||
fontSize: 16,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
editing: {
|
||||
backgroundColor: '#fff5df'
|
||||
|
@ -39,38 +54,41 @@ export default StyleSheet.create({
|
|||
height: 20
|
||||
},
|
||||
temp: { opacity: 0.3 },
|
||||
marginBottom: {
|
||||
marginBottom: 10
|
||||
marginTop: {
|
||||
marginTop: 6
|
||||
},
|
||||
reactionsContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 10
|
||||
marginTop: 6
|
||||
},
|
||||
reactionButton: {
|
||||
marginRight: 10,
|
||||
marginBottom: 10,
|
||||
marginRight: 6,
|
||||
marginBottom: 6,
|
||||
borderRadius: 2
|
||||
},
|
||||
reactionButtonReacted: {
|
||||
backgroundColor: '#e8f2ff'
|
||||
},
|
||||
reactionContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#e1e5e8',
|
||||
borderRadius: 2,
|
||||
borderWidth: 1,
|
||||
borderColor: COLOR_BORDER,
|
||||
height: 28,
|
||||
minWidth: 46
|
||||
minWidth: 46.3
|
||||
},
|
||||
reactedContainer: {
|
||||
borderColor: '#1d74f580'
|
||||
borderColor: COLOR_PRIMARY
|
||||
},
|
||||
reactionCount: {
|
||||
fontSize: 14,
|
||||
marginLeft: 3,
|
||||
marginRight: 8.5,
|
||||
fontWeight: '600',
|
||||
color: '#1D74F5'
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactionEmoji: {
|
||||
fontSize: 13,
|
||||
|
@ -82,47 +100,56 @@ export default StyleSheet.create({
|
|||
marginLeft: 7
|
||||
},
|
||||
avatar: {
|
||||
marginTop: 5
|
||||
marginTop: 4
|
||||
},
|
||||
avatarSmall: {
|
||||
marginLeft: 16
|
||||
},
|
||||
addReaction: {
|
||||
color: '#1D74F5'
|
||||
color: COLOR_PRIMARY
|
||||
},
|
||||
errorButton: {
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 5
|
||||
},
|
||||
broadcastButton: {
|
||||
width: 107,
|
||||
buttonContainer: {
|
||||
marginTop: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
button: {
|
||||
paddingHorizontal: 15,
|
||||
height: 44,
|
||||
marginTop: 15,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#1d74f5',
|
||||
borderRadius: 4
|
||||
backgroundColor: COLOR_PRIMARY,
|
||||
borderRadius: 2
|
||||
},
|
||||
broadcastButtonIcon: {
|
||||
color: '#fff',
|
||||
marginRight: 11
|
||||
smallButton: {
|
||||
height: 30
|
||||
},
|
||||
broadcastButtonText: {
|
||||
color: '#fff',
|
||||
buttonIcon: {
|
||||
color: COLOR_WHITE,
|
||||
marginRight: 6
|
||||
},
|
||||
buttonText: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
fontWeight: '500'
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
mention: {
|
||||
...sharedStyles.textMedium,
|
||||
color: '#0072FE',
|
||||
fontWeight: '500',
|
||||
padding: 5,
|
||||
backgroundColor: '#E8F2FF'
|
||||
},
|
||||
mentionLoggedUser: {
|
||||
color: '#fff',
|
||||
backgroundColor: '#1D74F5'
|
||||
color: COLOR_WHITE,
|
||||
backgroundColor: COLOR_PRIMARY
|
||||
},
|
||||
mentionAll: {
|
||||
color: '#fff',
|
||||
color: COLOR_WHITE,
|
||||
backgroundColor: '#FF5B5A'
|
||||
},
|
||||
paragraph: {
|
||||
|
@ -136,8 +163,6 @@ export default StyleSheet.create({
|
|||
imageContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
borderColor: '#F3F4F5',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4
|
||||
},
|
||||
image: {
|
||||
|
@ -145,7 +170,8 @@ export default StyleSheet.create({
|
|||
maxWidth: 400,
|
||||
minHeight: 200,
|
||||
borderRadius: 4,
|
||||
marginBottom: 10
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1
|
||||
},
|
||||
inlineImage: {
|
||||
width: 300,
|
||||
|
@ -154,6 +180,60 @@ export default StyleSheet.create({
|
|||
},
|
||||
edited: {
|
||||
fontSize: 14,
|
||||
color: '#9EA2A8'
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
codeInline: {
|
||||
...sharedStyles.textRegular,
|
||||
...codeFontFamily,
|
||||
borderWidth: 1,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderRadius: 4
|
||||
},
|
||||
codeBlock: {
|
||||
...sharedStyles.textRegular,
|
||||
...codeFontFamily,
|
||||
backgroundColor: COLOR_BACKGROUND_CONTAINER,
|
||||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
padding: 4
|
||||
},
|
||||
link: {
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
startedDiscussion: {
|
||||
fontStyle: 'italic',
|
||||
fontSize: 16,
|
||||
marginBottom: 6,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
paddingLeft: 10,
|
||||
lineHeight: 22,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
},
|
||||
repliedThread: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
marginBottom: 12
|
||||
},
|
||||
repliedThreadIcon: {
|
||||
color: COLOR_PRIMARY,
|
||||
marginRight: 10,
|
||||
marginLeft: 16
|
||||
},
|
||||
repliedThreadName: {
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,12 +5,13 @@ import fr from './locales/fr';
|
|||
import de from './locales/de';
|
||||
import ptBR from './locales/pt-BR';
|
||||
import zhCN from './locales/zh-CN';
|
||||
import ptPT from './locales/pt-PT';
|
||||
|
||||
I18n.fallbacks = true;
|
||||
I18n.defaultLocale = 'en';
|
||||
|
||||
I18n.translations = {
|
||||
en, ru, 'pt-BR': ptBR, 'zh-CN': zhCN, fr, de
|
||||
en, ru, 'pt-BR': ptBR, 'zh-CN': zhCN, fr, de, 'pt-PT': ptPT
|
||||
};
|
||||
|
||||
export default I18n;
|
||||
|
|
|
@ -124,6 +124,7 @@ export default {
|
|||
Connect: 'Connect',
|
||||
Connect_to_a_server: 'Connect to a server',
|
||||
Connected: 'Connected',
|
||||
connecting_server: 'connecting to server',
|
||||
Connecting: 'Connecting...',
|
||||
Continue_with: 'Continue with',
|
||||
Copied_to_clipboard: 'Copied to clipboard!',
|
||||
|
@ -141,6 +142,7 @@ export default {
|
|||
description: 'description',
|
||||
Description: 'Description',
|
||||
Disable_notifications: 'Disable notifications',
|
||||
Discussions: 'Discussions',
|
||||
Direct_Messages: 'Direct Messages',
|
||||
Dont_Have_An_Account: 'Don\'t have an account?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
|
||||
|
@ -158,6 +160,7 @@ export default {
|
|||
File_description: 'File description',
|
||||
File_name: 'File name',
|
||||
Finish_recording: 'Finish recording',
|
||||
Following_thread: 'Following thread',
|
||||
For_your_security_you_must_enter_your_current_password_to_continue: 'For your security, you must enter your current password to continue',
|
||||
Forgot_my_password: 'Forgot my password',
|
||||
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
|
||||
|
@ -166,6 +169,7 @@ export default {
|
|||
Group_by_favorites: 'Group favorites',
|
||||
Group_by_type: 'Group by type',
|
||||
Has_joined_the_channel: 'Has joined the channel',
|
||||
Has_joined_the_conversation: 'Has joined the conversation',
|
||||
Has_left_the_channel: 'Has left the channel',
|
||||
Invisible: 'Invisible',
|
||||
Invite: 'Invite',
|
||||
|
@ -196,6 +200,8 @@ export default {
|
|||
Message_actions: 'Message actions',
|
||||
Message_pinned: 'Message pinned',
|
||||
Message_removed: 'Message removed',
|
||||
message: 'message',
|
||||
messages: 'messages',
|
||||
Messages: 'Messages',
|
||||
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
|
||||
Microphone_Permission: 'Microphone Permission',
|
||||
|
@ -215,10 +221,12 @@ export default {
|
|||
No_pinned_messages: 'No pinned messages',
|
||||
No_results_found: 'No results found',
|
||||
No_starred_messages: 'No starred messages',
|
||||
No_thread_messages: 'No thread messages',
|
||||
No_announcement_provided: 'No announcement provided.',
|
||||
No_description_provided: 'No description provided.',
|
||||
No_topic_provided: 'No topic provided.',
|
||||
No_Message: 'No Message',
|
||||
No_messages_yet: 'No messages yet',
|
||||
No_Reactions: 'No Reactions',
|
||||
Not_logged: 'Not logged',
|
||||
Nothing_to_save: 'Nothing to save!',
|
||||
|
@ -254,6 +262,9 @@ export default {
|
|||
Read_Only: 'Read Only',
|
||||
Register: 'Register',
|
||||
Repeat_Password: 'Repeat Password',
|
||||
Replied_on: 'Replied on:',
|
||||
replies: 'replies',
|
||||
reply: 'reply',
|
||||
Reply: 'Reply',
|
||||
Resend: 'Resend',
|
||||
Reset_password: 'Reset password',
|
||||
|
@ -283,6 +294,7 @@ export default {
|
|||
Send: 'Send',
|
||||
Send_audio_message: 'Send audio message',
|
||||
Send_message: 'Send message',
|
||||
Sent_an_attachment: 'Sent an attachment',
|
||||
Server: 'Server',
|
||||
Servers: 'Servers',
|
||||
Set_username_subtitle: 'The username is used to allow others to mention you in messages',
|
||||
|
@ -299,6 +311,7 @@ export default {
|
|||
starred: 'starred',
|
||||
Starred: 'Starred',
|
||||
Start_of_conversation: 'Start of conversation',
|
||||
Started_discussion: 'Started a discussion:',
|
||||
Submit: 'Submit',
|
||||
Take_a_photo: 'Take a photo',
|
||||
tap_to_change_status: 'tap to change status',
|
||||
|
@ -308,6 +321,8 @@ export default {
|
|||
There_was_an_error_while_action: 'There was an error while {{action}}!',
|
||||
This_room_is_blocked: 'This room is blocked',
|
||||
This_room_is_read_only: 'This room is read only',
|
||||
Thread: 'Thread',
|
||||
Threads: 'Threads',
|
||||
Timezone: 'Timezone',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: 'topic',
|
||||
|
@ -318,6 +333,7 @@ export default {
|
|||
unarchive: 'unarchive',
|
||||
UNARCHIVE: 'UNARCHIVE',
|
||||
Unblock_user: 'Unblock user',
|
||||
Unfollowed_thread: 'Unfollowed thread',
|
||||
Unmute: 'Unmute',
|
||||
unmuted: 'unmuted',
|
||||
Unpin: 'Unpin',
|
||||
|
@ -349,7 +365,7 @@ export default {
|
|||
Yesterday: 'Yesterday',
|
||||
You_are_in_preview_mode: 'You are in preview mode',
|
||||
You_are_offline: 'You are offline',
|
||||
You_can_search_using_RegExp_eg: 'You can search using RegExp. e.g. `/^text$/i`',
|
||||
You_can_search_using_RegExp_eg: 'You can use RegExp. e.g. `/^text$/i`',
|
||||
You_colon: 'You: ',
|
||||
you_were_mentioned: 'you were mentioned',
|
||||
you: 'you',
|
||||
|
|
|
@ -131,6 +131,7 @@ export default {
|
|||
Connect: 'Conectar',
|
||||
Connect_to_a_server: 'Conectar a um servidor',
|
||||
Connected: 'Conectado',
|
||||
connecting_server: 'conectando no servidor',
|
||||
Connecting: 'Conectando...',
|
||||
Continue_with: 'Entrar com',
|
||||
Copied_to_clipboard: 'Copiado para a área de transferência!',
|
||||
|
@ -148,6 +149,7 @@ export default {
|
|||
description: 'descrição',
|
||||
Description: 'Descrição',
|
||||
Disable_notifications: 'Desabilitar notificações',
|
||||
Discussions: 'Discussões',
|
||||
Direct_Messages: 'Mensagens Diretas',
|
||||
Dont_Have_An_Account: 'Não tem uma conta?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
|
||||
|
@ -165,6 +167,7 @@ export default {
|
|||
File_description: 'Descrição do arquivo',
|
||||
File_name: 'Nome do arquivo',
|
||||
Finish_recording: 'Encerrar gravação',
|
||||
Following_thread: 'Começou a seguir tópico',
|
||||
For_your_security_you_must_enter_your_current_password_to_continue: 'Para sua segurança, você precisa digitar sua senha',
|
||||
Forgot_my_password: 'Esqueci minha senha',
|
||||
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.',
|
||||
|
@ -173,6 +176,7 @@ export default {
|
|||
Group_by_favorites: 'Agrupar favoritos',
|
||||
Group_by_type: 'Agrupar por tipo',
|
||||
Has_joined_the_channel: 'Entrou no canal',
|
||||
Has_joined_the_conversation: 'Entrou na conversa',
|
||||
Has_left_the_channel: 'Saiu da conversa',
|
||||
Invisible: 'Invisível',
|
||||
Invite: 'Convidar',
|
||||
|
@ -200,6 +204,8 @@ export default {
|
|||
Message_actions: 'Ações',
|
||||
Message_pinned: 'Fixou uma mensagem',
|
||||
Message_removed: 'Mensagem removida',
|
||||
message: 'mensagem',
|
||||
messages: 'mensagens',
|
||||
Messages: 'Mensagens',
|
||||
Microphone_Permission_Message: 'Rocket Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.',
|
||||
Microphone_Permission: 'Acesso ao Microfone',
|
||||
|
@ -218,10 +224,12 @@ export default {
|
|||
No_pinned_messages: 'Não há mensagens fixadas',
|
||||
No_results_found: 'Nenhum resultado encontrado',
|
||||
No_starred_messages: 'Não há mensagens favoritas',
|
||||
No_thread_messages: 'Não há tópicos',
|
||||
No_announcement_provided: 'Sem anúncio.',
|
||||
No_description_provided: 'Sem descrição.',
|
||||
No_topic_provided: 'Sem tópico.',
|
||||
No_Message: 'Não há mensagens',
|
||||
No_messages_yet: 'Não há mensagens ainda',
|
||||
No_Reactions: 'Sem reações',
|
||||
Nothing_to_save: 'Nada para salvar!',
|
||||
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
|
||||
|
@ -256,6 +264,9 @@ export default {
|
|||
Read_Only: 'Somente Leitura',
|
||||
Register: 'Registrar',
|
||||
Repeat_Password: 'Repetir Senha',
|
||||
Replied_on: 'Respondido em:',
|
||||
replies: 'respostas',
|
||||
reply: 'resposta',
|
||||
Reply: 'Responder',
|
||||
Resend: 'Reenviar',
|
||||
Reset_password: 'Resetar senha',
|
||||
|
@ -285,6 +296,7 @@ export default {
|
|||
Send: 'Enviar',
|
||||
Send_audio_message: 'Enviar mensagem de áudio',
|
||||
Send_message: 'Enviar mensagem',
|
||||
Sent_an_attachment: 'Enviou um anexo',
|
||||
Server: 'Servidor',
|
||||
Set_username_subtitle: 'O usuário é utilizado para permitir que você seja mencionado em mensagens',
|
||||
Settings: 'Configurações',
|
||||
|
@ -300,6 +312,7 @@ export default {
|
|||
starred: 'favoritou',
|
||||
Starred: 'Mensagens Favoritas',
|
||||
Start_of_conversation: 'Início da conversa',
|
||||
Started_discussion: 'Iniciou uma discussão:',
|
||||
Submit: 'Enviar',
|
||||
Take_a_photo: 'Tirar uma foto',
|
||||
Terms_of_Service: ' Termos de Serviço ',
|
||||
|
@ -307,6 +320,8 @@ export default {
|
|||
There_was_an_error_while_action: 'Aconteceu um erro {{action}}!',
|
||||
This_room_is_blocked: 'Este quarto está bloqueado',
|
||||
This_room_is_read_only: 'Este quarto é apenas de leitura',
|
||||
Thread: 'Tópico',
|
||||
Threads: 'Tópicos',
|
||||
Timezone: 'Fuso horário',
|
||||
topic: 'tópico',
|
||||
Topic: 'Tópico',
|
||||
|
@ -316,6 +331,7 @@ export default {
|
|||
unarchive: 'desarquivar',
|
||||
UNARCHIVE: 'DESARQUIVAR',
|
||||
Unblock_user: 'Desbloquear usuário',
|
||||
Unfollowed_thread: 'Parou de seguir tópico',
|
||||
Unmute: 'Permitir que o usuário fale',
|
||||
unmuted: 'permitiu que o usuário fale',
|
||||
Unpin: 'Desafixar Mensagem',
|
||||
|
@ -346,7 +362,7 @@ export default {
|
|||
Yesterday: 'Ontem',
|
||||
You_are_in_preview_mode: 'Está é uma prévia do canal',
|
||||
You_are_offline: 'Você está offline',
|
||||
You_can_search_using_RegExp_eg: 'Você pode pesquisar usando expressões regulares, por exemplo `/^text$/i`',
|
||||
You_can_search_using_RegExp_eg: 'Você pode usar expressões regulares, por exemplo `/^text$/i`',
|
||||
You_colon: 'Você: ',
|
||||
you_were_mentioned: 'você foi mencionado',
|
||||
you: 'você',
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
export default {
|
||||
'1_person_reacted': '1 pessoa reagiu',
|
||||
'1_user': '1 utilizador',
|
||||
'error-action-not-allowed': '{{action}} não é permitida',
|
||||
'error-application-not-found': 'Aplicação não encontrada',
|
||||
'error-archived-duplicate-name': 'Existe um canal arquivado com o nome {{room_name}}',
|
||||
'error-avatar-invalid-url': 'URL de avatar inválido: {{url}}',
|
||||
'error-avatar-url-handling': 'Erro ao manipular a configuração de avatar de um URL ({{url}}) para {{username}}',
|
||||
'error-cant-invite-for-direct-room': 'Não pode convidar utilizadores para salas de mensagens directas',
|
||||
'error-could-not-change-email': 'Não foi possível alterar o e-mail',
|
||||
'error-could-not-change-name': 'Não foi possível alterar o nome',
|
||||
'error-could-not-change-username': 'Não foi possível alterar o nome de utilizador',
|
||||
'error-delete-protected-role': 'Não é possível eliminar uma função protegida',
|
||||
'error-department-not-found': 'Departamento não encontrado',
|
||||
'error-direct-message-file-upload-not-allowed': 'Partilha de ficheiros não permitido em mensagens diretas',
|
||||
'error-duplicate-channel-name': 'Um canal com o nome {{channel_name}} existe',
|
||||
'error-email-domain-blacklisted': 'O domínio de e-mail está na lista negra',
|
||||
'error-email-send-failed': 'Erro ao tentar enviar e-mail: {{message}}',
|
||||
'error-field-unavailable': '{{field}} já está em uso :(',
|
||||
'error-file-too-large': 'Ficheiro demasiado grande',
|
||||
'error-importer-not-defined': 'O importador não foi definido correctamente, a classe Import está em falta.',
|
||||
'error-input-is-not-a-valid-field': '{{input}} não é um {{field}} válido',
|
||||
'error-invalid-actionlink': 'Link de acção inválido',
|
||||
'error-invalid-arguments': 'Argumentos inválidos',
|
||||
'error-invalid-asset': 'Ficheiro inválida',
|
||||
'error-invalid-channel': 'Canal inválido.',
|
||||
'error-invalid-channel-start-with-chars': 'Canal inválido. Começa por @ ou #',
|
||||
'error-invalid-custom-field': 'Campo personalizado inválido',
|
||||
'error-invalid-custom-field-name': 'Nome de campo personalizado inválido. Use apenas letras, números, hífens e sublinhados.',
|
||||
'error-invalid-date': 'Data inválida fornecida.',
|
||||
'error-invalid-description': 'Descrição inválida',
|
||||
'error-invalid-domain': 'Domínio inválido',
|
||||
'error-invalid-email': 'E-mail inválido {{emai}}',
|
||||
'error-invalid-email-address': 'Endereço de e-mail invalido',
|
||||
'error-invalid-file-height': 'Altura de ficheiro inválida',
|
||||
'error-invalid-file-type': 'Tipo de ficheiro inválido',
|
||||
'error-invalid-file-width': 'Largura de ficheiro inválida',
|
||||
'error-invalid-from-address': 'Você informou um endereço DE inválido.',
|
||||
'error-invalid-integration': 'Integração inválida',
|
||||
'error-invalid-message': 'Mensagem inválida',
|
||||
'error-invalid-method': 'Método inválido',
|
||||
'error-invalid-name': 'Nome inválido',
|
||||
'error-invalid-password': 'Palavra-passe inválida',
|
||||
'error-invalid-redirectUri': 'redirectUri inválido',
|
||||
'error-invalid-role': 'Função inválido',
|
||||
'error-invalid-room': 'Sala inválida',
|
||||
'error-invalid-room-name': '{{room_name}} não é um nome de sala válido',
|
||||
'error-invalid-room-type': '{{type}} não é um tipo de sala válido.',
|
||||
'error-invalid-settings': 'Configurações inválidas fornecidas',
|
||||
'error-invalid-subscription': 'Subscrição inválida',
|
||||
'error-invalid-token': 'Token inválido',
|
||||
'error-invalid-triggerWords': 'triggerWords inválido',
|
||||
'error-invalid-urls': 'URLs inválidos',
|
||||
'error-invalid-user': 'Utilizador inválido',
|
||||
'error-invalid-username': 'Nome de utilizador inválido',
|
||||
'error-invalid-webhook-response': 'O URL do webhook respondeu com um estado diferente de 200',
|
||||
'error-message-deleting-blocked': 'A remoção de mensagens está bloqueada',
|
||||
'error-message-editing-blocked': 'A edição de mensagens está bloqueada',
|
||||
'error-message-size-exceeded': 'O tamanho da mensagem excede Message_MaxAllowedSize',
|
||||
'error-missing-unsubscribe-link': 'Você deve fornecer o link para cancelar a subscrição: [unsubscribe].',
|
||||
'error-no-tokens-for-this-user': 'Não há tokens para este utilizador',
|
||||
'error-not-allowed': 'Não permitido',
|
||||
'error-not-authorized': 'Não autorizado',
|
||||
'error-push-disabled': 'Push está desactivado',
|
||||
'error-remove-last-owner': 'Este é o último proprietário. Por favor, defina um novo proprietário antes de remover este.',
|
||||
'error-role-in-use': 'Não é possível remover função porque está em uso',
|
||||
'error-role-name-required': 'Nome da função requerido',
|
||||
'error-the-field-is-required': 'O campo {{field}} é obrigatório.',
|
||||
'error-too-many-requests': 'Erro, demasiados pedidos. Por favor, diminua a velocidade. Você deve esperar {{seconds}} segundos antes de tentar novamente.',
|
||||
'error-user-is-not-activated': 'O utilizador não está activado',
|
||||
'error-user-has-no-roles': 'O utilizador não tem funções',
|
||||
'error-user-limit-exceeded': 'O número de utilizadores que você está a tentar convidar para #channel_name excede o limite definido pelo administrador',
|
||||
'error-user-not-in-room': 'O utilizador não está nesta sala',
|
||||
'error-user-registration-custom-field': 'error-user-registration-custom-field',
|
||||
'error-user-registration-disabled': 'O registo de utilizadores está desactivado',
|
||||
'error-user-registration-secret': 'O registo de utilizadores só é permitido por meio de um URL secreto',
|
||||
'error-you-are-last-owner': 'Você é o último proprietário. Por favor, defina novo proprietário antes de sair da sala.',
|
||||
Actions: 'Acções',
|
||||
activity: 'actividade',
|
||||
Activity: 'Actividade',
|
||||
Add_Reaction: 'Adicionar Reacção',
|
||||
Add_Server: 'Adicionar Servidor',
|
||||
Add_user: 'Adicionar utilizador',
|
||||
Alert: 'Alerta',
|
||||
alert: 'alerta',
|
||||
alerts: 'alertas',
|
||||
All_users_in_the_channel_can_write_new_messages: 'Todos os utilizadores no canal podem escrever novas mensagens',
|
||||
All: 'Todos',
|
||||
Allow_Reactions: 'Permitir Reacções',
|
||||
Alphabetical: 'Alfabética',
|
||||
and_more: 'e mais',
|
||||
and: 'e',
|
||||
announcement: 'anúncio',
|
||||
Announcement: 'Anúncio',
|
||||
ARCHIVE: 'ARQUIVAR',
|
||||
archive: 'arquivar',
|
||||
are_typing: 'estão a escrever',
|
||||
Are_you_sure_question_mark: 'Tem a certeza?',
|
||||
Are_you_sure_you_want_to_leave_the_room: 'Tem certeza de que quer sair da sala {{room}}?',
|
||||
Authenticating: 'Autenticando',
|
||||
Avatar_changed_successfully: 'Avatar alterado com sucesso!',
|
||||
Avatar_Url: 'URL do Avatar',
|
||||
Away: 'Ausente',
|
||||
Block_user: 'Bloquear utilizador',
|
||||
Broadcast_channel_Description: 'Apenas utilizadores autorizados podem escrever novas mensagens, mas os outros utilizadores poderão responder',
|
||||
Broadcast_Channel: 'Canal de Transmissão',
|
||||
Busy: 'Ocupado',
|
||||
By_proceeding_you_are_agreeing: 'Ao prosseguir você concorda com o(s) nosso(s)',
|
||||
Cancel_editing: 'Cancelar edição',
|
||||
Cancel_recording: 'Cancelar gravação',
|
||||
Cancel: 'Cancelar',
|
||||
changing_avatar: 'a alterar avatar',
|
||||
creating_channel: 'a criar canal',
|
||||
Channel_Name: 'Nome do Canal',
|
||||
Channels: 'Canais',
|
||||
Chats: 'Chats',
|
||||
Close: 'Fechar',
|
||||
Close_emoji_selector: 'Fechar selector de emoticons',
|
||||
Choose: 'Escolher',
|
||||
Choose_from_library: 'Escolher da biblioteca',
|
||||
Code: 'Código',
|
||||
Collaborative: 'Colaborativa',
|
||||
Confirm: 'Confirmar',
|
||||
Connect: 'Ligar',
|
||||
Connect_to_a_server: 'Ligue-se a um servidor',
|
||||
Connected: 'Ligado',
|
||||
Connecting: 'A ligar...',
|
||||
Continue_with: 'Continuar com',
|
||||
Copied_to_clipboard: 'Copiado para a área de transferência!',
|
||||
Copy: 'Copiar',
|
||||
Permalink: 'Link permanente',
|
||||
Create_account: 'Criar uma conta',
|
||||
Create_Channel: 'Criar Canal',
|
||||
Created_snippet: 'Criado um extracto',
|
||||
Create_a_new_workspace: 'Criar um novo espaço de trabalho',
|
||||
Create: 'Criar',
|
||||
Delete_Room_Warning: 'Apagar uma sala irá remover todas as mensagens contidas nela. Isto não pode ser desfeito.',
|
||||
delete: 'apagar',
|
||||
Delete: 'Apagar',
|
||||
DELETE: 'APAGAR',
|
||||
description: 'descrição',
|
||||
Description: 'Descrição',
|
||||
Disable_notifications: 'Desactivar notificações',
|
||||
Direct_Messages: 'Mensagens Directas',
|
||||
Dont_Have_An_Account: 'Não tem uma conta?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Você quer mesmo {{key}} esta sala?',
|
||||
edit: 'editar',
|
||||
erasing_room: 'apagando sala',
|
||||
Edit: 'Editar',
|
||||
Email_or_password_field_is_empty: 'O campo de e-mail ou palavra-passe está vazio',
|
||||
Email: 'E-mail',
|
||||
email: 'e-mail',
|
||||
Enable_notifications: 'Activar notificações',
|
||||
Everyone_can_access_this_channel: 'Todos podem aceder a este canal',
|
||||
Error_uploading: 'Erro ao fazer o envio',
|
||||
Favorites: 'Favoritos',
|
||||
Files: 'Ficheiros',
|
||||
File_description: 'Descrição do ficheiro',
|
||||
File_name: 'Nome do ficheiro',
|
||||
Finish_recording: 'Terminar a gravação',
|
||||
For_your_security_you_must_enter_your_current_password_to_continue: 'Para sua segurança, você deve escrever a sua palavra-passe actual para continuar',
|
||||
Forgot_my_password: 'Esqueci minha palavra-passe',
|
||||
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver registado, enviaremos instruções sobre como repor a sua palavra-passe. Se você não receber um e-mail em breve, volte e tente novamente.',
|
||||
Forgot_password: 'Esquecer palavra-passe',
|
||||
Forgot_Password: 'Esquecer Palavra-passe',
|
||||
Group_by_favorites: 'Agrupar por favoritos',
|
||||
Group_by_type: 'Agrupar por tipo',
|
||||
Has_joined_the_channel: 'Entrou no canal',
|
||||
Has_joined_the_conversation: 'Entrou na conversa',
|
||||
Has_left_the_channel: 'Saiu do canal',
|
||||
Invisible: 'Invisível',
|
||||
Invite: 'Convidar',
|
||||
is_a_valid_RocketChat_instance: 'é uma instância válida do Rocket.Chat',
|
||||
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
|
||||
is_typing: 'está a escrever',
|
||||
Invalid_server_version: 'O servidor ao qual esta tentando ligar-se, utiliza uma versão que não é suporta pela aplicação: {{currentVersion}}.\n\nA versão mínima requerida é {{minVersion}}',
|
||||
Join_the_community: 'Junte-se à comunidade',
|
||||
Join: 'Entrar',
|
||||
Just_invited_people_can_access_this_channel: 'Apenas utilizadores convidados podem aceder a este canal',
|
||||
Language: 'Idioma',
|
||||
last_message: 'última mensagem',
|
||||
Leave_channel: 'Sair do canal',
|
||||
leaving_room: 'a sair da sala',
|
||||
leave: 'sair',
|
||||
Legal: 'Legal',
|
||||
Livechat: 'Livechat',
|
||||
Login: 'Entrar',
|
||||
Login_error: 'As suas credenciais foram rejeitadas! Por favor, tente novamente.',
|
||||
Login_with: 'Entrar com',
|
||||
Logout: 'Sair',
|
||||
members: 'membros',
|
||||
Members: 'Membros',
|
||||
Mentioned_Messages: 'Mensagens Mencionadas',
|
||||
mentioned: 'mencionado',
|
||||
Mentions: 'Menções',
|
||||
Message_accessibility: 'Mensagem de {{user}} às {{time}}: {{message}}',
|
||||
Message_actions: 'Acções de mensagem',
|
||||
Message_pinned: 'Mensagem afixada',
|
||||
Message_removed: 'Mensagem removida',
|
||||
Messages: 'Mensagens',
|
||||
Microphone_Permission_Message: 'O Rocket Chat necessita de acesso ao seu microfone para que você possa enviar mensagens de áudio.',
|
||||
Microphone_Permission: 'Permissão de Microfone',
|
||||
Mute: 'Silenciar',
|
||||
muted: 'silenciado',
|
||||
My_servers: 'Meus servidores',
|
||||
N_people_reacted: '{{n}} pessoas reagiram',
|
||||
N_users: '{{n}} utilizadores',
|
||||
name: 'nome',
|
||||
Name: 'Nome',
|
||||
New_Message: 'Nova Mensagem',
|
||||
New_Password: 'Nova Palavra-passe',
|
||||
New_Server: 'Novo Servidor',
|
||||
Next: 'Próximo',
|
||||
No_files: 'Nenhum ficheiro',
|
||||
No_mentioned_messages: 'Nenhuma mensagem mencionada',
|
||||
No_pinned_messages: 'Nenhuma mensagem afixada',
|
||||
No_results_found: 'Nenhum resultado encontrado',
|
||||
No_starred_messages: 'Nenhuma mensagem marcada com estrela',
|
||||
No_announcement_provided: 'Nenhum anúncio fornecido.',
|
||||
No_description_provided: 'Nenhuma descrição fornecida.',
|
||||
No_topic_provided: 'Nenhum tópico fornecido.',
|
||||
No_Message: 'Nenhuma mensagem',
|
||||
No_Reactions: 'Nenhuma reação',
|
||||
Not_logged: 'Não ligado',
|
||||
Nothing_to_save: 'Nada para guardar!',
|
||||
Notify_active_in_this_room: 'Notifica utilizadores activos nesta sala',
|
||||
Notify_all_in_this_room: 'Notifica todos os utilizadores nesta sala',
|
||||
Offline: 'Desligado',
|
||||
Oops: 'Oops!',
|
||||
Online: 'Ligado',
|
||||
Only_authorized_users_can_write_new_messages: 'Apenas utilizadores autorizados podem escrever novas mensagens',
|
||||
Open_emoji_selector: 'Abra o selector de emoticons',
|
||||
Open_Source_Communication: 'Comunicação Open Source',
|
||||
Password: 'Palavra-passe',
|
||||
Permalink_copied_to_clipboard: 'Link permanente copiado para a área de transferência!',
|
||||
Pin: 'Afixar',
|
||||
Pinned_Messages: 'Mensagens Afixadas',
|
||||
pinned: 'afixada',
|
||||
Pinned: 'Afixada',
|
||||
Please_enter_your_password: 'Por favor, introduza a sua palavra-passe',
|
||||
Preferences_saved: 'Preferências guardadas!',
|
||||
Privacy_Policy: ' Política de Privacidade',
|
||||
Private_Channel: 'Canal Privado',
|
||||
Private_Groups: 'Grupos Privados',
|
||||
Private: 'Privado',
|
||||
Profile_saved_successfully: 'Perfil actualizado com sucesso!',
|
||||
Profile: 'Perfil',
|
||||
Public_Channel: 'Canal Público',
|
||||
Public: 'Público',
|
||||
Quote: 'Citar',
|
||||
Reactions_are_disabled: 'Reacções desactivadas',
|
||||
Reactions_are_enabled: 'Reacções activadas',
|
||||
Reactions: 'Reacções',
|
||||
Read_Only_Channel: 'Canal só de leitura',
|
||||
Read_Only: 'Só de Leitura',
|
||||
Register: 'Registar',
|
||||
Repeat_Password: 'Repita a palavra-passe',
|
||||
Reply: 'Responder',
|
||||
Resend: 'Reenviar',
|
||||
Reset_password: 'Repor palavra-passe',
|
||||
resetting_password: 'a repor palavra-passe',
|
||||
RESET: 'REPOR',
|
||||
Roles: 'Funções',
|
||||
Room_actions: 'Ações de sala',
|
||||
Room_changed_announcement: 'Anúncio da sala alterado para: {{announcement}} por {{userBy}}',
|
||||
Room_changed_description: 'Descrição da sala alterada para: {{description}} por {{userBy}}',
|
||||
Room_changed_privacy: 'Tipo de sala alterado para: {{type}} por {{userBy}}',
|
||||
Room_changed_topic: 'Tópico da sala alterado para: {{topic}} por {{userBy}}',
|
||||
Room_Files: 'Fiheiros da Sala',
|
||||
Room_Info_Edit: 'Editar Informação da Sala',
|
||||
Room_Info: 'Informação da Sala',
|
||||
Room_Members: 'Membros da Sala',
|
||||
Room_name_changed: 'Nome da sala alterado para: {{name}} por {{userBy}}',
|
||||
SAVE: 'GUARDAR',
|
||||
Save_Changes: 'Guardar Alterações',
|
||||
Save: 'Guardar',
|
||||
saving_preferences: 'a guardar preferências',
|
||||
saving_profile: 'a guardar perfil',
|
||||
saving_settings: 'a guardar configurações',
|
||||
Search_Messages: 'Pesquisar Mensagens',
|
||||
Search: 'Pesquisar',
|
||||
Select_Avatar: 'Seleccionar Avatar',
|
||||
Select_Users: 'Seleccionar Utilizadores',
|
||||
Send: 'Enviar',
|
||||
Send_audio_message: 'Enviar mensagem de áudio',
|
||||
Send_message: 'Enviar mensagem',
|
||||
Sent_an_attachment: 'Enviou um ficheiro',
|
||||
Server: 'Servidor',
|
||||
Servers: 'Servidores',
|
||||
Set_username_subtitle: 'O nome de utilizador é usado para permitir que outros mencionem você em mensagens',
|
||||
Settings: 'Definições',
|
||||
Settings_succesfully_changed: 'Definições guardadas com sucesso!',
|
||||
Share: 'Partilhar',
|
||||
Sign_in_your_server: 'Entre no seu servidor',
|
||||
Sign_Up: 'Inscreva-se',
|
||||
Some_field_is_invalid_or_empty: 'Algum campo é inválido ou está vazio',
|
||||
Sorting_by: 'Ordenar por {{key}}',
|
||||
Star_room: 'Marcar como favorito',
|
||||
Star: 'Dar estrela',
|
||||
Starred_Messages: 'Mensagens com estrela',
|
||||
starred: 'deu uma estrela',
|
||||
Starred: 'Deu uma estrela',
|
||||
Start_of_conversation: 'Início da conversa',
|
||||
Submit: 'Enviar',
|
||||
Take_a_photo: 'Tirar uma foto',
|
||||
tap_to_change_status: 'toque para alterar o estado',
|
||||
Tap_to_view_servers_list: 'Toque para ver a lista de servidores',
|
||||
Terms_of_Service: ' Termos do Serviço ',
|
||||
The_URL_is_invalid: 'O URL que você inseriu é inválido. Verifique e tente novamente, por favor!',
|
||||
There_was_an_error_while_action: 'Houve um erro enquanto {{action}}!',
|
||||
This_room_is_blocked: 'Esta sala está bloqueada',
|
||||
This_room_is_read_only: 'Esta sala é apenas de leitura',
|
||||
Timezone: 'Fuso Horário',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: 'tópico',
|
||||
Topic: 'Tópico',
|
||||
Try_again: 'Tente novamente',
|
||||
Two_Factor_Authentication: 'Autenticação 2FA',
|
||||
Type_the_channel_name_here: 'Escreva o nome do canal aqui',
|
||||
unarchive: 'desarquivar',
|
||||
UNARCHIVE: 'DESARQUIVAR',
|
||||
Unblock_user: 'Desbloquear utilizador',
|
||||
Unmute: 'Retirar silêncio',
|
||||
unmuted: 'silêncio removido',
|
||||
Unpin: 'Desafixar',
|
||||
unread_messages: 'não lidas',
|
||||
Unread: 'Não lidas',
|
||||
Unread_on_top: 'Não lidas no topo',
|
||||
Unstar: 'Retirar estrela',
|
||||
Updating: 'A actualizar...',
|
||||
Uploading: 'A enviar',
|
||||
Upload_file_question_mark: 'Enviar ficheiro?',
|
||||
User_added_by: 'Utilizador {{userAdded}} adicionado por {{userBy}}',
|
||||
User_has_been_key: 'Utilizador foi {{key}}!',
|
||||
User_is_no_longer_role_by_: '{{userBy}} removeu o estatuto de {{role}} de {{user}}',
|
||||
User_muted_by: 'Utilizador {{userMuted}} foi silenciado por {{userBy}}',
|
||||
User_removed_by: 'Utilizador {{userRemoved}} removido por {{userBy}}',
|
||||
User_sent_an_attachment: '{{user}} enviou um ficheiro',
|
||||
User_unmuted_by: '{{userBy}} retirou o silêncio a {{userUnmuted}}',
|
||||
User_was_set_role_by_: '{{userBy}} deu estatuto de {{role}} a {{user}}',
|
||||
Username_is_empty: 'O nome de utilizador está vazio',
|
||||
Username: 'Nome de utilizador',
|
||||
Username_or_email: 'Nome de utilizador ou e-mail',
|
||||
Validating: 'A validar',
|
||||
Video_call: 'Video chamada',
|
||||
Voice_call: 'Chamada de voz',
|
||||
Welcome: 'Bem vindo(a)',
|
||||
Welcome_to_RocketChat: 'Bem vindo(a) ao Rocket.Chat',
|
||||
Whats_your_2fa: 'Qual é o seu código 2FA?',
|
||||
Yes_action_it: 'Sim, {{action}}!',
|
||||
Yesterday: 'Ontem',
|
||||
You_are_in_preview_mode: 'Você está no modo de pré-visualização',
|
||||
You_are_offline: 'Você está desligado',
|
||||
You_can_search_using_RegExp_eg: 'Você pode pesquisar usando RegEx. por exemplo, `/^text$/i`',
|
||||
You_colon: 'Você: ',
|
||||
you_were_mentioned: 'você foi mencionado',
|
||||
you: 'você',
|
||||
You: 'Você',
|
||||
You_will_not_be_able_to_recover_this_message: 'Você será incapaz de recuperar esta mensagem!'
|
||||
};
|
122
app/index.js
122
app/index.js
|
@ -6,6 +6,7 @@ import { Provider } from 'react-redux';
|
|||
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import { appInit } from './actions';
|
||||
import { deepLinkingOpen } from './actions/deepLinking';
|
||||
import OnboardingView from './views/OnboardingView';
|
||||
import NewServerView from './views/NewServerView';
|
||||
|
@ -28,41 +29,35 @@ import MentionedMessagesView from './views/MentionedMessagesView';
|
|||
import StarredMessagesView from './views/StarredMessagesView';
|
||||
import SearchMessagesView from './views/SearchMessagesView';
|
||||
import PinnedMessagesView from './views/PinnedMessagesView';
|
||||
import ThreadMessagesView from './views/ThreadMessagesView';
|
||||
import SelectedUsersView from './views/SelectedUsersView';
|
||||
import CreateChannelView from './views/CreateChannelView';
|
||||
import LegalView from './views/LegalView';
|
||||
import TermsServiceView from './views/TermsServiceView';
|
||||
import PrivacyPolicyView from './views/PrivacyPolicyView';
|
||||
import ForgotPasswordView from './views/ForgotPasswordView';
|
||||
import RegisterView from './views/RegisterView';
|
||||
import OAuthView from './views/OAuthView';
|
||||
import SetUsernameView from './views/SetUsernameView';
|
||||
import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from './constants/colors';
|
||||
import parseQuery from './lib/methods/helpers/parseQuery';
|
||||
import { initializePushNotifications } from './push';
|
||||
import { initializePushNotifications, onNotification } from './push';
|
||||
import store from './lib/createStore';
|
||||
|
||||
useScreens();
|
||||
initializePushNotifications();
|
||||
|
||||
const handleOpenURL = ({ url }) => {
|
||||
const parseDeepLinking = (url) => {
|
||||
if (url) {
|
||||
url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
|
||||
const regex = /^(room|auth)\?/;
|
||||
if (url.match(regex)) {
|
||||
url = url.replace(regex, '');
|
||||
const params = parseQuery(url);
|
||||
store.dispatch(deepLinkingOpen(params));
|
||||
url = url.replace(regex, '').trim();
|
||||
if (url) {
|
||||
return parseQuery(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Linking
|
||||
.getInitialURL()
|
||||
.then(url => handleOpenURL({ url }))
|
||||
.catch(e => console.warn(e));
|
||||
Linking.addEventListener('url', handleOpenURL);
|
||||
|
||||
const defaultHeader = {
|
||||
headerStyle: {
|
||||
backgroundColor: HEADER_BACKGROUND
|
||||
|
@ -84,15 +79,8 @@ const OutsideStack = createStackNavigator({
|
|||
LoginSignupView,
|
||||
LoginView,
|
||||
ForgotPasswordView,
|
||||
RegisterView
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
||||
const LegalStack = createStackNavigator({
|
||||
LegalView,
|
||||
TermsServiceView,
|
||||
PrivacyPolicyView
|
||||
RegisterView,
|
||||
LegalView
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
@ -105,7 +93,6 @@ const OAuthStack = createStackNavigator({
|
|||
|
||||
const OutsideStackModal = createStackNavigator({
|
||||
OutsideStack,
|
||||
LegalStack,
|
||||
OAuthStack
|
||||
},
|
||||
{
|
||||
|
@ -126,23 +113,54 @@ const ChatsStack = createStackNavigator({
|
|||
StarredMessagesView,
|
||||
SearchMessagesView,
|
||||
PinnedMessagesView,
|
||||
SelectedUsersView
|
||||
SelectedUsersView,
|
||||
ThreadMessagesView
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
||||
ChatsStack.navigationOptions = ({ navigation }) => {
|
||||
let drawerLockMode = 'unlocked';
|
||||
if (navigation.state.index > 0) {
|
||||
drawerLockMode = 'locked-closed';
|
||||
}
|
||||
return {
|
||||
drawerLockMode
|
||||
};
|
||||
};
|
||||
|
||||
const ProfileStack = createStackNavigator({
|
||||
ProfileView
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
||||
ProfileStack.navigationOptions = ({ navigation }) => {
|
||||
let drawerLockMode = 'unlocked';
|
||||
if (navigation.state.index > 0) {
|
||||
drawerLockMode = 'locked-closed';
|
||||
}
|
||||
return {
|
||||
drawerLockMode
|
||||
};
|
||||
};
|
||||
|
||||
const SettingsStack = createStackNavigator({
|
||||
SettingsView
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
||||
SettingsStack.navigationOptions = ({ navigation }) => {
|
||||
let drawerLockMode = 'unlocked';
|
||||
if (navigation.state.index > 0) {
|
||||
drawerLockMode = 'locked-closed';
|
||||
}
|
||||
return {
|
||||
drawerLockMode
|
||||
};
|
||||
};
|
||||
|
||||
const ChatsDrawer = createDrawerNavigator({
|
||||
ChatsStack,
|
||||
ProfileStack,
|
||||
|
@ -184,12 +202,48 @@ const App = createAppContainer(createSwitchNavigator(
|
|||
}
|
||||
));
|
||||
|
||||
export default () => (
|
||||
<Provider store={store}>
|
||||
<App
|
||||
ref={(navigatorRef) => {
|
||||
Navigation.setTopLevelNavigator(navigatorRef);
|
||||
}}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
export default class Root extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.init();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listenerTimeout = setTimeout(() => {
|
||||
Linking.addEventListener('url', ({ url }) => {
|
||||
const parsedDeepLinkingURL = parseDeepLinking(url);
|
||||
if (parsedDeepLinkingURL) {
|
||||
store.dispatch(deepLinkingOpen(parsedDeepLinkingURL));
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.listenerTimeout);
|
||||
}
|
||||
|
||||
init = async() => {
|
||||
const [notification, deepLinking] = await Promise.all([initializePushNotifications(), Linking.getInitialURL()]);
|
||||
const parsedDeepLinkingURL = parseDeepLinking(deepLinking);
|
||||
if (notification) {
|
||||
onNotification(notification);
|
||||
} else if (parsedDeepLinkingURL) {
|
||||
store.dispatch(deepLinkingOpen(parsedDeepLinkingURL));
|
||||
} else {
|
||||
store.dispatch(appInit());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<App
|
||||
ref={(navigatorRef) => {
|
||||
Navigation.setTopLevelNavigator(navigatorRef);
|
||||
}}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +1,83 @@
|
|||
import { InteractionManager } from 'react-native';
|
||||
import semver from 'semver';
|
||||
|
||||
import reduxStore from '../createStore';
|
||||
import database from '../realm';
|
||||
import * as actions from '../../actions';
|
||||
import log from '../../utils/log';
|
||||
|
||||
const getLastMessage = () => {
|
||||
const setting = database.objects('customEmojis').sorted('_updatedAt', true)[0];
|
||||
return setting && setting._updatedAt;
|
||||
const getUpdatedSince = () => {
|
||||
const emoji = database.objects('customEmojis').sorted('_updatedAt', true)[0];
|
||||
return emoji && emoji._updatedAt.toISOString();
|
||||
};
|
||||
|
||||
// TODO: fix api (get emojis by date/version....)
|
||||
const create = (customEmojis) => {
|
||||
if (customEmojis && customEmojis.length) {
|
||||
customEmojis.forEach((emoji) => {
|
||||
try {
|
||||
database.create('customEmojis', emoji, true);
|
||||
} catch (e) {
|
||||
log('getEmojis create', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default async function() {
|
||||
try {
|
||||
const lastMessage = getLastMessage();
|
||||
// RC 0.61.0
|
||||
const result = await this.sdk.get('emoji-custom');
|
||||
let { emojis } = result;
|
||||
emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage);
|
||||
if (emojis.length === 0) {
|
||||
return;
|
||||
}
|
||||
emojis = this._prepareEmojis(emojis);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
database.write(() => {
|
||||
emojis.forEach((emoji) => {
|
||||
try {
|
||||
database.create('customEmojis', emoji, true);
|
||||
} catch (e) {
|
||||
log('create custom emojis', e);
|
||||
}
|
||||
const serverVersion = reduxStore.getState().server.version;
|
||||
const updatedSince = getUpdatedSince();
|
||||
|
||||
// if server version is lower than 0.75.0, fetches from old api
|
||||
if (semver.lt(serverVersion, '0.75.0')) {
|
||||
// RC 0.61.0
|
||||
const result = await this.sdk.get('emoji-custom');
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
let { emojis } = result;
|
||||
emojis = emojis.filter(emoji => !updatedSince || emoji._updatedAt > updatedSince);
|
||||
database.write(() => {
|
||||
create(emojis);
|
||||
});
|
||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(result.emojis)));
|
||||
});
|
||||
});
|
||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(emojis)));
|
||||
} else {
|
||||
const params = {};
|
||||
if (updatedSince) {
|
||||
params.updatedSince = updatedSince;
|
||||
}
|
||||
|
||||
// RC 0.75.0
|
||||
const result = await this.sdk.get('emoji-custom.list', params);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
InteractionManager.runAfterInteractions(
|
||||
() => database.write(() => {
|
||||
const { emojis } = result;
|
||||
create(emojis.update);
|
||||
|
||||
if (emojis.delete && emojis.delete.length) {
|
||||
emojis.delete.forEach((emoji) => {
|
||||
try {
|
||||
const emojiRecord = database.objectForPrimaryKey('customEmojis', emoji._id);
|
||||
if (emojiRecord) {
|
||||
database.delete(emojiRecord);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getEmojis delete', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const allEmojis = database.objects('customEmojis');
|
||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(allEmojis)));
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getCustomEmojis', e);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,75 @@
|
|||
import { InteractionManager } from 'react-native';
|
||||
import semver from 'semver';
|
||||
|
||||
import database from '../realm';
|
||||
import log from '../../utils/log';
|
||||
import defaultPermissions from '../../constants/permissions';
|
||||
import reduxStore from '../createStore';
|
||||
|
||||
const getUpdatedSince = () => {
|
||||
const permissions = database.objects('permissions').sorted('_updatedAt', true)[0];
|
||||
return permissions && permissions._updatedAt.toISOString();
|
||||
};
|
||||
|
||||
const create = (permissions) => {
|
||||
if (permissions && permissions.length) {
|
||||
permissions.forEach((permission) => {
|
||||
try {
|
||||
database.create('permissions', permission, true);
|
||||
} catch (e) {
|
||||
log('getPermissions create', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default async function() {
|
||||
try {
|
||||
// RC 0.66.0
|
||||
const result = await this.sdk.get('permissions.list');
|
||||
const serverVersion = reduxStore.getState().server.version;
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
const permissions = result.permissions.filter(permission => defaultPermissions.includes(permission._id));
|
||||
permissions
|
||||
.map((permission) => {
|
||||
permission._updatedAt = new Date();
|
||||
permission.roles = permission.roles.map(role => ({ value: role }));
|
||||
return permission;
|
||||
// if server version is lower than 0.73.0, fetches from old api
|
||||
if (semver.lt(serverVersion, '0.73.0')) {
|
||||
// RC 0.66.0
|
||||
const result = await this.sdk.get('permissions.list');
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
database.write(() => {
|
||||
create(result.permissions);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const params = {};
|
||||
const updatedSince = getUpdatedSince();
|
||||
if (updatedSince) {
|
||||
params.updatedSince = updatedSince;
|
||||
}
|
||||
// RC 0.73.0
|
||||
const result = await this.sdk.get('permissions.listAll', params);
|
||||
|
||||
InteractionManager.runAfterInteractions(
|
||||
() => database.write(() => permissions.forEach((permission) => {
|
||||
try {
|
||||
database.create('permissions', permission, true);
|
||||
} catch (e) {
|
||||
log('getPermissions create', e);
|
||||
}
|
||||
}))
|
||||
);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
InteractionManager.runAfterInteractions(
|
||||
() => database.write(() => {
|
||||
create(result.update);
|
||||
|
||||
if (result.delete && result.delete.length) {
|
||||
result.delete.forEach((p) => {
|
||||
try {
|
||||
const permission = database.objectForPrimaryKey('permissions', p._id);
|
||||
if (permission) {
|
||||
database.delete(permission);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getPermissions delete', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getPermissions', e);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { InteractionManager } from 'react-native';
|
||||
|
||||
import database from '../realm';
|
||||
import log from '../../utils/log';
|
||||
|
||||
export default async function() {
|
||||
try {
|
||||
// RC 0.70.0
|
||||
const result = await this.sdk.get('roles.list');
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { roles } = result;
|
||||
|
||||
if (roles && roles.length) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
database.write(() => roles.forEach((role) => {
|
||||
try {
|
||||
database.create('roles', role, true);
|
||||
} catch (e) {
|
||||
log('getRoles create', e);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('getRoles', e);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,9 @@ export const merge = (subscription, room) => {
|
|||
subscription.archived = room.archived;
|
||||
subscription.joinCodeRequired = room.joinCodeRequired;
|
||||
subscription.broadcast = room.broadcast;
|
||||
if (!subscription.roles || !subscription.roles.length) {
|
||||
subscription.roles = [];
|
||||
}
|
||||
|
||||
if (room.muted && room.muted.length) {
|
||||
subscription.muted = room.muted.filter(user => user).map(user => ({ value: user }));
|
||||
|
@ -31,11 +34,6 @@ export const merge = (subscription, room) => {
|
|||
subscription.muted = [];
|
||||
}
|
||||
}
|
||||
if (subscription.roles && subscription.roles.length) {
|
||||
subscription.roles = subscription.roles.map(role => (role.value ? role : { value: role }));
|
||||
} else {
|
||||
subscription.roles = [];
|
||||
}
|
||||
|
||||
if (subscription.mobilePushNotifications === 'nothing') {
|
||||
subscription.notifications = true;
|
||||
|
|
|
@ -18,7 +18,12 @@ function normalizeAttachments(msg) {
|
|||
}
|
||||
|
||||
export default (msg) => {
|
||||
if (!msg) { return; }
|
||||
/**
|
||||
* 2019-03-29: Realm object properties are *always* optional, but `u.username` is required
|
||||
* https://realm.io/docs/javascript/latest/#to-one-relationships
|
||||
*/
|
||||
if (!msg || !msg.u || !msg.u.username) { return; }
|
||||
|
||||
msg = normalizeAttachments(msg);
|
||||
msg.reactions = msg.reactions || [];
|
||||
// TODO: api problems
|
||||
|
|
|
@ -39,8 +39,18 @@ export default function loadMessagesForRoom(...args) {
|
|||
if (data && data.length) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
database.write(() => data.forEach((message) => {
|
||||
message = buildMessage(message);
|
||||
try {
|
||||
database.create('messages', buildMessage(message), true);
|
||||
database.create('messages', message, true);
|
||||
// if it's a thread "header"
|
||||
if (message.tlm) {
|
||||
database.create('threads', message, true);
|
||||
}
|
||||
// if it belongs to a thread
|
||||
if (message.tmid) {
|
||||
message.rid = message.tmid;
|
||||
database.create('threadMessages', message, true);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadMessagesForRoom -> create messages', e);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ async function load({ rid: roomId, lastOpen }) {
|
|||
lastUpdate = getLastUpdate(roomId);
|
||||
}
|
||||
// RC 0.60.0
|
||||
const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate, count: 50 });
|
||||
const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate });
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -31,16 +31,23 @@ export default function loadMissedMessages(...args) {
|
|||
if (data) {
|
||||
if (data.updated && data.updated.length) {
|
||||
const { updated } = data;
|
||||
updated.forEach(buildMessage);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
database.write(() => updated.forEach((message) => {
|
||||
try {
|
||||
message = buildMessage(message);
|
||||
database.create('messages', message, true);
|
||||
// if it's a thread "header"
|
||||
if (message.tlm) {
|
||||
database.create('threads', message, true);
|
||||
}
|
||||
if (message.tmid) {
|
||||
message.rid = message.tmid;
|
||||
database.create('threadMessages', message, true);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadMissedMessages -> create messages', e);
|
||||
}
|
||||
}));
|
||||
resolve(updated);
|
||||
});
|
||||
}
|
||||
if (data.deleted && data.deleted.length) {
|
||||
|
@ -51,6 +58,10 @@ export default function loadMissedMessages(...args) {
|
|||
deleted.forEach((m) => {
|
||||
const message = database.objects('messages').filtered('_id = $0', m._id);
|
||||
database.delete(message);
|
||||
const thread = database.objects('threads').filtered('_id = $0', m._id);
|
||||
database.delete(thread);
|
||||
const threadMessage = database.objects('threadMessages').filtered('_id = $0', m._id);
|
||||
database.delete(threadMessage);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -59,7 +70,7 @@ export default function loadMissedMessages(...args) {
|
|||
});
|
||||
}
|
||||
}
|
||||
resolve([]);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
log('loadMissedMessages', e);
|
||||
reject(e);
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { InteractionManager } from 'react-native';
|
||||
import EJSON from 'ejson';
|
||||
|
||||
import buildMessage from './helpers/buildMessage';
|
||||
import database from '../realm';
|
||||
import log from '../../utils/log';
|
||||
|
||||
async function load({ tmid, offset }) {
|
||||
try {
|
||||
// RC 1.0
|
||||
const result = await this.sdk.get('chat.getThreadMessages', {
|
||||
tmid, count: 50, offset, sort: { ts: -1 }, query: { _hidden: { $ne: true } }
|
||||
});
|
||||
if (!result || !result.success) {
|
||||
return [];
|
||||
}
|
||||
return result.messages;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default function loadThreadMessages({ tmid, offset = 0 }) {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
const data = await load.call(this, { tmid, offset });
|
||||
|
||||
if (data && data.length) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
database.write(() => data.forEach((m) => {
|
||||
try {
|
||||
const message = buildMessage(EJSON.fromJSONValue(m));
|
||||
message.rid = tmid;
|
||||
database.create('threadMessages', message, true);
|
||||
} catch (e) {
|
||||
log('loadThreadMessages -> create messages', e);
|
||||
}
|
||||
}));
|
||||
return resolve(data);
|
||||
});
|
||||
} else {
|
||||
return resolve([]);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadThreadMessages', e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -29,7 +29,7 @@ export async function cancelUpload(path) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function sendFileMessage(rid, fileInfo) {
|
||||
export async function sendFileMessage(rid, fileInfo, tmid) {
|
||||
try {
|
||||
const data = await RNFetchBlob.wrap(fileInfo.path);
|
||||
if (!fileInfo.size) {
|
||||
|
@ -86,6 +86,8 @@ export async function sendFileMessage(rid, fileInfo) {
|
|||
name: completeResult.name,
|
||||
description: completeResult.description,
|
||||
url: completeResult.path
|
||||
}, {
|
||||
tmid
|
||||
});
|
||||
|
||||
database.write(() => {
|
||||
|
|
|
@ -5,12 +5,13 @@ import reduxStore from '../createStore';
|
|||
import log from '../../utils/log';
|
||||
import random from '../../utils/random';
|
||||
|
||||
export const getMessage = (rid, msg = {}) => {
|
||||
export const getMessage = (rid, msg = '', tmid) => {
|
||||
const _id = random(17);
|
||||
const message = {
|
||||
_id,
|
||||
rid,
|
||||
msg,
|
||||
tmid,
|
||||
ts: new Date(),
|
||||
_updatedAt: new Date(),
|
||||
status: messagesStatus.TEMP,
|
||||
|
@ -30,21 +31,28 @@ export const getMessage = (rid, msg = {}) => {
|
|||
};
|
||||
|
||||
export async function sendMessageCall(message) {
|
||||
const { _id, rid, msg } = message;
|
||||
const {
|
||||
_id, rid, msg, tmid
|
||||
} = message;
|
||||
// RC 0.60.0
|
||||
const data = await this.sdk.post('chat.sendMessage', { message: { _id, rid, msg } });
|
||||
const data = await this.sdk.post('chat.sendMessage', {
|
||||
message: {
|
||||
_id, rid, msg, tmid
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export default async function(rid, msg) {
|
||||
export default async function(rid, msg, tmid) {
|
||||
try {
|
||||
const message = getMessage(rid, msg);
|
||||
const room = database.objects('subscriptions').filtered('rid == $0', rid);
|
||||
const message = getMessage(rid, msg, tmid);
|
||||
const [room] = database.objects('subscriptions').filtered('rid == $0', rid);
|
||||
|
||||
// TODO: do we need this?
|
||||
database.write(() => {
|
||||
room.lastMessage = message;
|
||||
});
|
||||
if (room) {
|
||||
database.write(() => {
|
||||
room.draftMessage = null;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const ret = await sendMessageCall.call(this, message);
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import EJSON from 'ejson';
|
||||
|
||||
import log from '../../../utils/log';
|
||||
import protectedFunction from '../helpers/protectedFunction';
|
||||
import buildMessage from '../helpers/buildMessage';
|
||||
import database from '../../realm';
|
||||
import debounce from '../../../utils/debounce';
|
||||
|
||||
const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
|
||||
const removeListener = listener => listener.stop();
|
||||
|
||||
let promises;
|
||||
let timer = null;
|
||||
let connectedListener;
|
||||
let disconnectedListener;
|
||||
|
||||
export default function subscribeRoom({ rid }) {
|
||||
if (promises) {
|
||||
promises.then(unsubscribe);
|
||||
promises = false;
|
||||
}
|
||||
let promises;
|
||||
let timer = null;
|
||||
let connectedListener;
|
||||
let disconnectedListener;
|
||||
let notifyRoomListener;
|
||||
let messageReceivedListener;
|
||||
const typingTimeouts = {};
|
||||
const loop = () => {
|
||||
if (timer) {
|
||||
return;
|
||||
|
@ -41,6 +45,116 @@ export default function subscribeRoom({ rid }) {
|
|||
}
|
||||
};
|
||||
|
||||
const getUserTyping = username => (
|
||||
database
|
||||
.memoryDatabase.objects('usersTyping')
|
||||
.filtered('rid = $0 AND username = $1', rid, username)
|
||||
);
|
||||
|
||||
const removeUserTyping = (username) => {
|
||||
const userTyping = getUserTyping(username);
|
||||
try {
|
||||
database.memoryDatabase.write(() => {
|
||||
database.memoryDatabase.delete(userTyping);
|
||||
});
|
||||
|
||||
if (typingTimeouts[username]) {
|
||||
clearTimeout(typingTimeouts[username]);
|
||||
typingTimeouts[username] = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('TCL: removeUserTyping -> error', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addUserTyping = (username) => {
|
||||
const userTyping = getUserTyping(username);
|
||||
// prevent duplicated
|
||||
if (userTyping.length === 0) {
|
||||
try {
|
||||
database.memoryDatabase.write(() => {
|
||||
database.memoryDatabase.create('usersTyping', { rid, username });
|
||||
});
|
||||
|
||||
if (typingTimeouts[username]) {
|
||||
clearTimeout(typingTimeouts[username]);
|
||||
typingTimeouts[username] = null;
|
||||
}
|
||||
|
||||
typingTimeouts[username] = setTimeout(() => {
|
||||
removeUserTyping(username);
|
||||
}, 10000);
|
||||
} catch (error) {
|
||||
console.log('TCL: addUserTyping -> error', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifyRoomReceived = protectedFunction((ddpMessage) => {
|
||||
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
|
||||
if (rid !== _rid) {
|
||||
return;
|
||||
}
|
||||
if (ev === 'typing') {
|
||||
const [username, typing] = ddpMessage.fields.args;
|
||||
if (typing) {
|
||||
addUserTyping(username);
|
||||
} else {
|
||||
removeUserTyping(username);
|
||||
}
|
||||
} else if (ev === 'deleteMessage') {
|
||||
database.write(() => {
|
||||
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {
|
||||
const { _id } = ddpMessage.fields.args[0];
|
||||
const message = database.objects('messages').filtered('_id = $0', _id);
|
||||
database.delete(message);
|
||||
const thread = database.objects('threads').filtered('_id = $0', _id);
|
||||
database.delete(thread);
|
||||
const threadMessage = database.objects('threadMessages').filtered('_id = $0', _id);
|
||||
database.delete(threadMessage);
|
||||
const cleanTmids = database.objects('messages').filtered('tmid = $0', _id).snapshot();
|
||||
if (cleanTmids && cleanTmids.length) {
|
||||
cleanTmids.forEach((m) => {
|
||||
m.tmid = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const read = debounce(() => {
|
||||
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||
if (room._id) {
|
||||
this.readMessages(rid);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const handleMessageReceived = protectedFunction((ddpMessage) => {
|
||||
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
|
||||
if (rid !== message.rid) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
database.write(() => {
|
||||
database.create('messages', message, true);
|
||||
// if it's a thread "header"
|
||||
if (message.tlm) {
|
||||
database.create('threads', message, true);
|
||||
} else if (message.tmid) {
|
||||
message.rid = message.tmid;
|
||||
database.create('threadMessages', message, true);
|
||||
}
|
||||
});
|
||||
|
||||
read();
|
||||
} catch (e) {
|
||||
console.warn('handleMessageReceived', e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const stop = () => {
|
||||
if (promises) {
|
||||
promises.then(unsubscribe);
|
||||
|
@ -54,12 +168,32 @@ export default function subscribeRoom({ rid }) {
|
|||
disconnectedListener.then(removeListener);
|
||||
disconnectedListener = false;
|
||||
}
|
||||
if (notifyRoomListener) {
|
||||
notifyRoomListener.then(removeListener);
|
||||
notifyRoomListener = false;
|
||||
}
|
||||
if (messageReceivedListener) {
|
||||
messageReceivedListener.then(removeListener);
|
||||
messageReceivedListener = false;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = false;
|
||||
Object.keys(typingTimeouts).forEach((key) => {
|
||||
if (typingTimeouts[key]) {
|
||||
clearTimeout(typingTimeouts[key]);
|
||||
typingTimeouts[key] = null;
|
||||
}
|
||||
});
|
||||
database.memoryDatabase.write(() => {
|
||||
const usersTyping = database.memoryDatabase.objects('usersTyping').filtered('rid == $0', rid);
|
||||
database.memoryDatabase.delete(usersTyping);
|
||||
});
|
||||
};
|
||||
|
||||
connectedListener = this.sdk.onStreamData('connected', handleConnected);
|
||||
disconnectedListener = this.sdk.onStreamData('close', handleDisconnected);
|
||||
notifyRoomListener = this.sdk.onStreamData('stream-notify-room', handleNotifyRoomReceived);
|
||||
messageReceivedListener = this.sdk.onStreamData('stream-room-messages', handleMessageReceived);
|
||||
|
||||
try {
|
||||
promises = this.sdk.subscribeRoom(rid);
|
||||
|
|
207
app/lib/realm.js
207
app/lib/realm.js
|
@ -11,7 +11,8 @@ const serversSchema = {
|
|||
id: 'string',
|
||||
name: { type: 'string', optional: true },
|
||||
iconURL: { type: 'string', optional: true },
|
||||
roomsUpdatedAt: { type: 'date', optional: true }
|
||||
roomsUpdatedAt: { type: 'date', optional: true },
|
||||
version: 'string?'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -27,20 +28,12 @@ const settingsSchema = {
|
|||
}
|
||||
};
|
||||
|
||||
const permissionsRolesSchema = {
|
||||
name: 'permissionsRoles',
|
||||
primaryKey: 'value',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const permissionsSchema = {
|
||||
name: 'permissions',
|
||||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
roles: { type: 'list', objectType: 'permissionsRoles' },
|
||||
roles: 'string[]',
|
||||
_updatedAt: { type: 'date', optional: true }
|
||||
}
|
||||
};
|
||||
|
@ -54,14 +47,6 @@ const roomsSchema = {
|
|||
}
|
||||
};
|
||||
|
||||
const subscriptionRolesSchema = {
|
||||
name: 'subscriptionRolesSchema',
|
||||
primaryKey: 'value',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const userMutedInRoomSchema = {
|
||||
name: 'usersMuted',
|
||||
primaryKey: 'value',
|
||||
|
@ -84,7 +69,7 @@ const subscriptionSchema = {
|
|||
rid: { type: 'string', indexed: true },
|
||||
open: { type: 'bool', optional: true },
|
||||
alert: { type: 'bool', optional: true },
|
||||
roles: { type: 'list', objectType: 'subscriptionRolesSchema' },
|
||||
roles: 'string[]',
|
||||
unread: { type: 'int', optional: true },
|
||||
userMentions: { type: 'int', optional: true },
|
||||
roomUpdatedAt: { type: 'date', optional: true },
|
||||
|
@ -101,7 +86,10 @@ const subscriptionSchema = {
|
|||
joinCodeRequired: { type: 'bool', optional: true },
|
||||
notifications: { type: 'bool', optional: true },
|
||||
muted: { type: 'list', objectType: 'usersMuted' },
|
||||
broadcast: { type: 'bool', optional: true }
|
||||
broadcast: { type: 'bool', optional: true },
|
||||
prid: { type: 'string', optional: true },
|
||||
draftMessage: { type: 'string', optional: true },
|
||||
lastThreadSync: 'date?'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -204,8 +192,73 @@ const messagesSchema = {
|
|||
rid: { type: 'string', indexed: true },
|
||||
ts: 'date',
|
||||
u: 'users',
|
||||
// mentions: [],
|
||||
// channels: [],
|
||||
alias: { type: 'string', optional: true },
|
||||
parseUrls: { type: 'bool', optional: true },
|
||||
groupable: { type: 'bool', optional: true },
|
||||
avatar: { type: 'string', optional: true },
|
||||
attachments: { type: 'list', objectType: 'attachment' },
|
||||
urls: { type: 'list', objectType: 'url', default: [] },
|
||||
_updatedAt: { type: 'date', optional: true },
|
||||
status: { type: 'int', optional: true },
|
||||
pinned: { type: 'bool', optional: true },
|
||||
starred: { type: 'bool', optional: true },
|
||||
editedBy: 'messagesEditedBy',
|
||||
reactions: { type: 'list', objectType: 'messagesReactions' },
|
||||
role: { type: 'string', optional: true },
|
||||
drid: { type: 'string', optional: true },
|
||||
dcount: { type: 'int', optional: true },
|
||||
dlm: { type: 'date', optional: true },
|
||||
tmid: { type: 'string', optional: true },
|
||||
tcount: { type: 'int', optional: true },
|
||||
tlm: { type: 'date', optional: true },
|
||||
replies: 'string[]'
|
||||
}
|
||||
};
|
||||
|
||||
const threadsSchema = {
|
||||
name: 'threads',
|
||||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
msg: { type: 'string', optional: true },
|
||||
t: { type: 'string', optional: true },
|
||||
rid: { type: 'string', indexed: true },
|
||||
ts: 'date',
|
||||
u: 'users',
|
||||
alias: { type: 'string', optional: true },
|
||||
parseUrls: { type: 'bool', optional: true },
|
||||
groupable: { type: 'bool', optional: true },
|
||||
avatar: { type: 'string', optional: true },
|
||||
attachments: { type: 'list', objectType: 'attachment' },
|
||||
urls: { type: 'list', objectType: 'url', default: [] },
|
||||
_updatedAt: { type: 'date', optional: true },
|
||||
status: { type: 'int', optional: true },
|
||||
pinned: { type: 'bool', optional: true },
|
||||
starred: { type: 'bool', optional: true },
|
||||
editedBy: 'messagesEditedBy',
|
||||
reactions: { type: 'list', objectType: 'messagesReactions' },
|
||||
role: { type: 'string', optional: true },
|
||||
drid: { type: 'string', optional: true },
|
||||
dcount: { type: 'int', optional: true },
|
||||
dlm: { type: 'date', optional: true },
|
||||
tmid: { type: 'string', optional: true },
|
||||
tcount: { type: 'int', optional: true },
|
||||
tlm: { type: 'date', optional: true },
|
||||
replies: 'string[]',
|
||||
draftMessage: 'string?'
|
||||
}
|
||||
};
|
||||
|
||||
const threadMessagesSchema = {
|
||||
name: 'threadMessages',
|
||||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
msg: { type: 'string', optional: true },
|
||||
t: { type: 'string', optional: true },
|
||||
rid: { type: 'string', indexed: true },
|
||||
ts: 'date',
|
||||
u: 'users',
|
||||
alias: { type: 'string', optional: true },
|
||||
parseUrls: { type: 'bool', optional: true },
|
||||
groupable: { type: 'bool', optional: true },
|
||||
|
@ -233,21 +286,13 @@ const frequentlyUsedEmojiSchema = {
|
|||
}
|
||||
};
|
||||
|
||||
const customEmojiAliasesSchema = {
|
||||
name: 'customEmojiAliases',
|
||||
primaryKey: 'value',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const customEmojisSchema = {
|
||||
name: 'customEmojis',
|
||||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
name: 'string',
|
||||
aliases: { type: 'list', objectType: 'customEmojiAliases' },
|
||||
aliases: 'string[]',
|
||||
extension: 'string',
|
||||
_updatedAt: { type: 'date', optional: true }
|
||||
}
|
||||
|
@ -278,21 +323,40 @@ const uploadsSchema = {
|
|||
}
|
||||
};
|
||||
|
||||
const usersTypingSchema = {
|
||||
name: 'usersTyping',
|
||||
properties: {
|
||||
rid: { type: 'string', indexed: true },
|
||||
username: { type: 'string', optional: true }
|
||||
}
|
||||
};
|
||||
|
||||
const activeUsersSchema = {
|
||||
name: 'activeUsers',
|
||||
primaryKey: 'id',
|
||||
properties: {
|
||||
id: 'string',
|
||||
name: 'string?',
|
||||
username: 'string?',
|
||||
status: 'string?',
|
||||
utcOffset: 'double?'
|
||||
}
|
||||
};
|
||||
|
||||
const schema = [
|
||||
settingsSchema,
|
||||
subscriptionSchema,
|
||||
subscriptionRolesSchema,
|
||||
messagesSchema,
|
||||
threadsSchema,
|
||||
threadMessagesSchema,
|
||||
usersSchema,
|
||||
roomsSchema,
|
||||
attachment,
|
||||
attachmentFields,
|
||||
messagesEditedBySchema,
|
||||
permissionsSchema,
|
||||
permissionsRolesSchema,
|
||||
url,
|
||||
frequentlyUsedEmojiSchema,
|
||||
customEmojiAliasesSchema,
|
||||
customEmojisSchema,
|
||||
messagesReactionsSchema,
|
||||
messagesReactionsUsernamesSchema,
|
||||
|
@ -301,6 +365,8 @@ const schema = [
|
|||
uploadsSchema
|
||||
];
|
||||
|
||||
const inMemorySchema = [usersTypingSchema, activeUsersSchema];
|
||||
|
||||
class DB {
|
||||
databases = {
|
||||
serversDB: new Realm({
|
||||
|
@ -308,7 +374,23 @@ class DB {
|
|||
schema: [
|
||||
serversSchema
|
||||
],
|
||||
schemaVersion: 1
|
||||
schemaVersion: 6,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
|
||||
const newServers = newRealm.objects('servers');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < newServers.length; i++) {
|
||||
newServers[i].roomsUpdatedAt = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
inMemoryDB: new Realm({
|
||||
path: 'memory.realm',
|
||||
schema: inMemorySchema,
|
||||
schemaVersion: 2,
|
||||
inMemory: true
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -332,17 +414,68 @@ class DB {
|
|||
return this.database.objects(...args);
|
||||
}
|
||||
|
||||
objectForPrimaryKey(...args) {
|
||||
return this.database.objectForPrimaryKey(...args);
|
||||
}
|
||||
|
||||
get database() {
|
||||
return this.databases.activeDB;
|
||||
}
|
||||
|
||||
get memoryDatabase() {
|
||||
return this.databases.inMemoryDB;
|
||||
}
|
||||
|
||||
setActiveDB(database = '') {
|
||||
const path = database.replace(/(^\w+:|^)\/\//, '');
|
||||
return this.databases.activeDB = new Realm({
|
||||
path: `${ path }.realm`,
|
||||
schema,
|
||||
schemaVersion: 1
|
||||
schemaVersion: 9,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < newSubs.length; i++) {
|
||||
newSubs[i].lastOpen = null;
|
||||
newSubs[i].ls = null;
|
||||
}
|
||||
const newMessages = newRealm.objects('messages');
|
||||
newRealm.delete(newMessages);
|
||||
const newThreads = newRealm.objects('threads');
|
||||
newRealm.delete(newThreads);
|
||||
const newThreadMessages = newRealm.objects('threadMessages');
|
||||
newRealm.delete(newThreadMessages);
|
||||
}
|
||||
if (newRealm.schemaVersion === 9) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
newRealm.delete(newSubs);
|
||||
const newEmojis = newRealm.objects('customEmojis');
|
||||
newRealm.delete(newEmojis);
|
||||
const newSettings = newRealm.objects('settings');
|
||||
newRealm.delete(newSettings);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
export default new DB();
|
||||
const db = new DB();
|
||||
export default db;
|
||||
|
||||
// Realm workaround for "Cannot create asynchronous query while in a write transaction"
|
||||
// inpired from https://github.com/realm/realm-js/issues/1188#issuecomment-359223918
|
||||
export function safeAddListener(results, callback, database = db) {
|
||||
if (!results || !results.addListener) {
|
||||
console.log('⚠️ safeAddListener called for non addListener-compliant object');
|
||||
return;
|
||||
}
|
||||
|
||||
if (database.isInTransaction) {
|
||||
setTimeout(() => {
|
||||
safeAddListener(results, callback);
|
||||
}, 50);
|
||||
} else {
|
||||
results.addListener(callback);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
import { AsyncStorage } from 'react-native';
|
||||
import foreach from 'lodash/forEach';
|
||||
import { AsyncStorage, InteractionManager } from 'react-native';
|
||||
import semver from 'semver';
|
||||
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
|
||||
|
||||
import reduxStore from './createStore';
|
||||
import defaultSettings from '../constants/settings';
|
||||
import messagesStatus from '../constants/messagesStatus';
|
||||
import database from './realm';
|
||||
import database, { safeAddListener } from './realm';
|
||||
import log from '../utils/log';
|
||||
import { isIOS, getBundleId } from '../utils/deviceInfo';
|
||||
import EventEmitter from '../utils/events';
|
||||
|
||||
import {
|
||||
setUser, setLoginServices, loginRequest, loginFailure, logout
|
||||
} from '../actions/login';
|
||||
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
|
||||
import { setActiveUser } from '../actions/activeUsers';
|
||||
import { someoneTyping, roomMessageReceived } from '../actions/room';
|
||||
import { setRoles } from '../actions/roles';
|
||||
|
||||
import subscribeRooms from './methods/subscriptions/rooms';
|
||||
import subscribeRoom from './methods/subscriptions/room';
|
||||
|
@ -28,11 +25,12 @@ import getSettings from './methods/getSettings';
|
|||
import getRooms from './methods/getRooms';
|
||||
import getPermissions from './methods/getPermissions';
|
||||
import getCustomEmoji from './methods/getCustomEmojis';
|
||||
import getRoles from './methods/getRoles';
|
||||
import canOpenRoom from './methods/canOpenRoom';
|
||||
|
||||
import _buildMessage from './methods/helpers/buildMessage';
|
||||
import loadMessagesForRoom from './methods/loadMessagesForRoom';
|
||||
import loadMissedMessages from './methods/loadMissedMessages';
|
||||
import loadThreadMessages from './methods/loadThreadMessages';
|
||||
|
||||
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
|
||||
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
|
||||
|
@ -43,7 +41,7 @@ import { roomsRequest } from '../actions/rooms';
|
|||
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
||||
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
|
||||
const returnAnArray = obj => obj || [];
|
||||
const MIN_ROCKETCHAT_VERSION = '0.66.0';
|
||||
const MIN_ROCKETCHAT_VERSION = '0.70.0';
|
||||
|
||||
const RocketChat = {
|
||||
TOKEN_KEY,
|
||||
|
@ -65,7 +63,7 @@ const RocketChat = {
|
|||
if (data.length) {
|
||||
return resolve(data[0]);
|
||||
}
|
||||
data.addListener(() => {
|
||||
safeAddListener(data, () => {
|
||||
if (!data.length) { return; }
|
||||
data.removeAllListeners();
|
||||
resolve(data[0]);
|
||||
|
@ -80,26 +78,24 @@ const RocketChat = {
|
|||
console.warn(`AsyncStorage error: ${ error.message }`);
|
||||
}
|
||||
},
|
||||
async testServer(server) {
|
||||
async getServerInfo(server) {
|
||||
try {
|
||||
const result = await fetch(`${ server }/api/v1/info`).then(response => response.json());
|
||||
if (result.success && result.info) {
|
||||
if (semver.lt(result.info.version, MIN_ROCKETCHAT_VERSION)) {
|
||||
const result = await fetch(`${ server }/api/info`).then(response => response.json());
|
||||
if (result.success) {
|
||||
if (semver.lt(result.version, MIN_ROCKETCHAT_VERSION)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Invalid_server_version',
|
||||
messageOptions: {
|
||||
currentVersion: result.info.version,
|
||||
currentVersion: result.version,
|
||||
minVersion: MIN_ROCKETCHAT_VERSION
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
log('testServer', e);
|
||||
log('getServerInfo', e);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
|
@ -114,25 +110,52 @@ const RocketChat = {
|
|||
reduxStore.dispatch(setUser(ddpMessage.fields));
|
||||
}
|
||||
|
||||
if (this._setUserTimer) {
|
||||
clearTimeout(this._setUserTimer);
|
||||
this._setUserTimer = null;
|
||||
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
|
||||
reduxStore.dispatch(setUser({ status: 'offline' }));
|
||||
}
|
||||
|
||||
this._setUserTimer = setTimeout(() => {
|
||||
reduxStore.dispatch(setActiveUser(this.activeUsers));
|
||||
this._setUserTimer = null;
|
||||
return this.activeUsers = {};
|
||||
}, 2000);
|
||||
if (!this._setUserTimer) {
|
||||
this._setUserTimer = setTimeout(() => {
|
||||
const batchUsers = this.activeUsers;
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
database.memoryDatabase.write(() => {
|
||||
Object.keys(batchUsers).forEach((key) => {
|
||||
if (batchUsers[key] && batchUsers[key].id) {
|
||||
try {
|
||||
const data = batchUsers[key];
|
||||
if (data.removed) {
|
||||
const userRecord = database.memoryDatabase.objectForPrimaryKey('activeUsers', data.id);
|
||||
if (userRecord) {
|
||||
userRecord.status = 'offline';
|
||||
}
|
||||
} else {
|
||||
database.memoryDatabase.create('activeUsers', data, true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
this._setUserTimer = null;
|
||||
return this.activeUsers = {};
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
const activeUser = reduxStore.getState().activeUsers[ddpMessage.id];
|
||||
if (!ddpMessage.fields) {
|
||||
this.activeUsers[ddpMessage.id] = {};
|
||||
this.activeUsers[ddpMessage.id] = {
|
||||
id: ddpMessage.id,
|
||||
removed: true
|
||||
};
|
||||
} else {
|
||||
this.activeUsers[ddpMessage.id] = { ...this.activeUsers[ddpMessage.id], ...activeUser, ...ddpMessage.fields };
|
||||
this.activeUsers[ddpMessage.id] = {
|
||||
id: ddpMessage.id, ...this.activeUsers[ddpMessage.id], ...ddpMessage.fields
|
||||
};
|
||||
}
|
||||
},
|
||||
async loginSuccess({ user }) {
|
||||
EventEmitter.emit('connected');
|
||||
reduxStore.dispatch(setUser(user));
|
||||
reduxStore.dispatch(roomsRequest());
|
||||
|
||||
|
@ -141,11 +164,18 @@ const RocketChat = {
|
|||
}
|
||||
this.roomsSub = await this.subscribeRooms();
|
||||
|
||||
this.sdk.subscribe('activeUsers');
|
||||
this.sdk.subscribe('roles');
|
||||
this.getPermissions();
|
||||
this.getCustomEmoji();
|
||||
this.getRoles();
|
||||
this.registerPushToken().catch(e => console.log(e));
|
||||
|
||||
if (this.activeUsersSubTimeout) {
|
||||
clearTimeout(this.activeUsersSubTimeout);
|
||||
this.activeUsersSubTimeout = false;
|
||||
}
|
||||
this.activeUsersSubTimeout = setTimeout(() => {
|
||||
this.sdk.subscribe('activeUsers');
|
||||
}, 5000);
|
||||
},
|
||||
connect({ server, user }) {
|
||||
database.setActiveDB(server);
|
||||
|
@ -190,53 +220,6 @@ const RocketChat = {
|
|||
});
|
||||
|
||||
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
|
||||
|
||||
this.sdk.onStreamData('stream-room-messages', (ddpMessage) => {
|
||||
// TODO: debounce
|
||||
const message = _buildMessage(ddpMessage.fields.args[0]);
|
||||
requestAnimationFrame(() => reduxStore.dispatch(roomMessageReceived(message)));
|
||||
});
|
||||
|
||||
this.sdk.onStreamData('stream-notify-room', protectedFunction((ddpMessage) => {
|
||||
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
|
||||
if (ev === 'typing') {
|
||||
reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
|
||||
} else if (ev === 'deleteMessage') {
|
||||
database.write(() => {
|
||||
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {
|
||||
const { _id } = ddpMessage.fields.args[0];
|
||||
const message = database.objects('messages').filtered('_id = $0', _id);
|
||||
database.delete(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.sdk.onStreamData('rocketchat_roles', protectedFunction((ddpMessage) => {
|
||||
this.roles = this.roles || {};
|
||||
|
||||
if (this.roleTimer) {
|
||||
clearTimeout(this.roleTimer);
|
||||
this.roleTimer = null;
|
||||
}
|
||||
this.roleTimer = setTimeout(() => {
|
||||
reduxStore.dispatch(setRoles(this.roles));
|
||||
|
||||
database.write(() => {
|
||||
foreach(this.roles, (description, _id) => {
|
||||
try {
|
||||
database.create('roles', { _id, description }, true);
|
||||
} catch (e) {
|
||||
log('create roles', e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.roleTimer = null;
|
||||
return this.roles = {};
|
||||
}, 1000);
|
||||
this.roles[ddpMessage.id] = (ddpMessage.fields && ddpMessage.fields.description) || undefined;
|
||||
}));
|
||||
},
|
||||
|
||||
register(credentials) {
|
||||
|
@ -309,7 +292,8 @@ const RocketChat = {
|
|||
language: result.me.language,
|
||||
status: result.me.status,
|
||||
customFields: result.me.customFields,
|
||||
emails: result.me.emails
|
||||
emails: result.me.emails,
|
||||
roles: result.me.roles
|
||||
};
|
||||
return user;
|
||||
} catch (e) {
|
||||
|
@ -326,6 +310,11 @@ const RocketChat = {
|
|||
this.roomsSub.stop();
|
||||
}
|
||||
|
||||
if (this.activeUsersSubTimeout) {
|
||||
clearTimeout(this.activeUsersSubTimeout);
|
||||
this.activeUsersSubTimeout = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.removePushToken();
|
||||
} catch (error) {
|
||||
|
@ -377,6 +366,7 @@ const RocketChat = {
|
|||
},
|
||||
loadMissedMessages,
|
||||
loadMessagesForRoom,
|
||||
loadThreadMessages,
|
||||
getMessage,
|
||||
sendMessage,
|
||||
getRooms,
|
||||
|
@ -421,8 +411,6 @@ const RocketChat = {
|
|||
data = data.filtered('t != $0', 'd');
|
||||
}
|
||||
data = data.slice(0, 7);
|
||||
const array = Array.from(data);
|
||||
data = JSON.parse(JSON.stringify(array));
|
||||
|
||||
const usernames = data.map(sub => sub.name);
|
||||
try {
|
||||
|
@ -473,6 +461,7 @@ const RocketChat = {
|
|||
getSettings,
|
||||
getPermissions,
|
||||
getCustomEmoji,
|
||||
getRoles,
|
||||
parseSettings: settings => settings.reduce((ret, item) => {
|
||||
ret[item._id] = item[defaultSettings[item._id].type];
|
||||
return ret;
|
||||
|
@ -551,6 +540,9 @@ const RocketChat = {
|
|||
unsubscribe(subscription) {
|
||||
return this.sdk.unsubscribe(subscription);
|
||||
},
|
||||
onStreamData(...args) {
|
||||
return this.sdk.onStreamData(...args);
|
||||
},
|
||||
emitTyping(room, t = true) {
|
||||
const { login } = reduxStore.getState();
|
||||
return this.sdk.methodCall('stream-notify-room', `${ room }/typing`, login.user.username, t);
|
||||
|
@ -572,9 +564,9 @@ const RocketChat = {
|
|||
// RC 0.64.0
|
||||
return this.sdk.post('rooms.favorite', { roomId, favorite });
|
||||
},
|
||||
getRoomMembers(rid, allUsers) {
|
||||
getRoomMembers(rid, allUsers, skip = 0, limit = 10) {
|
||||
// RC 0.42.0
|
||||
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers);
|
||||
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
|
||||
},
|
||||
getUserRoles() {
|
||||
// RC 0.27.0
|
||||
|
@ -584,16 +576,19 @@ const RocketChat = {
|
|||
// RC 0.65.0
|
||||
return this.sdk.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId });
|
||||
},
|
||||
async getRoomMember(rid, currentUserId) {
|
||||
try {
|
||||
if (rid === `${ currentUserId }${ currentUserId }`) {
|
||||
return Promise.resolve(currentUserId);
|
||||
}
|
||||
const membersResult = await RocketChat.getRoomMembers(rid, true);
|
||||
return Promise.resolve(membersResult.records.find(m => m._id !== currentUserId));
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
getChannelInfo(roomId) {
|
||||
// RC 0.48.0
|
||||
return this.sdk.get('channels.info', { roomId });
|
||||
},
|
||||
getUserInfo(userId) {
|
||||
// RC 0.48.0
|
||||
return this.sdk.get('users.info', { userId });
|
||||
},
|
||||
getRoomMemberId(rid, currentUserId) {
|
||||
if (rid === `${ currentUserId }${ currentUserId }`) {
|
||||
return currentUserId;
|
||||
}
|
||||
return rid.replace(currentUserId, '').trim();
|
||||
},
|
||||
toggleBlockUser(rid, blocked, block) {
|
||||
if (block) {
|
||||
|
@ -649,21 +644,29 @@ const RocketChat = {
|
|||
// RC 0.51.0
|
||||
return this.sdk.methodCall('addUsersToRoom', { rid, users });
|
||||
},
|
||||
getSingleMessage(msgId) {
|
||||
// RC 0.57.0
|
||||
return this.sdk.methodCall('getSingleMessage', msgId);
|
||||
},
|
||||
hasPermission(permissions, rid) {
|
||||
let roles = [];
|
||||
let roomRoles = [];
|
||||
try {
|
||||
// get the room from realm
|
||||
const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];
|
||||
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||
if (!room) {
|
||||
return permissions.reduce((result, permission) => {
|
||||
result[permission] = false;
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
// get room roles
|
||||
roles = room.roles; // eslint-disable-line prefer-destructuring
|
||||
roomRoles = room.roles;
|
||||
} catch (error) {
|
||||
console.log('hasPermission -> error', error);
|
||||
}
|
||||
// get permissions from realm
|
||||
const permissionsFiltered = database.objects('permissions')
|
||||
.filter(permission => permissions.includes(permission._id));
|
||||
// transform room roles to array
|
||||
const roomRoles = Array.from(Object.keys(roles), i => roles[i].value);
|
||||
// get user roles on the server from redux
|
||||
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || [];
|
||||
// merge both roles
|
||||
|
@ -675,7 +678,7 @@ const RocketChat = {
|
|||
result[permission] = false;
|
||||
const permissionFound = permissionsFiltered.find(p => p._id === permission);
|
||||
if (permissionFound) {
|
||||
result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r.value));
|
||||
result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r));
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
@ -762,6 +765,25 @@ const RocketChat = {
|
|||
roomId,
|
||||
searchText
|
||||
});
|
||||
},
|
||||
toggleFollowMessage(mid, follow) {
|
||||
// RC 1.0
|
||||
if (follow) {
|
||||
return this.sdk.methodCall('followMessage', { mid });
|
||||
}
|
||||
return this.sdk.methodCall('unfollowMessage', { mid });
|
||||
},
|
||||
getThreadsList({ rid, count, offset }) {
|
||||
// RC 1.0
|
||||
return this.sdk.get('chat.getThreadsList', {
|
||||
rid, count, offset, sort: { ts: -1 }
|
||||
});
|
||||
},
|
||||
getSyncThreadsList({ rid, updatedSince }) {
|
||||
// RC 1.0
|
||||
return this.sdk.get('chat.syncThreadsList', {
|
||||
rid, updatedSince
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,273 +0,0 @@
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { emojify } from 'react-emojione';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
|
||||
import Avatar from '../containers/Avatar';
|
||||
import Status from '../containers/Status';
|
||||
import RoomTypeIcon from '../containers/RoomTypeIcon';
|
||||
import I18n from '../i18n';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 15
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
height: '100%'
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
color: '#0C0D0F',
|
||||
fontWeight: '400',
|
||||
marginRight: 5,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
},
|
||||
alert: {
|
||||
fontWeight: '600'
|
||||
},
|
||||
row: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
marginTop: isIOS ? 5 : 2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
date: {
|
||||
fontSize: 14,
|
||||
color: '#9EA2A8',
|
||||
fontWeight: 'normal',
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
},
|
||||
updateAlert: {
|
||||
color: '#1D74F5',
|
||||
fontWeight: '700'
|
||||
},
|
||||
unreadNumberContainer: {
|
||||
minWidth: 23,
|
||||
padding: 3,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#1D74F5',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
unreadNumberText: {
|
||||
color: '#fff',
|
||||
overflow: 'hidden',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.56
|
||||
},
|
||||
status: {
|
||||
marginRight: 7,
|
||||
marginTop: 3
|
||||
},
|
||||
markdownText: {
|
||||
flex: 1,
|
||||
color: '#9EA2A8',
|
||||
fontSize: 15,
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
markdownTextAlert: {
|
||||
color: '#0C0D0F'
|
||||
},
|
||||
avatar: {
|
||||
marginRight: 10
|
||||
}
|
||||
});
|
||||
|
||||
const renderNumber = (unread, userMentions) => {
|
||||
if (!unread || unread <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (unread >= 1000) {
|
||||
unread = '999+';
|
||||
}
|
||||
|
||||
if (userMentions > 0) {
|
||||
unread = `@ ${ unread }`;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.unreadNumberContainer}>
|
||||
<Text style={styles.unreadNumberText}>{ unread }</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const attrs = ['name', 'unread', 'userMentions', 'StoreLastMessage', 'alert', 'type'];
|
||||
@connect(state => ({
|
||||
user: {
|
||||
id: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
token: state.login.user && state.login.user.token
|
||||
},
|
||||
StoreLastMessage: state.settings.Store_Last_Message,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
||||
}))
|
||||
export default class RoomItem extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
StoreLastMessage: PropTypes.bool,
|
||||
_updatedAt: PropTypes.string,
|
||||
lastMessage: PropTypes.object,
|
||||
favorite: PropTypes.bool,
|
||||
alert: PropTypes.bool,
|
||||
unread: PropTypes.number,
|
||||
userMentions: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
}),
|
||||
avatarSize: PropTypes.number,
|
||||
testID: PropTypes.string,
|
||||
height: PropTypes.number
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
avatarSize: 48
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { lastMessage, _updatedAt } = this.props;
|
||||
const oldlastMessage = lastMessage;
|
||||
const newLastmessage = nextProps.lastMessage;
|
||||
|
||||
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
|
||||
return true;
|
||||
}
|
||||
if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) {
|
||||
return true;
|
||||
}
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
return attrs.some(key => nextProps[key] !== this.props[key]);
|
||||
}
|
||||
|
||||
get avatar() {
|
||||
const {
|
||||
type, name, avatarSize, baseUrl, user
|
||||
} = this.props;
|
||||
return <Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={styles.avatar} user={user} />;
|
||||
}
|
||||
|
||||
get lastMessage() {
|
||||
const {
|
||||
lastMessage, type, StoreLastMessage, user
|
||||
} = this.props;
|
||||
|
||||
if (!StoreLastMessage) {
|
||||
return '';
|
||||
}
|
||||
if (!lastMessage) {
|
||||
return I18n.t('No_Message');
|
||||
}
|
||||
|
||||
let prefix = '';
|
||||
const me = lastMessage.u.username === user.username;
|
||||
|
||||
if (!lastMessage.msg && Object.keys(lastMessage.attachments).length > 0) {
|
||||
if (me) {
|
||||
return I18n.t('User_sent_an_attachment', { user: I18n.t('You') });
|
||||
} else {
|
||||
return I18n.t('User_sent_an_attachment', { user: lastMessage.u.username });
|
||||
}
|
||||
}
|
||||
|
||||
if (me) {
|
||||
prefix = I18n.t('You_colon');
|
||||
} else if (type !== 'd') {
|
||||
prefix = `${ lastMessage.u.username }: `;
|
||||
}
|
||||
|
||||
let msg = `${ prefix }${ lastMessage.msg.replace(/[\n\t\r]/igm, '') }`;
|
||||
msg = emojify(msg, { output: 'unicode' });
|
||||
return msg;
|
||||
}
|
||||
|
||||
get type() {
|
||||
const { type, id } = this.props;
|
||||
if (type === 'd') {
|
||||
return <Status style={styles.status} size={10} id={id} />;
|
||||
}
|
||||
return <RoomTypeIcon type={type} />;
|
||||
}
|
||||
|
||||
formatDate = date => moment(date).calendar(null, {
|
||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'h:mm A',
|
||||
lastWeek: 'dddd',
|
||||
sameElse: 'MMM D'
|
||||
})
|
||||
|
||||
render() {
|
||||
const {
|
||||
favorite, unread, userMentions, name, _updatedAt, alert, testID, height, onPress
|
||||
} = this.props;
|
||||
|
||||
const date = this.formatDate(_updatedAt);
|
||||
|
||||
let accessibilityLabel = name;
|
||||
if (unread === 1) {
|
||||
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
|
||||
} else if (unread > 1) {
|
||||
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
|
||||
}
|
||||
|
||||
if (userMentions > 0) {
|
||||
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
|
||||
}
|
||||
|
||||
if (date) {
|
||||
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
|
||||
}
|
||||
|
||||
return (
|
||||
<RectButton
|
||||
onPress={onPress}
|
||||
activeOpacity={0.8}
|
||||
underlayColor='#e1e5e8'
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
style={[styles.container, favorite && styles.favorite, height && { height }]}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
{this.avatar}
|
||||
<View style={styles.centerContainer}>
|
||||
<View style={styles.titleContainer}>
|
||||
{this.type}
|
||||
<Text style={[styles.title, alert && styles.alert]} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
|
||||
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
|
||||
{this.lastMessage}
|
||||
</Text>
|
||||
{renderNumber(unread, userMentions)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RectButton>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import { emojify } from 'react-emojione';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import styles from './styles';
|
||||
|
||||
const formatMsg = ({
|
||||
lastMessage, type, showLastMessage, username
|
||||
}) => {
|
||||
if (!showLastMessage) {
|
||||
return '';
|
||||
}
|
||||
if (!lastMessage) {
|
||||
return I18n.t('No_Message');
|
||||
}
|
||||
|
||||
let prefix = '';
|
||||
const isLastMessageSentByMe = lastMessage.u.username === username;
|
||||
|
||||
if (!lastMessage.msg && Object.keys(lastMessage.attachments).length) {
|
||||
const user = isLastMessageSentByMe ? I18n.t('You') : lastMessage.u.username;
|
||||
return I18n.t('User_sent_an_attachment', { user });
|
||||
}
|
||||
|
||||
if (isLastMessageSentByMe) {
|
||||
prefix = I18n.t('You_colon');
|
||||
} else if (type !== 'd') {
|
||||
prefix = `${ lastMessage.u.username }: `;
|
||||
}
|
||||
|
||||
let msg = `${ prefix }${ lastMessage.msg.replace(/[\n\t\r]/igm, '') }`;
|
||||
if (msg) {
|
||||
msg = emojify(msg, { output: 'unicode' });
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps);
|
||||
|
||||
const LastMessage = React.memo(({
|
||||
lastMessage, type, showLastMessage, username, alert
|
||||
}) => (
|
||||
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
|
||||
{formatMsg({
|
||||
lastMessage, type, showLastMessage, username
|
||||
})}
|
||||
</Text>
|
||||
), arePropsEqual);
|
||||
|
||||
LastMessage.propTypes = {
|
||||
lastMessage: PropTypes.object,
|
||||
type: PropTypes.string,
|
||||
showLastMessage: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
alert: PropTypes.bool
|
||||
};
|
||||
|
||||
export default LastMessage;
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Status from '../../containers/Status';
|
||||
import RoomTypeIcon from '../../containers/RoomTypeIcon';
|
||||
import styles from './styles';
|
||||
|
||||
const TypeIcon = React.memo(({ type, id, prid }) => {
|
||||
if (type === 'd') {
|
||||
return <Status style={styles.status} size={10} id={id} />;
|
||||
}
|
||||
return <RoomTypeIcon type={prid ? 'discussion' : type} />;
|
||||
});
|
||||
|
||||
TypeIcon.propTypes = {
|
||||
type: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
prid: PropTypes.string
|
||||
};
|
||||
|
||||
export default TypeIcon;
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, Text } from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const UnreadBadge = React.memo(({ unread, userMentions, type }) => {
|
||||
if (!unread || unread <= 0) {
|
||||
return;
|
||||
}
|
||||
if (unread >= 1000) {
|
||||
unread = '999+';
|
||||
}
|
||||
const mentioned = userMentions > 0 && type !== 'd';
|
||||
|
||||
return (
|
||||
<View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentionedContainer]}>
|
||||
<Text style={[styles.unreadText, mentioned && styles.unreadMentionedText]}>{ unread }</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
UnreadBadge.propTypes = {
|
||||
unread: PropTypes.number,
|
||||
userMentions: PropTypes.number,
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
export default UnreadBadge;
|
|
@ -0,0 +1,120 @@
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, Text } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import I18n from '../../i18n';
|
||||
import styles, { ROW_HEIGHT } from './styles';
|
||||
import UnreadBadge from './UnreadBadge';
|
||||
import TypeIcon from './TypeIcon';
|
||||
import LastMessage from './LastMessage';
|
||||
|
||||
export { ROW_HEIGHT };
|
||||
|
||||
const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
|
||||
@connect(state => ({
|
||||
userId: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
token: state.login.user && state.login.user.token
|
||||
}))
|
||||
export default class RoomItem extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
showLastMessage: PropTypes.bool,
|
||||
_updatedAt: PropTypes.string,
|
||||
lastMessage: PropTypes.object,
|
||||
alert: PropTypes.bool,
|
||||
unread: PropTypes.number,
|
||||
userMentions: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
prid: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
userId: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
token: PropTypes.string,
|
||||
avatarSize: PropTypes.number,
|
||||
testID: PropTypes.string,
|
||||
height: PropTypes.number
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
avatarSize: 48
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { lastMessage, _updatedAt } = this.props;
|
||||
const oldlastMessage = lastMessage;
|
||||
const newLastmessage = nextProps.lastMessage;
|
||||
|
||||
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
|
||||
return true;
|
||||
}
|
||||
if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) {
|
||||
return true;
|
||||
}
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
return attrs.some(key => nextProps[key] !== this.props[key]);
|
||||
}
|
||||
|
||||
formatDate = date => moment(date).calendar(null, {
|
||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'h:mm A',
|
||||
lastWeek: 'dddd',
|
||||
sameElse: 'MMM D'
|
||||
})
|
||||
|
||||
render() {
|
||||
const {
|
||||
unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, onPress, id, prid, showLastMessage, lastMessage
|
||||
} = this.props;
|
||||
|
||||
const date = this.formatDate(_updatedAt);
|
||||
|
||||
let accessibilityLabel = name;
|
||||
if (unread === 1) {
|
||||
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
|
||||
} else if (unread > 1) {
|
||||
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
|
||||
}
|
||||
|
||||
if (userMentions > 0) {
|
||||
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
|
||||
}
|
||||
|
||||
if (date) {
|
||||
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
|
||||
}
|
||||
|
||||
return (
|
||||
<RectButton
|
||||
onPress={onPress}
|
||||
activeOpacity={0.8}
|
||||
underlayColor='#e1e5e8'
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
style={[styles.container, height && { height }]}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
<Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
|
||||
<View style={styles.centerContainer}>
|
||||
<View style={styles.titleContainer}>
|
||||
<TypeIcon type={type} id={id} prid={prid} />
|
||||
<Text style={[styles.title, alert && styles.alert]} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
|
||||
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<LastMessage lastMessage={lastMessage} type={type} showLastMessage={showLastMessage} username={username} alert={alert} />
|
||||
<UnreadBadge unread={unread} userMentions={userMentions} type={type} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RectButton>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import { StyleSheet, PixelRatio } from 'react-native';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_UNREAD, COLOR_TEXT
|
||||
} from '../../constants/colors';
|
||||
|
||||
export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 14,
|
||||
height: ROW_HEIGHT
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingRight: 14,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: COLOR_SEPARATOR
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
lineHeight: 20,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
alert: {
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
row: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
date: {
|
||||
fontSize: 13,
|
||||
marginLeft: 4,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
updateAlert: {
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
unreadNumberContainer: {
|
||||
minWidth: 21,
|
||||
height: 21,
|
||||
paddingVertical: 3,
|
||||
paddingHorizontal: 5,
|
||||
borderRadius: 10.5,
|
||||
backgroundColor: COLOR_UNREAD,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 10
|
||||
},
|
||||
unreadMentionedContainer: {
|
||||
backgroundColor: COLOR_PRIMARY
|
||||
},
|
||||
unreadText: {
|
||||
color: COLOR_TEXT,
|
||||
overflow: 'hidden',
|
||||
fontSize: 13,
|
||||
...sharedStyles.textMedium,
|
||||
letterSpacing: 0.56,
|
||||
textAlign: 'center'
|
||||
},
|
||||
unreadMentionedText: {
|
||||
color: COLOR_WHITE
|
||||
},
|
||||
status: {
|
||||
marginRight: 7,
|
||||
marginTop: 3
|
||||
},
|
||||
markdownText: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
lineHeight: 17,
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorDescription
|
||||
},
|
||||
markdownTextAlert: {
|
||||
...sharedStyles.textColorNormal
|
||||
},
|
||||
avatar: {
|
||||
marginRight: 10
|
||||
}
|
||||
});
|
|
@ -6,13 +6,14 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import Avatar from '../containers/Avatar';
|
||||
import Touch from '../utils/touch';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
height: 54,
|
||||
backgroundColor: '#fff'
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row'
|
||||
|
@ -23,23 +24,23 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
name: {
|
||||
fontSize: 18,
|
||||
color: '#0C0D0F',
|
||||
marginTop: isIOS ? 6 : 3,
|
||||
marginBottom: 1,
|
||||
textAlign: 'left'
|
||||
fontSize: 17,
|
||||
...sharedStyles.textMedium,
|
||||
...sharedStyles.textColorNormal
|
||||
},
|
||||
username: {
|
||||
fontSize: 14,
|
||||
color: '#9EA2A8'
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorDescription
|
||||
},
|
||||
icon: {
|
||||
marginHorizontal: 15,
|
||||
alignSelf: 'center',
|
||||
color: '#1D74F5'
|
||||
color: COLOR_PRIMARY
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -48,7 +49,7 @@ const UserItem = ({
|
|||
}) => (
|
||||
<Touch onPress={onPress} onLongPress={onLongPress} style={styles.button} testID={testID}>
|
||||
<View style={[styles.container, style]}>
|
||||
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} user={user} />
|
||||
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} />
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={styles.name}>{name}</Text>
|
||||
<Text style={styles.username}>@{username}</Text>
|
||||
|
|
|
@ -4,7 +4,7 @@ import PushNotification from './push';
|
|||
import store from '../lib/createStore';
|
||||
import { deepLinkingOpen } from '../actions/deepLinking';
|
||||
|
||||
const onNotification = (notification) => {
|
||||
export const onNotification = (notification) => {
|
||||
if (notification) {
|
||||
const data = notification.getData();
|
||||
if (data) {
|
||||
|
@ -31,13 +31,11 @@ const onNotification = (notification) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getDeviceToken = () => PushNotification.getDeviceToken();
|
||||
const setBadgeCount = count => PushNotification.setBadgeCount(count);
|
||||
const initializePushNotifications = () => {
|
||||
PushNotification.configure({
|
||||
export const getDeviceToken = () => PushNotification.getDeviceToken();
|
||||
export const setBadgeCount = count => PushNotification.setBadgeCount(count);
|
||||
export const initializePushNotifications = () => {
|
||||
setBadgeCount();
|
||||
return PushNotification.configure({
|
||||
onNotification
|
||||
});
|
||||
setBadgeCount();
|
||||
};
|
||||
|
||||
export { initializePushNotifications, getDeviceToken, setBadgeCount };
|
||||
|
|
|
@ -25,12 +25,7 @@ class PushNotification {
|
|||
this.onRegister = params.onRegister;
|
||||
this.onNotification = params.onNotification;
|
||||
NotificationsAndroid.refreshToken();
|
||||
|
||||
PendingNotifications.getInitialNotification()
|
||||
.then((notification) => {
|
||||
this.onNotification(notification);
|
||||
})
|
||||
.catch(e => console.warn(e));
|
||||
return PendingNotifications.getInitialNotification();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ class PushNotification {
|
|||
this.onNotification = params.onNotification;
|
||||
|
||||
NotificationsIOS.consumeBackgroundQueue();
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
export default new PushNotification();
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import * as types from '../actions/actionsTypes';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case types.ACTIVE_USERS.SET:
|
||||
return {
|
||||
...state,
|
||||
...action.data
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -3,15 +3,12 @@ import settings from './reducers';
|
|||
import login from './login';
|
||||
import meteor from './connect';
|
||||
import messages from './messages';
|
||||
import room from './room';
|
||||
import rooms from './rooms';
|
||||
import server from './server';
|
||||
import selectedUsers from './selectedUsers';
|
||||
import createChannel from './createChannel';
|
||||
import app from './app';
|
||||
import customEmojis from './customEmojis';
|
||||
import activeUsers from './activeUsers';
|
||||
import roles from './roles';
|
||||
import sortPreferences from './sortPreferences';
|
||||
|
||||
export default combineReducers({
|
||||
|
@ -23,10 +20,7 @@ export default combineReducers({
|
|||
selectedUsers,
|
||||
createChannel,
|
||||
app,
|
||||
room,
|
||||
rooms,
|
||||
customEmojis,
|
||||
activeUsers,
|
||||
roles,
|
||||
sortPreferences
|
||||
});
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import * as types from '../actions/actionsTypes';
|
||||
|
||||
const initialState = {
|
||||
isFetching: false,
|
||||
failure: false,
|
||||
message: {},
|
||||
actionMessage: {},
|
||||
replyMessage: {},
|
||||
replying: false,
|
||||
editing: false,
|
||||
showActions: false,
|
||||
showErrorActions: false,
|
||||
|
@ -14,23 +13,6 @@ const initialState = {
|
|||
|
||||
export default function messages(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case types.MESSAGES.REQUEST:
|
||||
return {
|
||||
...state,
|
||||
isFetching: true
|
||||
};
|
||||
case types.MESSAGES.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
isFetching: false
|
||||
};
|
||||
case types.LOGIN.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
failure: true,
|
||||
errorMessage: action.err
|
||||
};
|
||||
case types.MESSAGES.ACTIONS_SHOW:
|
||||
return {
|
||||
...state,
|
||||
|
@ -83,12 +65,14 @@ export default function messages(state = initialState, action) {
|
|||
replyMessage: {
|
||||
...action.message,
|
||||
mention: action.mention
|
||||
}
|
||||
},
|
||||
replying: true
|
||||
};
|
||||
case types.MESSAGES.REPLY_CANCEL:
|
||||
return {
|
||||
...state,
|
||||
replyMessage: {}
|
||||
replyMessage: {},
|
||||
replying: false
|
||||
};
|
||||
case types.MESSAGES.SET_INPUT:
|
||||
return {
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import * as types from '../actions/actionsTypes';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case types.ROLES.SET:
|
||||
return {
|
||||
...state,
|
||||
...action.data
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
import * as types from '../actions/actionsTypes';
|
||||
|
||||
const initialState = {
|
||||
usersTyping: []
|
||||
};
|
||||
|
||||
export default function room(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case types.ROOM.OPEN:
|
||||
return {
|
||||
...initialState,
|
||||
...action.room,
|
||||
lastOpen: new Date()
|
||||
};
|
||||
case types.ROOM.CLOSE:
|
||||
return {
|
||||
...initialState
|
||||
};
|
||||
case types.ROOM.SET_LAST_OPEN:
|
||||
return {
|
||||
...state,
|
||||
lastOpen: action.date
|
||||
};
|
||||
case types.ROOM.ADD_USER_TYPING:
|
||||
return {
|
||||
...state,
|
||||
usersTyping: [...state.usersTyping.filter(user => user !== action.username), action.username]
|
||||
};
|
||||
case types.ROOM.REMOVE_USER_TYPING:
|
||||
return {
|
||||
...state,
|
||||
usersTyping: [...state.usersTyping.filter(user => user !== action.username)]
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ const initialState = {
|
|||
connected: false,
|
||||
failure: false,
|
||||
server: '',
|
||||
version: null,
|
||||
loading: true,
|
||||
adding: false
|
||||
};
|
||||
|
@ -29,6 +30,7 @@ export default function server(state = initialState, action) {
|
|||
return {
|
||||
...state,
|
||||
server: action.server,
|
||||
version: action.version,
|
||||
connecting: true,
|
||||
connected: false,
|
||||
loading: true
|
||||
|
@ -37,6 +39,7 @@ export default function server(state = initialState, action) {
|
|||
return {
|
||||
...state,
|
||||
server: action.server,
|
||||
version: action.version,
|
||||
connecting: false,
|
||||
connected: true,
|
||||
loading: false
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AsyncStorage } from 'react-native';
|
||||
import { delay } from 'redux-saga';
|
||||
import {
|
||||
takeLatest, take, select, put, all, race
|
||||
takeLatest, take, select, put, all
|
||||
} from 'redux-saga/effects';
|
||||
|
||||
import Navigation from '../lib/Navigation';
|
||||
|
@ -10,28 +10,25 @@ import { selectServerRequest } from '../actions/server';
|
|||
import database from '../lib/realm';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import EventEmitter from '../utils/events';
|
||||
import { appStart } from '../actions';
|
||||
|
||||
const roomTypes = {
|
||||
channel: 'c', direct: 'd', group: 'p'
|
||||
};
|
||||
|
||||
const navigate = function* navigate({ params }) {
|
||||
yield put(appStart('inside'));
|
||||
if (params.rid) {
|
||||
const canOpenRoom = yield RocketChat.canOpenRoom(params);
|
||||
if (canOpenRoom) {
|
||||
const [type, name] = params.path.split('/');
|
||||
yield Navigation.navigate('RoomsListView');
|
||||
Navigation.navigate('RoomView', { rid: params.rid, name, t: roomTypes[type] });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = function* handleOpen({ params }) {
|
||||
const isReady = yield select(state => state.app.ready);
|
||||
|
||||
if (!isReady) {
|
||||
yield take(types.APP.READY);
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
return;
|
||||
}
|
||||
|
@ -54,29 +51,28 @@ const handleOpen = function* handleOpen({ params }) {
|
|||
// if deep link is from same server
|
||||
if (server === host) {
|
||||
if (user) {
|
||||
yield race({
|
||||
typing: take(types.SERVER.SELECT_SUCCESS),
|
||||
timeout: delay(3000)
|
||||
});
|
||||
const connected = yield select(state => state.server.connected);
|
||||
if (!connected) {
|
||||
yield put(selectServerRequest(host));
|
||||
yield take(types.SERVER.SELECT_SUCCESS);
|
||||
}
|
||||
yield navigate({ params });
|
||||
} else {
|
||||
yield put(appStart('outside'));
|
||||
}
|
||||
} else {
|
||||
// if deep link is from a different server
|
||||
const result = yield RocketChat.testServer(server);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// search if deep link's server already exists
|
||||
const servers = yield database.databases.serversDB.objects('servers').filtered('id = $0', host); // TODO: need better test
|
||||
if (servers.length && user) {
|
||||
yield put(selectServerRequest(host));
|
||||
yield race({
|
||||
typing: take(types.SERVER.SELECT_SUCCESS),
|
||||
timeout: delay(3000)
|
||||
});
|
||||
yield take(types.SERVER.SELECT_SUCCESS);
|
||||
yield navigate({ params });
|
||||
} else {
|
||||
// if deep link is from a different server
|
||||
const result = yield RocketChat.getServerInfo(server);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
Navigation.navigate('OnboardingView', { previousServer: server });
|
||||
yield delay(1000);
|
||||
EventEmitter.emit('NewServer', { server: host });
|
||||
|
|
|
@ -9,6 +9,7 @@ import { APP } from '../actions/actionsTypes';
|
|||
import RocketChat from '../lib/rocketchat';
|
||||
import log from '../utils/log';
|
||||
import Navigation from '../lib/Navigation';
|
||||
import database from '../lib/realm';
|
||||
|
||||
const restore = function* restore() {
|
||||
try {
|
||||
|
@ -27,7 +28,8 @@ const restore = function* restore() {
|
|||
]);
|
||||
yield put(actions.appStart('outside'));
|
||||
} else if (server) {
|
||||
yield put(selectServerRequest(server));
|
||||
const serverObj = database.databases.serversDB.objectForPrimaryKey('servers', server);
|
||||
yield put(selectServerRequest(server, serverObj && serverObj.version));
|
||||
}
|
||||
|
||||
yield put(actions.appReady({}));
|
||||
|
|
|
@ -4,8 +4,6 @@ import { takeLatest, put, call } from 'redux-saga/effects';
|
|||
import Navigation from '../lib/Navigation';
|
||||
import { MESSAGES } from '../actions/actionsTypes';
|
||||
import {
|
||||
messagesSuccess,
|
||||
messagesFailure,
|
||||
deleteSuccess,
|
||||
deleteFailure,
|
||||
editSuccess,
|
||||
|
@ -25,19 +23,6 @@ const editMessage = message => RocketChat.editMessage(message);
|
|||
const toggleStarMessage = message => RocketChat.toggleStarMessage(message);
|
||||
const togglePinMessage = message => RocketChat.togglePinMessage(message);
|
||||
|
||||
const get = function* get({ room }) {
|
||||
try {
|
||||
if (room.lastOpen) {
|
||||
yield RocketChat.loadMissedMessages(room);
|
||||
} else {
|
||||
yield RocketChat.loadMessagesForRoom(room);
|
||||
}
|
||||
yield put(messagesSuccess());
|
||||
} catch (err) {
|
||||
yield put(messagesFailure(err));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRequest = function* handleDeleteRequest({ message }) {
|
||||
try {
|
||||
yield call(deleteMessage, message);
|
||||
|
@ -97,7 +82,6 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
|
|||
};
|
||||
|
||||
const root = function* root() {
|
||||
yield takeLatest(MESSAGES.REQUEST, get);
|
||||
yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest);
|
||||
yield takeLatest(MESSAGES.EDIT_REQUEST, handleEditRequest);
|
||||
yield takeLatest(MESSAGES.TOGGLE_STAR_REQUEST, handleToggleStarRequest);
|
||||
|
|
|
@ -1,122 +1,30 @@
|
|||
import { Alert } from 'react-native';
|
||||
import {
|
||||
put, call, takeLatest, take, select, race, fork, cancel, takeEvery
|
||||
call, takeLatest, take, select
|
||||
} from 'redux-saga/effects';
|
||||
import { delay } from 'redux-saga';
|
||||
import EJSON from 'ejson';
|
||||
|
||||
import Navigation from '../lib/Navigation';
|
||||
import * as types from '../actions/actionsTypes';
|
||||
import { addUserTyping, removeUserTyping } from '../actions/room';
|
||||
import { messagesRequest, editCancel, replyCancel } from '../actions/messages';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import database from '../lib/realm';
|
||||
import log from '../utils/log';
|
||||
import I18n from '../i18n';
|
||||
|
||||
|
||||
let sub;
|
||||
let thread;
|
||||
|
||||
const cancelTyping = function* cancelTyping(username) {
|
||||
while (true) {
|
||||
const { typing, timeout } = yield race({
|
||||
typing: take(types.ROOM.SOMEONE_TYPING),
|
||||
timeout: call(delay, 5000)
|
||||
});
|
||||
if (timeout || (typing.username === username && !typing.typing)) {
|
||||
return yield put(removeUserTyping(username));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const usersTyping = function* usersTyping({ rid }) {
|
||||
while (true) {
|
||||
const { _rid, username, typing } = yield take(types.ROOM.SOMEONE_TYPING);
|
||||
if (_rid === rid) {
|
||||
yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username)));
|
||||
if (typing) {
|
||||
yield fork(cancelTyping, username);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleMessageReceived = function* handleMessageReceived({ message }) {
|
||||
try {
|
||||
const room = yield select(state => state.room);
|
||||
|
||||
if (message.rid === room.rid) {
|
||||
database.write(() => {
|
||||
database.create('messages', EJSON.fromJSONValue(message), true);
|
||||
});
|
||||
|
||||
if (room._id) {
|
||||
RocketChat.readMessages(room.rid);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('handleMessageReceived', e);
|
||||
}
|
||||
};
|
||||
|
||||
let opened = false;
|
||||
|
||||
const watchRoomOpen = function* watchRoomOpen({ room }) {
|
||||
try {
|
||||
if (opened) {
|
||||
return;
|
||||
}
|
||||
opened = true;
|
||||
|
||||
const auth = yield select(state => state.login.isAuthenticated);
|
||||
if (!auth) {
|
||||
yield take(types.LOGIN.SUCCESS);
|
||||
}
|
||||
|
||||
yield put(messagesRequest({ ...room }));
|
||||
|
||||
if (room._id) {
|
||||
RocketChat.readMessages(room.rid);
|
||||
}
|
||||
|
||||
sub = yield RocketChat.subscribeRoom(room);
|
||||
|
||||
thread = yield fork(usersTyping, { rid: room.rid });
|
||||
yield race({
|
||||
open: take(types.ROOM.OPEN),
|
||||
close: take(types.ROOM.CLOSE)
|
||||
});
|
||||
opened = false;
|
||||
cancel(thread);
|
||||
sub.stop();
|
||||
yield put(editCancel());
|
||||
yield put(replyCancel());
|
||||
} catch (e) {
|
||||
log('watchRoomOpen', e);
|
||||
}
|
||||
};
|
||||
|
||||
const watchuserTyping = function* watchuserTyping({ status }) {
|
||||
const watchUserTyping = function* watchUserTyping({ rid, status }) {
|
||||
const auth = yield select(state => state.login.isAuthenticated);
|
||||
if (!auth) {
|
||||
yield take(types.LOGIN.SUCCESS);
|
||||
}
|
||||
|
||||
const room = yield select(state => state.room);
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield RocketChat.emitTyping(room.rid, status);
|
||||
yield RocketChat.emitTyping(rid, status);
|
||||
|
||||
if (status) {
|
||||
yield call(delay, 5000);
|
||||
yield RocketChat.emitTyping(room.rid, false);
|
||||
yield RocketChat.emitTyping(rid, false);
|
||||
}
|
||||
} catch (e) {
|
||||
log('watchuserTyping', e);
|
||||
log('watchUserTyping', e);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -147,9 +55,7 @@ const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
|
|||
};
|
||||
|
||||
const root = function* root() {
|
||||
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
|
||||
yield takeEvery(types.ROOM.OPEN, watchRoomOpen);
|
||||
yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
|
||||
yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping);
|
||||
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
|
||||
yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
|
||||
};
|
||||
|
|
|
@ -5,15 +5,37 @@ import Navigation from '../lib/Navigation';
|
|||
import { SERVER } from '../actions/actionsTypes';
|
||||
import * as actions from '../actions';
|
||||
import { serverFailure, selectServerRequest, selectServerSuccess } from '../actions/server';
|
||||
import { setRoles } from '../actions/roles';
|
||||
import { setUser } from '../actions/login';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import database from '../lib/realm';
|
||||
import log from '../utils/log';
|
||||
import I18n from '../i18n';
|
||||
|
||||
const handleSelectServer = function* handleSelectServer({ server }) {
|
||||
const getServerInfo = function* getServerInfo({ server }) {
|
||||
try {
|
||||
const serverInfo = yield RocketChat.getServerInfo(server);
|
||||
if (!serverInfo.success) {
|
||||
Alert.alert(I18n.t('Oops'), I18n.t(serverInfo.message, serverInfo.messageOptions));
|
||||
yield put(serverFailure());
|
||||
return;
|
||||
}
|
||||
|
||||
database.databases.serversDB.write(() => {
|
||||
database.databases.serversDB.create('servers', { id: server, version: serverInfo.version }, true);
|
||||
});
|
||||
|
||||
return serverInfo;
|
||||
} catch (e) {
|
||||
log('getServerInfo', e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
|
||||
try {
|
||||
let serverInfo;
|
||||
if (fetchVersion) {
|
||||
serverInfo = yield getServerInfo({ server });
|
||||
}
|
||||
yield AsyncStorage.setItem('currentServer', server);
|
||||
const userStringified = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
|
||||
|
||||
|
@ -24,19 +46,15 @@ const handleSelectServer = function* handleSelectServer({ server }) {
|
|||
yield put(actions.appStart('inside'));
|
||||
} else {
|
||||
RocketChat.connect({ server });
|
||||
yield put(actions.appStart('outside'));
|
||||
}
|
||||
|
||||
const settings = database.objects('settings');
|
||||
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
|
||||
const emojis = database.objects('customEmojis');
|
||||
yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length))));
|
||||
const roles = database.objects('roles');
|
||||
yield put(setRoles(roles.reduce((result, role) => {
|
||||
result[role._id] = role.description;
|
||||
return result;
|
||||
}, {})));
|
||||
|
||||
yield put(selectServerSuccess(server));
|
||||
yield put(selectServerSuccess(server, fetchVersion ? serverInfo && serverInfo.version : version));
|
||||
} catch (e) {
|
||||
log('handleSelectServer', e);
|
||||
}
|
||||
|
@ -44,13 +62,9 @@ const handleSelectServer = function* handleSelectServer({ server }) {
|
|||
|
||||
const handleServerRequest = function* handleServerRequest({ server }) {
|
||||
try {
|
||||
const result = yield RocketChat.testServer(server);
|
||||
if (!result.success) {
|
||||
Alert.alert(I18n.t('Oops'), I18n.t(result.message, result.messageOptions));
|
||||
yield put(serverFailure());
|
||||
return;
|
||||
}
|
||||
const serverInfo = yield getServerInfo({ server });
|
||||
|
||||
// TODO: cai aqui O.o
|
||||
const loginServicesLength = yield RocketChat.getLoginServices(server);
|
||||
if (loginServicesLength === 0) {
|
||||
Navigation.navigate('LoginView');
|
||||
|
@ -58,10 +72,7 @@ const handleServerRequest = function* handleServerRequest({ server }) {
|
|||
Navigation.navigate('LoginSignupView');
|
||||
}
|
||||
|
||||
database.databases.serversDB.write(() => {
|
||||
database.databases.serversDB.create('servers', { id: server }, true);
|
||||
});
|
||||
yield put(selectServerRequest(server));
|
||||
yield put(selectServerRequest(server, serverInfo.version, false));
|
||||
} catch (e) {
|
||||
yield put(serverFailure());
|
||||
log('handleServerRequest', e);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { TouchableHighlight } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const Touch = ({ children, onPress, ...props }) => (
|
||||
<TouchableHighlight
|
||||
underlayColor='#FFFFFF'
|
||||
underlayColor={COLOR_WHITE}
|
||||
activeOpacity={0.5}
|
||||
onPress={onPress}
|
||||
{...props}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, Image } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
import { isAndroid } from '../utils/deviceInfo';
|
||||
import { appInit as appInitAction } from '../actions';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
|
@ -14,25 +11,9 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
@connect(null, dispatch => ({
|
||||
appInit: () => dispatch(appInitAction())
|
||||
}))
|
||||
export default class Loading extends React.PureComponent {
|
||||
static propTypes = {
|
||||
appInit: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.appInit();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<StatusBar />
|
||||
{isAndroid ? <Image source={{ uri: 'launch_screen' }} style={styles.image} /> : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default React.memo(() => (
|
||||
<React.Fragment>
|
||||
<StatusBar />
|
||||
{isAndroid ? <Image source={{ uri: 'launch_screen' }} style={styles.image} /> : null}
|
||||
</React.Fragment>
|
||||
));
|
||||
|
|
|
@ -20,6 +20,7 @@ import { showErrorAlert } from '../utils/info';
|
|||
import { isAndroid } from '../utils/deviceInfo';
|
||||
import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
import { COLOR_TEXT_DESCRIPTION, COLOR_WHITE } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -28,7 +29,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
list: {
|
||||
width: '100%',
|
||||
backgroundColor: '#FFFFFF'
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
separator: {
|
||||
marginLeft: 60
|
||||
|
@ -39,22 +40,23 @@ const styles = StyleSheet.create({
|
|||
input: {
|
||||
height: 54,
|
||||
paddingHorizontal: 18,
|
||||
color: '#9EA2A8',
|
||||
backgroundColor: '#fff',
|
||||
fontSize: 18
|
||||
fontSize: 17,
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorNormal,
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
swithContainer: {
|
||||
height: 54,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: COLOR_WHITE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 18
|
||||
},
|
||||
label: {
|
||||
color: '#0C0D0F',
|
||||
fontSize: 18,
|
||||
fontWeight: '500'
|
||||
fontSize: 17,
|
||||
...sharedStyles.textMedium,
|
||||
...sharedStyles.textColorNormal
|
||||
},
|
||||
invitedHeader: {
|
||||
marginTop: 18,
|
||||
|
@ -64,14 +66,15 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center'
|
||||
},
|
||||
invitedTitle: {
|
||||
color: '#2F343D',
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
...sharedStyles.textSemibold,
|
||||
...sharedStyles.textColorNormal,
|
||||
lineHeight: 41
|
||||
},
|
||||
invitedCount: {
|
||||
color: '#9EA2A8',
|
||||
fontSize: 15
|
||||
fontSize: 14,
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorDescription
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -347,6 +350,7 @@ export default class CreateChannelView extends LoggedView {
|
|||
value={channelName}
|
||||
onChangeText={this.onChangeText}
|
||||
placeholder={I18n.t('Channel_Name')}
|
||||
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
|
||||
returnKeyType='done'
|
||||
testID='create-channel-name'
|
||||
autoCorrect={false}
|
||||
|
|
|
@ -5,14 +5,16 @@ import {
|
|||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import sharedStyles from './Styles';
|
||||
import scrollPersistTaps from '../utils/scrollPersistTaps';
|
||||
import LoggedView from './View';
|
||||
import I18n from '../i18n';
|
||||
import DisclosureIndicator from '../containers/DisclosureIndicator';
|
||||
import { CloseModalButton } from '../containers/HeaderButton';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
import { COLOR_SEPARATOR, COLOR_WHITE } from '../constants/colors';
|
||||
import openLink from '../utils/openLink';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -21,13 +23,13 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
scroll: {
|
||||
marginTop: 35,
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#cbced1',
|
||||
backgroundColor: COLOR_WHITE,
|
||||
borderColor: COLOR_SEPARATOR,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: '#cbced1',
|
||||
backgroundColor: COLOR_SEPARATOR,
|
||||
height: StyleSheet.hairlineWidth,
|
||||
width: '100%',
|
||||
marginLeft: 20
|
||||
|
@ -35,7 +37,7 @@ const styles = StyleSheet.create({
|
|||
item: {
|
||||
width: '100%',
|
||||
height: 48,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: COLOR_WHITE,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 10,
|
||||
flexDirection: 'row',
|
||||
|
@ -44,22 +46,24 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
text: {
|
||||
...sharedStyles.textMedium,
|
||||
color: '#0c0d0f',
|
||||
...sharedStyles.textColorNormal,
|
||||
fontSize: 18
|
||||
}
|
||||
});
|
||||
|
||||
const Separator = () => <View style={styles.separator} />;
|
||||
|
||||
@connect(state => ({
|
||||
server: state.server.server
|
||||
}))
|
||||
/** @extends React.Component */
|
||||
export default class LegalView extends LoggedView {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
headerLeft: <CloseModalButton testID='legal-view-close' navigation={navigation} />,
|
||||
static navigationOptions = () => ({
|
||||
title: I18n.t('Legal')
|
||||
})
|
||||
|
||||
static propTypes = {
|
||||
navigation: PropTypes.object
|
||||
server: PropTypes.string
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -67,8 +71,11 @@ export default class LegalView extends LoggedView {
|
|||
}
|
||||
|
||||
onPressItem = ({ route }) => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate(route);
|
||||
const { server } = this.props;
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
openLink(`${ server }/${ route }`);
|
||||
}
|
||||
|
||||
renderItem = ({ text, route, testID }) => (
|
||||
|
@ -83,9 +90,9 @@ export default class LegalView extends LoggedView {
|
|||
<SafeAreaView style={styles.container} testID='legal-view' forceInset={{ bottom: 'never' }}>
|
||||
<StatusBar />
|
||||
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.scroll}>
|
||||
{this.renderItem({ text: 'Terms_of_Service', route: 'TermsServiceView', testID: 'legal-terms-button' })}
|
||||
{this.renderItem({ text: 'Terms_of_Service', route: 'terms-of-service', testID: 'legal-terms-button' })}
|
||||
<Separator />
|
||||
{this.renderItem({ text: 'Privacy_Policy', route: 'PrivacyPolicyView', testID: 'legal-privacy-button' })}
|
||||
{this.renderItem({ text: 'Privacy_Policy', route: 'privacy-policy', testID: 'legal-privacy-button' })}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
@ -17,6 +17,7 @@ import Button from '../containers/Button';
|
|||
import I18n from '../i18n';
|
||||
import { LegalButton } from '../containers/HeaderButton';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
import { COLOR_SEPARATOR, COLOR_BORDER } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -32,7 +33,7 @@ const styles = StyleSheet.create({
|
|||
serviceButtonContainer: {
|
||||
borderRadius: 2,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e1e5e8',
|
||||
borderColor: COLOR_BORDER,
|
||||
width: '100%',
|
||||
height: 48,
|
||||
flexDirection: 'row',
|
||||
|
@ -49,8 +50,8 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
serviceText: {
|
||||
...sharedStyles.textRegular,
|
||||
fontSize: 16,
|
||||
color: '#2f343d'
|
||||
...sharedStyles.textColorNormal,
|
||||
fontSize: 16
|
||||
},
|
||||
serviceName: {
|
||||
...sharedStyles.textBold
|
||||
|
@ -72,7 +73,7 @@ const styles = StyleSheet.create({
|
|||
separatorLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: '#e1e5e8'
|
||||
backgroundColor: COLOR_SEPARATOR
|
||||
},
|
||||
separatorLineLeft: {
|
||||
marginRight: 15
|
||||
|
|
|
@ -18,6 +18,7 @@ import I18n from '../i18n';
|
|||
import { loginRequest as loginRequestAction } from '../actions/login';
|
||||
import { LegalButton } from '../containers/HeaderButton';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
import { COLOR_PRIMARY } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonsContainer: {
|
||||
|
@ -31,12 +32,12 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
dontHaveAccount: {
|
||||
...sharedStyles.textRegular,
|
||||
color: '#9ea2a8',
|
||||
...sharedStyles.textColorDescription,
|
||||
fontSize: 13
|
||||
},
|
||||
createAccount: {
|
||||
...sharedStyles.textSemibold,
|
||||
color: '#1d74f5',
|
||||
color: COLOR_PRIMARY,
|
||||
fontSize: 13
|
||||
},
|
||||
loginTitle: {
|
||||
|
@ -96,7 +97,7 @@ export default class LoginView extends LoggedView {
|
|||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { Site_Name, error } = this.props;
|
||||
if (Site_Name && nextProps.Site_Name !== Site_Name) {
|
||||
if (nextProps.Site_Name && nextProps.Site_Name !== Site_Name) {
|
||||
this.setTitle(nextProps.Site_Name);
|
||||
} else if (nextProps.failure && !equal(error, nextProps.error)) {
|
||||
if (nextProps.error && nextProps.error.error === 'totp-required') {
|
||||
|
|
|
@ -16,7 +16,6 @@ import StatusBar from '../../containers/StatusBar';
|
|||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
customEmojis: state.customEmojis,
|
||||
room: state.room,
|
||||
user: {
|
||||
id: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
|
@ -33,7 +32,7 @@ export default class MentionedMessagesView extends LoggedView {
|
|||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
customEmojis: PropTypes.object,
|
||||
room: PropTypes.object
|
||||
navigation: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -42,6 +41,8 @@ export default class MentionedMessagesView extends LoggedView {
|
|||
loading: false,
|
||||
messages: []
|
||||
};
|
||||
this.rid = props.navigation.getParam('rid');
|
||||
this.t = props.navigation.getParam('t');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -71,10 +72,9 @@ export default class MentionedMessagesView extends LoggedView {
|
|||
this.setState({ loading: true });
|
||||
|
||||
try {
|
||||
const { room } = this.props;
|
||||
const result = await RocketChat.getMessages(
|
||||
room.rid,
|
||||
room.t,
|
||||
this.rid,
|
||||
this.t,
|
||||
{ 'mentions._id': { $in: [user.id] } },
|
||||
messages.length
|
||||
);
|
||||
|
@ -93,7 +93,7 @@ export default class MentionedMessagesView extends LoggedView {
|
|||
|
||||
renderEmpty = () => (
|
||||
<View style={styles.listEmptyContainer} testID='mentioned-messages-view'>
|
||||
<Text>{I18n.t('No_mentioned_messages')}</Text>
|
||||
<Text style={styles.noDataFound}>{I18n.t('No_mentioned_messages')}</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
|
@ -101,7 +101,6 @@ export default class MentionedMessagesView extends LoggedView {
|
|||
const { user, customEmojis, baseUrl } = this.props;
|
||||
return (
|
||||
<Message
|
||||
style={styles.message}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import sharedStyles from '../Styles';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
export default StyleSheet.create({
|
||||
list: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff'
|
||||
},
|
||||
message: {
|
||||
transform: [{ scaleY: 1 }]
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
listEmptyContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#ffffff'
|
||||
backgroundColor: COLOR_WHITE
|
||||
},
|
||||
noDataFound: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textColorNormal
|
||||
}
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue