[RELEASE] Merge beta into master (#961)
* Create LICENSE * Beta (#265) * Fabric iOS * Fabric configured on iOS and Android * - react-native-fabric configured - login tracked * README updated * Run scripts from README updated * README scripts * get rooms and messages by rest * user status * more improves * more improves * send pong on timeout * fix some methods * more tests * rest messages * Room actions (#266) * Toggle notifications * Search messages * Invite users * Mute/Unmute users in room * rocket.cat messages * Room topic layout fixed * Starred messages loading onEndReached * Room actions onEndReached * Unnecessary login request * Login loading * Login services fixed * User presence layout * ïmproves on room actions view * Removed unnecessary data from SelectedUsersView * load few messages on open room, search message improve * fix loading messages forever * Removed state from search * Custom message time format * secureTextEntry layout * Reduce android app size * Roles subscription fix * Public routes navigation * fix reconnect * - New login/register, login, register * proguard * Login flux * App init/restore * Android layout fixes * Multiple meteor connection requests fixed * Nested attachments * Nested attachments * fix check status * New login layout (#269) * Public routes navigation * New login/register, login, register * Multiple meteor connection requests fixed * Nested attachments * Button component * TextInput android layout fixed * Register fixed * Thinner close modal button * Requests /me after login only one time * Static images moved * fix reconnect * fix ddp * fix custom emoji * New message layout (#273) * Grouping messages * Message layout * Users typing animation * Image attachment layout * Fabric and image fix (#284) * Fixed images not showing * Keyboard libs updated * Fabric fix and location removed (#286) * Proguard disabled * message with list + links fixed (#288) * Better image cache component (#292) * react-native-img-cache removed * Improve list render * Support <http://link/Text> inside markdown * Deep linking (#291) * deep linking * Basic deep link working * Deep link routing * Multiple servers working * Send user to the room * Avatar initials and room type icon (#298) * Deep linking fix and more (#294) * Fix - Any https link was deep linking to RocketChat * Keyboard dismiss after add new server * Room info bug fix * Opacity animation * Navigation when adding server fixed * Throttle for unnecessary render on receiving several messages * Search inputs without autocorrect and autocapitalize * Search messages fixed * Messagebox unnecessary render and spotlight fixed * react-native-keyboard-input updated * Lint * Tests updated * Update all dependencies (#299) * Update react-navigation to the latest version 🚀 (#293) * fix(package): update react-navigation to version 2.0.0 * Code updated to support breaking changes of react-navigation * Detox tests E2E (#283) * RoomsListView re-render (#304) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> - [x] Removed unnecessary re-renders on RoomsListView * [NEW] Broadcast channels (#301) * Broadcast channels * e2e tests * New markdown (#306) Our current markdown is causing a lot of issues on Android devices, since it wraps everything inside a Text component. On Android, Text doesn't support View as a child. This PR adds react-native-markdown-renderer, that uses View as wrapper and may be better. * Fixed audio recording issues (#310) * Fix for "java.lang.IllegalArgumentException: unexpected url" (#313) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> User was able to add an invalid instance of Rocket.Chat by pressing submit button instead of "Connect" button. <!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> * I18n (#312) * Unread and date separator layout improved (#319) <!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> @RocketChat/ReactNative <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> - [x] Unread and date separator layout - [x] "Start of conversation"/"Loading messages" label   <!-- 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 -->   * [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 -->  * [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 -->    * 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 -->    * [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 -->   * [FIX] Only single attachment rendered (#417) * [NEW] Rooms list layout (#413) * RoomsListView layout * Rooms list layout * Sort component * Header icons * Default header colors * Add server dropdown * Close sort dropdown if server dropdown will open * UserItem * Room type icon * Search working * Tests updated * Android layout * Using realm queries instead of array iterates * Animation duration * Fixed render bug * [NEW] Create channel layout (#420) * RoomsListView layout * Rooms list layout * Sort component * Header icons * Default header colors * Add server dropdown * Close sort dropdown if server dropdown will open * UserItem * Room type icon * Search working * Tests updated * Android layout * Using realm queries instead of array iterates * Animation duration * Fixed render bug * - NewMessageView - backButtonTitle always empty - SearchBox created * New create channel layout * Search refactored * loginSuccess dismiss modal * Tests working * [FIX] Open unsupported videos on browser (#422) * 1.1 * Sort/group rooms local only (#425) * Update android api from ci * Sort local only * [FIX] Missing current server (#427) * server.current removed * Increased area of touch on header * Hide search when sort dropdown is tapped * default server icon url * 1.1.1 * [NEW] Experimental Icon (#430) * [NEW] Message layout (#426) * message container/component * Separator component * Reply * Url * tests updated * Minor changes * Audio component * Broadcast button * Minor touches * Reply preview * Edited * Minor bug fixes * - Update roadmap - Bump version to 1.2 * Onboarding styles fix * [FIX] Drawer navigation won't refresh chats (#432) * Avoid errors on Audio/Image/Video (#443) * Bump version to 1.2.1 (#444) * Stop supporting Android 4.4 and lower (#447) * Several fixes for 1.2.1 (#448) * Fix user.roles * Better onLongPress handle on messages * Indicator position * Fix role undefined in system messages * Add baseUrl in case of file attachments * Join room fixed * RoomView params * Broadcast fixes * Add server layout changes * Use native images * Subscribe to not joined channels * Fix alerts without i18n * Tests updated * Bump version to 1.2.2 (#449) * [NEW] Use community JSC for Android (#450) * [NEW] Use community JSC for Android * Quick fix on unread chats * [NEW] Show app version (#454) * [NEW] Portuguese translation (#452) * [NEW] Portuguese translation * Remove servers from sidebar * Update dependencies (#431) * Update dependencies * Lint and test * Added react-native fork * rn 57 * Lint and tests updated * Update xcode on circleci * Use legacy build system * Update tests * Use inline requires (#459) * Update dependencies * Lint and test * Added react-native fork * rn 57 * Lint and tests updated * Update xcode on circleci * Use legacy build system * Update tests * Inline requires * Fix eslint and remove temp gradle * Unnecessary renders * Update isNotch and Readme * Tests updated * Bump version to 1.3.0 (#461) * Better touch handling on rooms list (#462) * Use react-native-gesture-handler at RoomItem * Fixed info message author * Edit message render improvement * Fix ws to http replace * Bump version to 1.3.1 (#463) * Composer layout tweaked (#464) * Composer layout tweaked * Fix localization error * Bump version to 1.3.2 * [FIX] Handle deleted messages (#466) * [FIX] Handle deleted messages * Fix rest error * Fix some connection issues * [FIX] Search rooms (#468) * Bump version to 1.3.3 (#469) * Connecting to DDP badge (#471) * Display custom fields on user info (#476) * Render custom fields on user info * renderCustomFields fix * Display custom fields in user info * Fix lint error * [FIX] DDP badge wasn't hiding on fast connections (#477) * Use Rocket.Chat JS SDK (#481) * JS SDK * API working * Multiple servers * Bump version to 1.4.0 (#482) * [FIX] 2FA and LDAP (#488) * [FIX] Unread rooms group order (#487) * Use grouping setting on temp messages (#486) * [FIX] Delete room error (#485) * Rename to Rocket.Chat Experimental (#483) * Update dependencies (#484) * Bump version to 1.4.0 (#482) * test * one more test * Fix build * Regression: Wait for unmount to delete database after logout (#489) * Bump version to 1.4.1 (#490) * Regression: Crash on Android search (#492) * Bump version to 1.4.2 (#493) * Update Rocket.Chat.js.SDK (#494) * Bump version to v1.4.3 (#495) * [FIX] OAuth (#496) * Smaller header icons inside the room (#499) * [FIX] Logout (#497) * [FIX] Logout * Removed realm instances on rooms list * Bump version to 1.4.4 (#498) * Update navigation library (#501) * v2 * Working on Android 0.57.3 * Drawer working * Removing v1 navigator * - Splash screen - Icons changed * Deeplink * Remove EventEmitter from CreateChannelView * Android search * Android notifications * OAuth * Fix search props * Lint and tests fixed * Fix android build * Improvements on iPhone X* usage * Fix detox * Fix android build * Room.f added to RoomView.shouldComponentUpdate * Animations on RoomsListView and RoomView * Fix topbar buttons on Android * Bump version to 1.5.0 (#503) * Check $FABRIC_KEY availability in CircleCI (#506) * Check $FABRIC_KEY in CircleCI * Remove config scripts * Check $FABRIC_KEY availability in CircleCI for iOS (#507) * [I18n] Add Simplified Chinese(zh-CN) locale (#505) * [FIX] iOS pop gesture not working properly (#509) * Check if lastMessage has an attachment and show "User sent an attachment" at RoomsList (#510) * [FIX] Messages not being loaded properly (#513) * Fetch avatar initials from server (#512) * Fix iOS pop gesture and open sidemenu gesture (#511) * Bump version to 1.5.1 (#516) * [NEW] Room header layout (#521) * Clear iOS notification on resume/open (#520) * [FIX] Flashing avatars on Android after #512 (#519) * [FIX] App connects to previous server instead of the recent added (#518) * [FIX] Room view header crashes when destructuring reducer (#523) * [FIX] Dismiss keyboard on room close (#530) * [FIX] Composer composer's send icon slowness (#528) * [WIP] New Authentication layout (#536) New Authentication layout * Regression: Resend messages with error (#532) * DDP Connection badge animation changed (#533) * [FIX] Upload buttons on Android (#541) * Bump version to 1.6.0 (#543) * I18n: Add missing translation of simplified Chinese (#539) * Update dependencies (#544) * AndroidManifest changes * Regression: Deep linking stopped working after react-native-navigation update (#549) * [FIX] Android stuck on splash screen after hardware back button is pressed (#550) * [FIX] Android stuck on splash screen after hardware button is pressed * Fix empty user at asyncstorage * Remove unused subscribe * [FIX] x-instance-id header prop is case insensitive (#551) * Bump version to 1.6.1 (#553) * [FIX] x-instance-id header prop is case insensitive * Use Rest API calls (#558) * Chats: Don't show group header if none of the filters is selected (#560) * [CHORE] Update Xcode image version on CircleCI (#561) * Bump version to 1.7.0 (#562) * [FIX] Load messages on notification tap (#564) * Use Rest API pt 2 (#568) * Room files * Pinned messages * Starred messages * Mentioned messages * Search messages * Bug fixes * Profile * Livechat * Block/unblock user * Erase room * Archive room * Remove unused method * Bug fix * [CHORE] Add hold step on CircleCI before TestFlight (#572) * [FIX] GET /info to check if it's a valid server instead of x-instance-id (#573) * Bump version to 1.7.1 (#574) * Unnecessary re-renders removed (#570) * shouldComponentUpdate * Rooms list shouldcomponentupdate * RoomView shouldComponentUpdate * Messagebox and Message shouldComponentUpdate * EmojiPicker shouldComponentUpdate * RoomActions shouldComponentUpdate * Room info shouldComponentUpdate * Update RNN * Use only one Flatlist if none group filter is selected * Update fix * shouldComponentUpdate * Bug fixes * ListView changes * Bug fix * render list bug fix * Changes on public channels * - RoomView saga leak removed - Join room e2e tests added * Rest versions * Method call versions * Min RocketChat version alert * Update dependencies (#587) * [FIX] Better message actions (#567) * [FIX] Back button press on message actions (#592) * Bump version to 1.8.0 (#595) * [FIX] LDAP login (#596) * Create class to manage navigation (#594) * Add Navigation class * Place Drawer.js logic inside of Navigation * Load less views at startup * [FIX] v1.8.0 (#599) * Downgrade react-native-fast-image * Update iOS permission usage descriptions * [FIX] Delete upload item * Update JS SDK version (#602) * Add Icons class (#611) Creates Icons class to manage when to load icons from native side or react-native-vector-icons. It also fixes `react-native run-android` #517 * Updating room indicator (#609) Shows "Updating..." when requesting rooms from Rest API. * [FIX] Load avatar on servers that prevent unauthenticated avatar access (#604) App would show an empty space on servers that require authentication on avatar access * [FIX] 2FA login in a server with LDAP enabled (#612) * [FIX] Start loop searching for rooms updates only when connection goes down and SDK has userId (#613) * Allow to create empty channel (#615) * [FIX] Reply title should break text (#616) * Bump version to 1.9.0 (#617) * [FIX] SDK issues (#621) * Remove listeners from room * Properly close connections on change server * Minor layout change on connecting badge * [CHORE] Add TestFlight invite and update Readme (#623) * [FIX] npm -> yarn dependencies migration (#622) * I18n: Add French (#629) * [FIX] Remove rooms listener (#630) * [CHORE] Update issue template (#638) * I18n: Add German (#641) * Bump version to 1.10.0 (#644) * [FIX] Prevent mass is typing dispatchs (#651) * [FIX] Handle database errors properly (#650) * [FIX] Change actions labels (#654) * [FIX] Room members filter (#655) * [FIX] uploadProgress is not a function (#656) * [FIX] Slow messagebox (#658) * Remove drawer (#653) * Remove drawer (layout needs to be changed in future releases, though) * Don't navigate outside on logout if there's other logged server * Update react-native-navigation * Message button (#660) * Remove touchable opacity when scrolling messages * Tap on disable messages closes keyboard * Unify vibration * Vibrate only on Android * [FIX] Fetch rooms date (#662) * [FIX] Select emoji error (#666) * Update Realm to 2.24 (#667) * Update React Native to 0.58.6 (#668) * [FIX] Fix some language issues in German language (#664) * New icons (#643) * New Icons * Remove unused assets * Change send icon * Layout tweaks * Refactor Status * Styles changed * User layout fix * Separator layout changes * Sidebar status layout fix * Fix Message.onLongPress issue * Fix code markdown Closes https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/625 * Status lint * Fix tests * Navigation debounce * RoomActions icons * Space between components * Group text * Update tests * [CHORE] Remove .debug suffix on Android (#681) * [FIX] Fix null native Messagebox component object (#680) * Fix null native Messagebox component object * [iOS] Fix header alignment * Remove unused files * Switch to react-navigation (#687) * Update readme (#714) * Bump to 1.10.1 (#731) * [FIX] Deep linking between multiple logged servers (#730) * Fix handle invisible status (#692) * I18n: Add Portuguese (Portugal) (#722) * [FIX] Show ActivityIndicator in RoomMembersView (#686) * Bump version to 1.11.0 (#761) * Migrate from GCM to FCM (#760) * [NEW] Scrollable room name feature (#756) * [NEW] Scroll down floating button (#735) * [CHORE] Added Storybook documentation (#757) * Use FlatList in RoomView (#762) * [FIX] iOS requiring location permission (#768) * Room item layout (#771) * [NEW] Draft message per room (#772) * [FIX] Add Realm.safeAddListener (#785) * [CHORE] Remove tvOS target (#779) * [NEW] Discussions (#696) * Bump version to 1.12.0 (#804) * [NEW] Threads (#798) * RoomsListView improvements (#819) * [FIX] Giphy not showing (#810) * [FIX] Apply emojify on empty texts (#824) * Lock drawer when stack is not on root screen (#825) * Room item layout (#835) * [FIX] Threads (#838) Closes #826 Closes #827 Closes #828 Closes #829 Closes #830 Closes #831 Closes #832 Closes #833 * [FIX] Smaller thread title (#846) * [FIX] Smaller thread title * Remove markdown notation from thread title * On message press debounce * Align vertical thread title * [Regression] Search stopped working on Android after LastMessage refactor (#851) * Load legal pages from web (#849) * Update fetch permissions api (#850) * Update custom emojis endpoint (#852) * Update emoji endpoint * Use React.memo on Markdown * Support RC versions lower than 0.75.0 * Realm migration * Fetch roles from rest api (#853) * Fetch roles from rest api * Fix RoomInfoView role get * Remove roles from redux * Bump version to 1.13 (#857) * Active users improvements (#855) * Remove connection badge (#862) * Connecting indicator on RoomsListView header * Connecting indicator on RoomView header * Remove ConnectionBadge * Show updating on RoomView load messages * Update dependencies (#863) * Minor updates * Update jsc-android * Update react-native-modal * Minor updates * Update react-native-fast-image * Minor dev updates * Few major updates * Update react-native-keyboard-aware-scroll-view * Update pods * Update android-support * Update tests * Remove duplicated getRoleDescription function (#866) * [FIX] Load local URL image (#871) * [FIX] Toggle/follow thread icon (#867) * Tweaks on sequential threads messages layout (#858) * Tweaks on sequential threads messages * Update tests * Fix quote * Prevent from deleting thread start message when positioned inside the thread * Remove thread listener from RightButtons * Fix error on thread start parse * Stop parsing threads on render * Check replied thread only if necessary * Fix messages don't displaying * Fix threads e2e * RoomsListView.updateState slice * Stop fetching hidden messages on threads * Set initialNumToRender to 5 * [FIX] Check if room is mounted before setting state (#864) * Tweaks on sequential threads messages * Update tests * Fix quote * Prevent from deleting thread start message when positioned inside the thread * Remove thread listener from RightButtons * Fix error on thread start parse * Stop parsing threads on render * Check replied thread only if necessary * Fix messages don't displaying * Fix threads e2e * RoomsListView.updateState slice * Stop fetching hidden messages on threads * Check if RoomView is mounted before rendering * Refactor navigation events on RoomsListView * Fix lint * Fix listener * [FIX] Typing not getting cleared after popping a room (#873) * [CHORE] Remove e2e tests from CI (#875) * [FIX] Remove listeners on RoomView header unmount (#874) * issue #799 merger message views (#876) On Room Actions, we have Files, Mentions, Starred and Pinned. They have similar APIs and logic. All of those could be merged into one generic view (MessagesView). Maybe even Search could be in this merge. Note: They're similar, but have own rules (unstar, unpin, etc). This change may reduce 1MB to our release bundle size, since we're going to remove a lot of boilerplate. * Bump version to 1.14.0 (#889) * Remove "updating" indicator inside the room (#895) * Switch toast lib (#898) * removed toast from ios * changed showToast to showAlert * removed from android * fix lint * conflict resolved * fixed lint * Fix toast position * Change toast style * Use followMessage from rest * Temporary disable some visual toast tests * Unnecessary lib version change * [NEW] Report message (#818) * [NEW] Admin (#800) * added admin panel * reverting some changes * fixed problem with authToken * changed tab to space * done requested changes * fixed lint * added react-native-webview * Install webview pod * Message render performance (#880) - Refactored Message component to use React.memo and re-render only what's necessary - Added a test mode to toggle markdown parse by long press drawer (it'll be removed in the next release) * [CHORE] Add pre-commit rules (#816) Run lint and jest during pre-commit * [IMPROVEMENT] Add toggle markdown to settings (#907) * Add toggle markdown to settings * Remove unused translation * [FIX] Message grouping not re-rendering (#911) * [FIX] Get custom emoji on reactions modal (#913) * Update RN to 0.59.8 (#896) * update IOS react native to 0.59.8 * update Android react native to 0.59.8 * fix eslint errors * Android debug working * Android build * Fix lint * Making jest happy * Update CircleCI android image * Fix android build * Use 32 bits * Fix iOS build * Update detox * Use new Xcode build system * Use old build system * Update realm (64 bits support) * [CHORE] Upgrade Mac CI image to 10.2.1 (#914) * [CHORE] Update readme (#885) * [FIX] Reaction count not rerendering (#917) * [CHORE] Android app bundle (#915) * [CHORE] Upgrade Mac CI image to 10.2.1 * [CHORE] Android App Bundle * Fix CI * Fix arch typo (#918) * [IMPROVEMENT] Messagebox typing and buttons refactor (#920) * Debounce onChangeText * Refactor FilesActions * Clear input asap * Different buttons on iOS/Android * Minor fragment refactor * Import emoji keyboard on android only * [CHORE] Use react-native-firebase (#928) We need to migrate from deprecated react-native-fabric to react-native-firebase. This PR enables following Firebase features: * Analytics * Crashlytics * Performance It also tracks screen view without the necessity of HOC. Future work: I won't do it in this PR because it's large enough, but we need to log more app events, like 'sent_message', 'open_admin', 'media_upload', etc. * [FIX] Analytics error events (#930) * [IMPROVEMENT] Update user presence endpoint (#924) * [IMPROVEMENT] Update user presence endpoint * Use `from` parameter in case of reconnection * [IMPROVEMENT] Share channel (#908) * Generate and share permalink to rooms * Create constant to share type * Fix unnecessary await * Remove unnecessary test * Revert delete e2e test * [FIX] App crash with backspace on input message (#906) * Fix - App crash with backspace on input message * Improving code to fix backspace bug * Fix destructuring undefined * Improvement code to fix backspace bug * [FIX] Gitlab url hardcoded (#921) * [FIX] Gitlab url hardcoded problem * Closes https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/251 * Fix API_Gitlab_URL type * [CHORE] Split Google Services in debug and production (#941) * Split android * Split iOS * Update CI * [FIX] Crash on message long press (#945) * [FIX] Reply preview showing the entire message (#947) * [IMPROVEMENT] Open links as push instead of modal (#949) * [FIX] Crashing during app launch on Samsung devices (#937) * Apply alpha update * Update to Realm released fix * [FIX] Thread crash if room is undefined (#956)
This commit is contained in:
parent
fe46929238
commit
1610ba5759
|
@ -44,7 +44,7 @@ jobs:
|
|||
|
||||
e2e-test:
|
||||
macos:
|
||||
xcode: "10.1.0"
|
||||
xcode: "10.2.1"
|
||||
|
||||
environment:
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
|
@ -90,7 +90,7 @@ jobs:
|
|||
android-build:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/android:api-28-node8-alpha
|
||||
- image: circleci/android:api-28-node
|
||||
|
||||
environment:
|
||||
# GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
|
||||
|
@ -131,24 +131,18 @@ jobs:
|
|||
|
||||
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
|
||||
|
||||
if [[ $FABRIC_KEY ]]; then
|
||||
echo -e "" > ./app/fabric.properties
|
||||
echo -e "apiKey=$FABRIC_KEY" >> ./app/fabric.properties
|
||||
echo -e "apiSecret=$FABRIC_SECRET" >> ./app/fabric.properties
|
||||
fi
|
||||
|
||||
- run:
|
||||
name: Install Android Depedencies
|
||||
name: Set Google Services
|
||||
command: |
|
||||
cd android
|
||||
./gradlew androidDependencies
|
||||
cd android/app
|
||||
cp google-services.prod.json google-services.json
|
||||
|
||||
- run:
|
||||
name: Build Android App
|
||||
command: |
|
||||
cd android
|
||||
if [[ $KEYSTORE ]]; then
|
||||
./gradlew assembleRelease
|
||||
./gradlew bundleRelease
|
||||
else
|
||||
./gradlew assembleDebug
|
||||
fi
|
||||
|
@ -172,7 +166,7 @@ jobs:
|
|||
|
||||
ios-build:
|
||||
macos:
|
||||
xcode: "10.1.0"
|
||||
xcode: "10.2.1"
|
||||
|
||||
environment:
|
||||
BASH_ENV: "~/.nvm/nvm.sh"
|
||||
|
@ -201,13 +195,11 @@ jobs:
|
|||
command: |
|
||||
yarn
|
||||
|
||||
# - run:
|
||||
# name: Fix known build error
|
||||
# command: |
|
||||
# # Fix error https://github.com/facebook/react-native/issues/14382
|
||||
# cd node_modules/react-native/scripts/
|
||||
# curl https://raw.githubusercontent.com/facebook/react-native/5c53f89dd86160301feee024bce4ce0c89e8c187/scripts/ios-configure-glog.sh > ios-configure-glog.sh
|
||||
# chmod +x ios-configure-glog.sh
|
||||
- run:
|
||||
name: Set Google Services
|
||||
command: |
|
||||
cd ios
|
||||
cp GoogleService-Info.prod.plist GoogleService-Info.plist
|
||||
|
||||
- run:
|
||||
name: Fastlane Build
|
||||
|
@ -215,11 +207,6 @@ jobs:
|
|||
command: |
|
||||
cd ios
|
||||
agvtool new-version -all $CIRCLE_BUILD_NUM
|
||||
/usr/libexec/PlistBuddy -c "Set Fabric:APIKey $FABRIC_KEY" ./RocketChatRN/Info.plist
|
||||
|
||||
if [[ $FABRIC_KEY ]]; then
|
||||
echo -e > "./Fabric.framework/run $FABRIC_KEY $FABRIC_SECRET" > ./RocketChatRN/Fabric.sh
|
||||
fi
|
||||
|
||||
if [[ $MATCH_KEYCHAIN_NAME ]]; then
|
||||
fastlane ios release
|
||||
|
@ -240,7 +227,7 @@ jobs:
|
|||
|
||||
ios-testflight:
|
||||
macos:
|
||||
xcode: "10.1.0"
|
||||
xcode: "10.2.1"
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
|
|
@ -118,7 +118,7 @@ module.exports = {
|
|||
"new-cap": [2],
|
||||
"use-isnan": 2,
|
||||
"valid-typeof": 2,
|
||||
"linebreak-style": [2, "unix"],
|
||||
"linebreak-style": 0,
|
||||
"prefer-template": 2,
|
||||
"template-curly-spacing": [2, "always"],
|
||||
"quotes": [2, "single"],
|
||||
|
|
14
README.md
14
README.md
|
@ -61,11 +61,9 @@ Readme will guide you on how to config.
|
|||
## Current priorities
|
||||
1) [NEW] Jitsi integration ([#711][i711])
|
||||
2) [NEW] Federation ([#706][i706])
|
||||
3) [NEW] Threads ([#707][i707])
|
||||
4) [NEW] Record video ([#712][i712])
|
||||
5) [NEW] Slash Commands ([#405][i405])
|
||||
6) [NEW] Draft message per room ([#708][i708])
|
||||
7) [NEW] Share extension ([#391][i391])
|
||||
3) [NEW] Record video ([#712][i712])
|
||||
4) [NEW] Slash Commands ([#405][i405])
|
||||
5) [NEW] Share extension ([#391][i391])
|
||||
|
||||
[i711]: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/711
|
||||
[i706]: https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/706
|
||||
|
@ -80,11 +78,11 @@ Readme will guide you on how to config.
|
|||
|--------------------------------------------------------------- |-------- |
|
||||
| Jitsi Integration | ❌ |
|
||||
| Federation (Directory) | ❌ |
|
||||
| Threads | ❌ |
|
||||
| Threads | ✅ |
|
||||
| Record Audio | ✅ |
|
||||
| Record Video | ❌ |
|
||||
| Commands | ❌ |
|
||||
| Draft message per room | ❌ |
|
||||
| Draft message per room | ✅ |
|
||||
| Share Extension | ❌ |
|
||||
| Notifications Preferences | ✅ |
|
||||
| Edited status | ✅ |
|
||||
|
@ -102,7 +100,7 @@ Readme will guide you on how to config.
|
|||
| Theming | ❌ |
|
||||
| Settings -> Review the App | ❌ |
|
||||
| Settings -> Default Browser | ❌ |
|
||||
| Admin panel | ❌ |
|
||||
| Admin panel | ✅ |
|
||||
| Reply message from notification | ❌ |
|
||||
| Unread counter banner on message list | ✅ |
|
||||
| E2E | ❌ |
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,6 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply plugin: "io.fabric"
|
||||
apply plugin: "com.google.firebase.firebase-perf"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
|
||||
|
@ -95,25 +97,22 @@ def enableSeparateBuildPerCPUArchitecture = false
|
|||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "28.0.3"
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "chat.rocket.reactnative"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "1.13.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
versionName "1.14.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/libjsc.so'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
if (project.hasProperty('KEYSTORE')) {
|
||||
|
@ -129,14 +128,11 @@ android {
|
|||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86"
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
// shrinkResources enableProguardInReleaseBuilds
|
||||
// zipAlignEnabled enableProguardInReleaseBuilds
|
||||
// useProguard enableProguardInReleaseBuilds
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
|
||||
signingConfig signingConfigs.release
|
||||
|
@ -147,7 +143,7 @@ android {
|
|||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2]
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
|
@ -155,51 +151,23 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven { url 'https://maven.fabric.io/public' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// These docs use an open ended version so that our plugin
|
||||
// can be updated quickly in response to Android tooling updates
|
||||
|
||||
// We recommend changing it to the latest version from our changelog:
|
||||
// https://docs.fabric.io/android/changelog.html#fabric-gradle-plugin
|
||||
classpath 'io.fabric.tools:gradle:1.+'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'io.fabric'
|
||||
|
||||
repositories {
|
||||
maven { url 'https://maven.fabric.io/public' }
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'org.webkit:android-jsc:r241213'
|
||||
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-tasks') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-stats') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-basement') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
bundle {
|
||||
language {
|
||||
enableSplit = false
|
||||
}
|
||||
density {
|
||||
enableSplit = true
|
||||
}
|
||||
abi {
|
||||
enableSplit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':react-native-firebase')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':react-native-orientation-locker')
|
||||
implementation project(':react-native-splash-screen')
|
||||
implementation project(':react-native-screens')
|
||||
|
@ -208,31 +176,30 @@ dependencies {
|
|||
implementation project(':react-native-gesture-handler')
|
||||
implementation project(':react-native-image-crop-picker')
|
||||
implementation project(':react-native-i18n')
|
||||
implementation project(':react-native-fabric')
|
||||
implementation project(':react-native-audio')
|
||||
implementation project(":reactnativekeyboardinput")
|
||||
implementation project(':react-native-video')
|
||||
implementation project(':react-native-vector-icons')
|
||||
implementation project(':rn-fetch-blob')
|
||||
implementation project(':react-native-toast')
|
||||
implementation project(':react-native-fast-image')
|
||||
implementation project(':realm')
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation 'org.webkit:android-jsc-cppruntime:+'
|
||||
implementation "com.android.support:appcompat-v7:28.0.0"
|
||||
implementation "com.android.support:support-v4:28.0.0"
|
||||
implementation 'com.android.support:customtabs:28.0.0'
|
||||
implementation 'com.android.support:design:28.0.0'
|
||||
implementation "com.android.support:appcompat-v7:${ rootProject.ext.supportLibVersion }"
|
||||
implementation "com.android.support:support-v4:${ rootProject.ext.supportLibVersion }"
|
||||
implementation "com.android.support:customtabs:${ rootProject.ext.supportLibVersion }"
|
||||
implementation "com.android.support:design:${ rootProject.ext.supportLibVersion }"
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation 'com.facebook.fresco:fresco:1.10.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:1.10.0'
|
||||
implementation 'com.facebook.fresco:animated-webp:1.10.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:1.10.0'
|
||||
implementation "com.google.firebase:firebase-core:16.0.1"
|
||||
implementation "com.google.firebase:firebase-messaging:17.3.4"
|
||||
implementation "com.google.android.gms:play-services-base:16.1.0"
|
||||
implementation "com.google.firebase:firebase-messaging:18.0.0"
|
||||
implementation "com.google.firebase:firebase-core:16.0.9"
|
||||
implementation "com.google.firebase:firebase-perf:16.2.5"
|
||||
implementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') {
|
||||
transitive = true;
|
||||
transitive = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,4 +211,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
|||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true
|
|
@ -1,2 +0,0 @@
|
|||
apiKey=ef3f46fdf18479fd3e1b9b78d0ec73751a255e14
|
||||
apiSecret=e8e3d04c28bc04acd009484da5bb9d1440c4f53851564e9f95c3225ec8b0bc76
|
|
@ -1,242 +1,37 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "673693445664",
|
||||
"firebase_url": "https://rocketchat-9e9be.firebaseio.com",
|
||||
"project_id": "rocketchat-9e9be",
|
||||
"storage_bucket": "rocketchat-9e9be.appspot.com"
|
||||
"project_number": "115198584049",
|
||||
"firebase_url": "https://rocketchat-reactnative-test.firebaseio.com",
|
||||
"project_id": "rocketchat-reactnative-test",
|
||||
"storage_bucket": "rocketchat-reactnative-test.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:6ef4638e500ec958",
|
||||
"android_client_info": {
|
||||
"package_name": "RocketChat"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:16da2e50aff9f0c9",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.android"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-hrjftksij02vqtd467ln2cubvu48ft5j.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android",
|
||||
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-k0mvosdjoe5dbvqce3b377ckabb5dgu8.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android",
|
||||
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 2,
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.ios",
|
||||
"app_store_id": "1148741252"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:1551054db195f705",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.android.dev"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-t5aeku0oie010npd40a0tgn27c418vk7.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android.dev",
|
||||
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-iml14ln4vccuu7liclrpt2k671fkjs38.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android.dev",
|
||||
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 2,
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.ios",
|
||||
"app_store_id": "1148741252"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:8be27b1f7c42a2ed",
|
||||
"mobilesdk_app_id": "1:115198584049:android:8be27b1f7c42a2ed",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.reactnative"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_id": "115198584049-ack609b1338b827fta26s9rd2ab1aad5.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
"current_key": "AIzaSyAWwowhAfACHBw3YxmDOXY3QyakgjhJLqc"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:64932c99863e2838",
|
||||
"android_client_info": {
|
||||
"package_name": "com.konecty.rocket.chat"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-3ajben08beuco6eout3kpod2gbbm8fij.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.konecty.rocket.chat",
|
||||
"certificate_hash": "cd5806ba3f0141d0f2e47acfe64a485f575108ab"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 2,
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_id": "115198584049-ack609b1338b827fta26s9rd2ab1aad5.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-dumairnsk1sbkca5nmsq2b5kdglqpc0a.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.ios",
|
||||
"app_store_id": "1148741252"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "673693445664",
|
||||
"firebase_url": "https://rocketchat-9e9be.firebaseio.com",
|
||||
"project_id": "rocketchat-9e9be",
|
||||
"storage_bucket": "rocketchat-9e9be.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:6ef4638e500ec958",
|
||||
"android_client_info": {
|
||||
"package_name": "RocketChat"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.reactnative"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:16da2e50aff9f0c9",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.android"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-k0mvosdjoe5dbvqce3b377ckabb5dgu8.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android",
|
||||
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-hrjftksij02vqtd467ln2cubvu48ft5j.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android",
|
||||
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.reactnative"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:1551054db195f705",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.android.dev"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-t5aeku0oie010npd40a0tgn27c418vk7.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android.dev",
|
||||
"certificate_hash": "41cf750df786a6d9da712a98a629d0c8391876d6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-iml14ln4vccuu7liclrpt2k671fkjs38.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "chat.rocket.android.dev",
|
||||
"certificate_hash": "33fa8582794176014a59054192e261bfad0e5273"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.reactnative"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:8be27b1f7c42a2ed",
|
||||
"android_client_info": {
|
||||
"package_name": "chat.rocket.reactnative"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.reactnative"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:673693445664:android:64932c99863e2838",
|
||||
"android_client_info": {
|
||||
"package_name": "com.konecty.rocket.chat"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-3ajben08beuco6eout3kpod2gbbm8fij.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.konecty.rocket.chat",
|
||||
"certificate_hash": "cd5806ba3f0141d0f2e47acfe64a485f575108ab"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDIkZj1TRz8TmhnMswDwVY5OnWuzFK3rxg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "673693445664-97s9t777ful7mn2510vuhb48958qd9tb.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "673693445664-jbf9m30ta163gobjfp0v7j1v7kpo7kmv.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "chat.rocket.reactnative"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" android:networkSecurityConfig="@xml/react_native_config" />
|
||||
</manifest>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">10.0.3.2</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
|
@ -1,8 +1,8 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="chat.rocket.reactnative">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
@ -15,7 +15,7 @@
|
|||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:resizeableActivity="true">
|
||||
>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
|
|
|
@ -3,6 +3,11 @@ package chat.rocket.reactnative;
|
|||
import android.app.Application;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import io.invertase.firebase.RNFirebasePackage;
|
||||
import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage;
|
||||
import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage;
|
||||
import io.invertase.firebase.perf.RNFirebasePerformancePackage;
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
import org.wonday.orientation.OrientationPackage;
|
||||
import org.devio.rn.splashscreen.SplashScreenReactPackage;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
|
@ -14,12 +19,9 @@ import com.AlexanderZaytsev.RNI18n.RNI18nPackage;
|
|||
import com.reactnative.ivpusic.imagepicker.PickerPackage;
|
||||
import com.RNFetchBlob.RNFetchBlobPackage;
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.crashlytics.android.Crashlytics;
|
||||
import com.dylanvann.fastimage.FastImageViewPackage;
|
||||
import com.oblador.vectoricons.VectorIconsPackage;
|
||||
import com.remobile.toast.RCTToastPackage;
|
||||
import com.rnim.rn.audio.ReactNativeAudioPackage;
|
||||
import com.smixx.fabric.FabricPackage;
|
||||
import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
|
@ -30,7 +32,6 @@ import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
|||
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
|
||||
import com.learnium.RNDeviceInfo.RNDeviceInfo;
|
||||
import com.actionsheet.ActionSheetPackage;
|
||||
import io.fabric.sdk.android.Fabric;
|
||||
import io.realm.react.RealmReactPackage;
|
||||
import com.swmansion.rnscreens.RNScreensPackage;
|
||||
|
||||
|
@ -52,6 +53,11 @@ public class MainApplication extends Application implements ReactApplication, IN
|
|||
protected List<ReactPackage> getPackages() {
|
||||
return Arrays.<ReactPackage>asList(
|
||||
new MainReactPackage(),
|
||||
new RNFirebasePackage(),
|
||||
new RNFirebaseCrashlyticsPackage(),
|
||||
new RNFirebaseAnalyticsPackage(),
|
||||
new RNFirebasePerformancePackage(),
|
||||
new RNCWebViewPackage(),
|
||||
new OrientationPackage(),
|
||||
new SplashScreenReactPackage(),
|
||||
new RNGestureHandlerPackage(),
|
||||
|
@ -63,11 +69,9 @@ public class MainApplication extends Application implements ReactApplication, IN
|
|||
new RNFetchBlobPackage(),
|
||||
new RealmReactPackage(),
|
||||
new ReactVideoPackage(),
|
||||
new RCTToastPackage(),
|
||||
new ReactNativeAudioPackage(),
|
||||
new KeyboardInputPackage(MainApplication.this),
|
||||
new RocketChatNativePackage(),
|
||||
new FabricPackage(),
|
||||
new FastImageViewPackage(),
|
||||
new RNI18nPackage(),
|
||||
new RNNotificationsPackage(MainApplication.this)
|
||||
|
@ -88,7 +92,6 @@ public class MainApplication extends Application implements ReactApplication, IN
|
|||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Fabric.with(this, new Crashlytics());
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "28.0.3"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 28
|
||||
targetSdkVersion = 28
|
||||
supportLibVersion = "28.0.0"
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.0'
|
||||
classpath 'com.google.gms:google-services:4.0.1'
|
||||
classpath 'com.android.tools.build:gradle:3.3.1'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'io.fabric.tools:gradle:1.25.4'
|
||||
classpath 'com.google.firebase:firebase-plugins:1.1.5'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -25,23 +36,19 @@ allprojects {
|
|||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
maven {
|
||||
// Local Maven repo containing AARs with JSC library built for Android
|
||||
url "$rootDir/../node_modules/jsc-android/dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects { subproject ->
|
||||
afterEvaluate {
|
||||
if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "28.0.3"
|
||||
defaultConfig {
|
||||
targetSdkVersion 28
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subprojects { subproject ->
|
||||
afterEvaluate {
|
||||
if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion "28.0.3"
|
||||
defaultConfig {
|
||||
targetSdkVersion 28
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
# distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
rootProject.name = 'RocketChatRN'
|
||||
include ':react-native-firebase'
|
||||
project(':react-native-firebase').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-firebase/android')
|
||||
include ':react-native-webview'
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
include ':react-native-orientation-locker'
|
||||
project(':react-native-orientation-locker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation-locker/android')
|
||||
include ':react-native-splash-screen'
|
||||
|
@ -11,8 +15,6 @@ include ':react-native-device-info'
|
|||
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
|
||||
include ':react-native-gesture-handler'
|
||||
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
|
||||
include ':react-native-toast'
|
||||
project(':react-native-toast').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-toast/android')
|
||||
include ':rn-fetch-blob'
|
||||
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
|
||||
include ':react-native-image-crop-picker'
|
||||
|
@ -21,8 +23,6 @@ include ':react-native-i18n'
|
|||
project(':react-native-i18n').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-i18n/android')
|
||||
include ':react-native-fast-image'
|
||||
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
|
||||
include ':react-native-fabric'
|
||||
project(':react-native-fabric').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fabric/android')
|
||||
include ':react-native-audio'
|
||||
project(':react-native-audio').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio/android')
|
||||
include ':reactnativekeyboardinput'
|
||||
|
|
|
@ -12,8 +12,7 @@ function createRequestTypes(base, types = defaultTypes) {
|
|||
export const LOGIN = createRequestTypes('LOGIN', [
|
||||
...defaultTypes,
|
||||
'SET_SERVICES',
|
||||
'SET_PREFERENCE',
|
||||
'SET_SORT_PREFERENCE'
|
||||
'SET_PREFERENCE'
|
||||
]);
|
||||
export const USER = createRequestTypes('USER', ['SET']);
|
||||
export const ROOMS = createRequestTypes('ROOMS', [
|
||||
|
@ -67,3 +66,4 @@ export const LOGOUT = 'LOGOUT'; // logout is always success
|
|||
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
|
||||
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
||||
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
||||
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';
|
||||
|
|
|
@ -40,13 +40,6 @@ export function setAllSettings(settings) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setCustomEmojis(emojis) {
|
||||
return {
|
||||
type: types.SET_CUSTOM_EMOJIS,
|
||||
payload: emojis
|
||||
};
|
||||
}
|
||||
|
||||
export function login() {
|
||||
return {
|
||||
type: 'LOGIN'
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function toggleMarkdown(value) {
|
||||
return {
|
||||
type: types.TOGGLE_MARKDOWN,
|
||||
payload: value
|
||||
};
|
||||
}
|
|
@ -12,6 +12,7 @@ export const COLOR_SEPARATOR = '#A7A7AA';
|
|||
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
|
||||
export const COLOR_BORDER = '#e1e5e8';
|
||||
export const COLOR_UNREAD = '#e1e5e8';
|
||||
export const COLOR_TOAST = '#0C0D0F';
|
||||
export const STATUS_COLORS = {
|
||||
online: '#2de0a5',
|
||||
busy: COLOR_DANGER,
|
||||
|
|
|
@ -58,5 +58,8 @@ export default {
|
|||
},
|
||||
Threads_enabled: {
|
||||
type: null
|
||||
},
|
||||
API_Gitlab_URL: {
|
||||
type: 'valueAsString'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, ViewPropTypes } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
const Avatar = React.memo(({
|
||||
|
@ -48,7 +48,7 @@ const Avatar = React.memo(({
|
|||
|
||||
Avatar.propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
style: ViewPropTypes.style,
|
||||
style: PropTypes.any,
|
||||
text: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import { ViewPropTypes, Image } from 'react-native';
|
||||
import { Image } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class CustomEmoji extends React.Component {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
emoji: PropTypes.object.isRequired,
|
||||
style: ViewPropTypes.style
|
||||
style: PropTypes.any
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import VideoPlayer from 'react-native-video-controls';
|
||||
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_WHITE } from '../constants/colors';
|
||||
import { formatAttachmentUrl } from '../lib/utils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1
|
||||
},
|
||||
modal: {
|
||||
margin: 0
|
||||
},
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
marginVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
description: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
indicator: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
|
||||
const Indicator = React.memo(() => (
|
||||
<ActivityIndicator style={styles.indicator} />
|
||||
));
|
||||
|
||||
const ModalContent = React.memo(({
|
||||
attachment, onClose, user, baseUrl
|
||||
}) => {
|
||||
if (attachment && attachment.image_url) {
|
||||
const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{attachment.title}</Text>
|
||||
{attachment.description ? <Text style={styles.description}>{attachment.description}</Text> : null}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<ImageViewer
|
||||
imageUrls={[{ url }]}
|
||||
onClick={onClose}
|
||||
backgroundColor='transparent'
|
||||
enableSwipeDown
|
||||
onSwipeDown={onClose}
|
||||
renderIndicator={() => null}
|
||||
renderImage={props => <FastImage {...props} />}
|
||||
loadingRender={() => <Indicator />}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
if (attachment && attachment.video_url) {
|
||||
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<VideoPlayer
|
||||
source={{ uri }}
|
||||
onBack={onClose}
|
||||
disableVolume
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const FileModal = React.memo(({
|
||||
isVisible, onClose, attachment, user, baseUrl
|
||||
}) => (
|
||||
<Modal
|
||||
style={styles.modal}
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
onSwipeComplete={onClose}
|
||||
swipeDirection={['up', 'left', 'right', 'down']}
|
||||
>
|
||||
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} />
|
||||
</Modal>
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
|
||||
|
||||
FileModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
attachment: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
FileModal.displayName = 'FileModal';
|
||||
|
||||
ModalContent.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
ModalContent.displayName = 'FileModalContent';
|
||||
|
||||
export default FileModal;
|
|
@ -20,9 +20,9 @@ export const CustomHeaderButtons = React.memo(props => (
|
|||
/>
|
||||
));
|
||||
|
||||
export const DrawerButton = React.memo(({ navigation, testID }) => (
|
||||
export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => (
|
||||
<CustomHeaderButtons left>
|
||||
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} />
|
||||
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
|
||||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
|
|
|
@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { Alert, Clipboard, Share } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
import * as moment from 'moment';
|
||||
|
||||
import moment from 'moment';
|
||||
import {
|
||||
actionsHide as actionsHideAction,
|
||||
deleteRequest as deleteRequestAction,
|
||||
|
@ -14,10 +13,10 @@ import {
|
|||
toggleReactionPicker as toggleReactionPickerAction,
|
||||
toggleStarRequest as toggleStarRequestAction
|
||||
} from '../actions/messages';
|
||||
import { showToast } from '../utils/info';
|
||||
import { vibrate } from '../utils/vibration';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import I18n from '../i18n';
|
||||
import log from '../utils/log';
|
||||
|
||||
@connect(
|
||||
state => ({
|
||||
|
@ -44,6 +43,7 @@ export default class MessageActions extends React.Component {
|
|||
actionsHide: PropTypes.func.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
actionMessage: PropTypes.object,
|
||||
toast: PropTypes.element,
|
||||
// user: PropTypes.object.isRequired,
|
||||
deleteRequest: PropTypes.func.isRequired,
|
||||
editInit: PropTypes.func.isRequired,
|
||||
|
@ -118,6 +118,10 @@ export default class MessageActions extends React.Component {
|
|||
this.REACTION_INDEX = this.options.length - 1;
|
||||
}
|
||||
|
||||
// Report
|
||||
this.options.push(I18n.t('Report'));
|
||||
this.REPORT_INDEX = this.options.length - 1;
|
||||
|
||||
// Delete
|
||||
if (this.allowDelete(props)) {
|
||||
this.options.push(I18n.t('Delete'));
|
||||
|
@ -151,7 +155,7 @@ export default class MessageActions extends React.Component {
|
|||
|
||||
getPermalink = async(message) => {
|
||||
try {
|
||||
return await RocketChat.getPermalink(message);
|
||||
return await RocketChat.getPermalinkMessage(message);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
@ -253,9 +257,9 @@ export default class MessageActions extends React.Component {
|
|||
}
|
||||
|
||||
handleCopy = async() => {
|
||||
const { actionMessage } = this.props;
|
||||
const { actionMessage, toast } = this.props;
|
||||
await Clipboard.setString(actionMessage.msg);
|
||||
showToast(I18n.t('Copied_to_clipboard'));
|
||||
toast.show(I18n.t('Copied_to_clipboard'));
|
||||
}
|
||||
|
||||
handleShare = async() => {
|
||||
|
@ -272,10 +276,10 @@ export default class MessageActions extends React.Component {
|
|||
}
|
||||
|
||||
handlePermalink = async() => {
|
||||
const { actionMessage } = this.props;
|
||||
const { actionMessage, toast } = this.props;
|
||||
const permalink = await this.getPermalink(actionMessage);
|
||||
Clipboard.setString(permalink);
|
||||
showToast(I18n.t('Permalink_copied_to_clipboard'));
|
||||
toast.show(I18n.t('Permalink_copied_to_clipboard'));
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
|
@ -298,6 +302,16 @@ export default class MessageActions extends React.Component {
|
|||
toggleReactionPicker(actionMessage);
|
||||
}
|
||||
|
||||
handleReport = async() => {
|
||||
const { actionMessage } = this.props;
|
||||
try {
|
||||
await RocketChat.reportMessage(actionMessage._id);
|
||||
Alert.alert(I18n.t('Message_Reported'));
|
||||
} catch (err) {
|
||||
log('err_report_message', err);
|
||||
}
|
||||
}
|
||||
|
||||
handleActionPress = (actionIndex) => {
|
||||
if (actionIndex) {
|
||||
switch (actionIndex) {
|
||||
|
@ -328,6 +342,9 @@ export default class MessageActions extends React.Component {
|
|||
case this.REACTION_INDEX:
|
||||
this.handleReaction();
|
||||
break;
|
||||
case this.REPORT_INDEX:
|
||||
this.handleReport();
|
||||
break;
|
||||
case this.DELETE_INDEX:
|
||||
this.handleDelete();
|
||||
break;
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
import { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
|
||||
export default class FilesActions extends PureComponent {
|
||||
static propTypes = {
|
||||
hideActions: PropTypes.func.isRequired,
|
||||
takePhoto: PropTypes.func.isRequired,
|
||||
chooseFromLibrary: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Cancel
|
||||
this.options = [I18n.t('Cancel')];
|
||||
this.CANCEL_INDEX = 0;
|
||||
|
||||
// Photo
|
||||
this.options.push(I18n.t('Take_a_photo'));
|
||||
this.PHOTO_INDEX = 1;
|
||||
|
||||
// Library
|
||||
this.options.push(I18n.t('Choose_from_library'));
|
||||
this.LIBRARY_INDEX = 2;
|
||||
|
||||
setTimeout(() => {
|
||||
this.showActionSheet();
|
||||
});
|
||||
}
|
||||
|
||||
showActionSheet = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: this.options,
|
||||
cancelButtonIndex: this.CANCEL_INDEX
|
||||
}, (actionIndex) => {
|
||||
this.handleActionPress(actionIndex);
|
||||
});
|
||||
}
|
||||
|
||||
handleActionPress = (actionIndex) => {
|
||||
const { takePhoto, chooseFromLibrary, hideActions } = this.props;
|
||||
switch (actionIndex) {
|
||||
case this.PHOTO_INDEX:
|
||||
takePhoto();
|
||||
break;
|
||||
case this.LIBRARY_INDEX:
|
||||
chooseFromLibrary();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
hideActions();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CancelEditingButton, ToggleEmojiButton } from './buttons';
|
||||
|
||||
const LeftButtons = React.memo(({
|
||||
showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji
|
||||
}) => {
|
||||
if (editing) {
|
||||
return <CancelEditingButton onPress={editCancel} />;
|
||||
}
|
||||
return (
|
||||
<ToggleEmojiButton
|
||||
show={showEmojiKeyboard}
|
||||
open={openEmoji}
|
||||
close={closeEmoji}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
LeftButtons.propTypes = {
|
||||
showEmojiKeyboard: PropTypes.bool,
|
||||
openEmoji: PropTypes.func.isRequired,
|
||||
closeEmoji: PropTypes.func.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
editCancel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LeftButtons;
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CancelEditingButton, FileButton } from './buttons';
|
||||
|
||||
const LeftButtons = React.memo(({
|
||||
showFileActions, editing, editCancel
|
||||
}) => {
|
||||
if (editing) {
|
||||
return <CancelEditingButton onPress={editCancel} />;
|
||||
}
|
||||
return <FileButton onPress={showFileActions} />;
|
||||
});
|
||||
|
||||
LeftButtons.propTypes = {
|
||||
showFileActions: PropTypes.func.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
editCancel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LeftButtons;
|
|
@ -5,6 +5,7 @@ import moment from 'moment';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import Markdown from '../message/Markdown';
|
||||
import { getCustomEmoji } from '../message/utils';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
|
@ -49,7 +50,6 @@ const styles = StyleSheet.create({
|
|||
|
||||
@connect(state => ({
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
customEmojis: state.customEmojis,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
||||
}))
|
||||
export default class ReplyPreview extends Component {
|
||||
|
@ -57,7 +57,6 @@ export default class ReplyPreview extends Component {
|
|||
message: PropTypes.object.isRequired,
|
||||
Message_TimeFormat: PropTypes.string.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired
|
||||
}
|
||||
|
@ -73,7 +72,7 @@ export default class ReplyPreview extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
message, Message_TimeFormat, customEmojis, baseUrl, username
|
||||
message, Message_TimeFormat, baseUrl, username
|
||||
} = this.props;
|
||||
const time = moment(message.ts).format(Message_TimeFormat);
|
||||
return (
|
||||
|
@ -83,7 +82,7 @@ export default class ReplyPreview extends Component {
|
|||
<Text style={styles.username}>{message.u.username}</Text>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
|
||||
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} />
|
||||
</View>
|
||||
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
||||
</View>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { SendButton, AudioButton, FileButton } from './buttons';
|
||||
|
||||
const RightButtons = React.memo(({
|
||||
showSend, submit, recordAudioMessage, showFileActions
|
||||
}) => {
|
||||
if (showSend) {
|
||||
return <SendButton onPress={submit} />;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AudioButton onPress={recordAudioMessage} />
|
||||
<FileButton onPress={showFileActions} />
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
RightButtons.propTypes = {
|
||||
showSend: PropTypes.bool,
|
||||
submit: PropTypes.func.isRequired,
|
||||
recordAudioMessage: PropTypes.func.isRequired,
|
||||
showFileActions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RightButtons;
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { SendButton, AudioButton } from './buttons';
|
||||
|
||||
const RightButtons = React.memo(({
|
||||
showSend, submit, recordAudioMessage
|
||||
}) => {
|
||||
if (showSend) {
|
||||
return <SendButton onPress={submit} />;
|
||||
}
|
||||
return <AudioButton onPress={recordAudioMessage} />;
|
||||
});
|
||||
|
||||
RightButtons.propTypes = {
|
||||
showSend: PropTypes.bool,
|
||||
submit: PropTypes.func.isRequired,
|
||||
recordAudioMessage: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RightButtons;
|
|
@ -179,6 +179,7 @@ export default class UploadModal extends Component {
|
|||
animationOut='fadeOut'
|
||||
useNativeDriver
|
||||
hideModalContentWhileAnimating
|
||||
avoidKeyboard
|
||||
>
|
||||
<View style={[styles.container, { width: width - 32 }]}>
|
||||
<View style={styles.titleContainer}>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const AudioButton = React.memo(({ onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-send-audio'
|
||||
accessibilityLabel='Send_audio_message'
|
||||
icon='mic'
|
||||
/>
|
||||
));
|
||||
|
||||
AudioButton.propTypes = {
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AudioButton;
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COLOR_PRIMARY } from '../../../constants/colors';
|
||||
import { CustomIcon } from '../../../lib/Icons';
|
||||
import styles from '../styles';
|
||||
import I18n from '../../../i18n';
|
||||
|
||||
const BaseButton = React.memo(({
|
||||
onPress, testID, accessibilityLabel, icon
|
||||
}) => (
|
||||
<BorderlessButton
|
||||
onPress={onPress}
|
||||
style={styles.actionButton}
|
||||
testID={testID}
|
||||
accessibilityLabel={I18n.t(accessibilityLabel)}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name={icon} size={23} color={COLOR_PRIMARY} />
|
||||
</BorderlessButton>
|
||||
));
|
||||
|
||||
BaseButton.propTypes = {
|
||||
onPress: PropTypes.func.isRequired,
|
||||
testID: PropTypes.string.isRequired,
|
||||
accessibilityLabel: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default BaseButton;
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const CancelEditingButton = React.memo(({ onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-cancel-editing'
|
||||
accessibilityLabel='Cancel_editing'
|
||||
icon='cross'
|
||||
/>
|
||||
));
|
||||
|
||||
CancelEditingButton.propTypes = {
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CancelEditingButton;
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const FileButton = React.memo(({ onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-actions'
|
||||
accessibilityLabel='Message_actions'
|
||||
icon='plus'
|
||||
/>
|
||||
));
|
||||
|
||||
FileButton.propTypes = {
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default FileButton;
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const SendButton = React.memo(({ onPress }) => (
|
||||
<BaseButton
|
||||
onPress={onPress}
|
||||
testID='messagebox-send-message'
|
||||
accessibilityLabel='Send_message'
|
||||
icon='send1'
|
||||
/>
|
||||
));
|
||||
|
||||
SendButton.propTypes = {
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SendButton;
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import BaseButton from './BaseButton';
|
||||
|
||||
const ToggleEmojiButton = React.memo(({ show, open, close }) => {
|
||||
if (show) {
|
||||
return (
|
||||
<BaseButton
|
||||
onPress={close}
|
||||
testID='messagebox-close-emoji'
|
||||
accessibilityLabel='Close_emoji_selector'
|
||||
icon='keyboard'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<BaseButton
|
||||
onPress={open}
|
||||
testID='messagebox-open-emoji'
|
||||
accessibilityLabel='Open_emoji_selector'
|
||||
icon='emoji'
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleEmojiButton.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
open: PropTypes.func.isRequired,
|
||||
close: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ToggleEmojiButton;
|
|
@ -0,0 +1,13 @@
|
|||
import CancelEditingButton from './CancelEditingButton';
|
||||
import ToggleEmojiButton from './ToggleEmojiButton';
|
||||
import SendButton from './SendButton';
|
||||
import AudioButton from './AudioButton';
|
||||
import FileButton from './FileButton';
|
||||
|
||||
export {
|
||||
CancelEditingButton,
|
||||
ToggleEmojiButton,
|
||||
SendButton,
|
||||
AudioButton,
|
||||
FileButton
|
||||
};
|
|
@ -7,8 +7,8 @@ import { connect } from 'react-redux';
|
|||
import { emojify } from 'react-emojione';
|
||||
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||
import ImagePicker from 'react-native-image-crop-picker';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import equal from 'deep-equal';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
|
||||
import { userTyping as userTypingAction } from '../../actions/room';
|
||||
import {
|
||||
|
@ -23,15 +23,15 @@ import Avatar from '../Avatar';
|
|||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import { emojis } from '../../emojis';
|
||||
import Recording from './Recording';
|
||||
import FilesActions from './FilesActions';
|
||||
import UploadModal from './UploadModal';
|
||||
import './EmojiKeyboard';
|
||||
import log from '../../utils/log';
|
||||
import I18n from '../../i18n';
|
||||
import ReplyPreview from './ReplyPreview';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { COLOR_PRIMARY, COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
|
||||
import { COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
|
||||
import LeftButtons from './LeftButtons';
|
||||
import RightButtons from './RightButtons';
|
||||
import { isAndroid } from '../../utils/deviceInfo';
|
||||
|
||||
const MENTIONS_TRACKING_TYPE_USERS = '@';
|
||||
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
|
||||
|
@ -48,6 +48,17 @@ const imagePickerConfig = {
|
|||
cropperCancelText: I18n.t('Cancel')
|
||||
};
|
||||
|
||||
const fileOptions = [I18n.t('Cancel')];
|
||||
const FILE_CANCEL_INDEX = 0;
|
||||
|
||||
// Photo
|
||||
fileOptions.push(I18n.t('Take_a_photo'));
|
||||
const FILE_PHOTO_INDEX = 1;
|
||||
|
||||
// Library
|
||||
fileOptions.push(I18n.t('Choose_from_library'));
|
||||
const FILE_LIBRARY_INDEX = 2;
|
||||
|
||||
class MessageBox extends Component {
|
||||
static propTypes = {
|
||||
rid: PropTypes.string.isRequired,
|
||||
|
@ -77,7 +88,6 @@ class MessageBox extends Component {
|
|||
this.state = {
|
||||
mentions: [],
|
||||
showEmojiKeyboard: false,
|
||||
showFilesAction: false,
|
||||
showSend: false,
|
||||
recording: false,
|
||||
trackingType: '',
|
||||
|
@ -111,6 +121,10 @@ class MessageBox extends Component {
|
|||
this.setInput(msg);
|
||||
this.setShowSend(true);
|
||||
}
|
||||
|
||||
if (isAndroid) {
|
||||
require('./EmojiKeyboard');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
@ -133,7 +147,7 @@ class MessageBox extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {
|
||||
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
|
||||
showEmojiKeyboard, showSend, recording, mentions, file
|
||||
} = this.state;
|
||||
const {
|
||||
roomType, replying, editing, isFocused
|
||||
|
@ -153,9 +167,6 @@ class MessageBox extends Component {
|
|||
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.showFilesAction !== showFilesAction) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.showSend !== showSend) {
|
||||
return true;
|
||||
}
|
||||
|
@ -171,32 +182,25 @@ class MessageBox extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
onChangeText = (text) => {
|
||||
onChangeText = debounce((text) => {
|
||||
const isTextEmpty = text.length === 0;
|
||||
this.setShowSend(!isTextEmpty);
|
||||
this.handleTyping(!isTextEmpty);
|
||||
this.debouncedOnChangeText(text);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
debouncedOnChangeText = debounce((text) => {
|
||||
this.setInput(text);
|
||||
|
||||
if (this.component) {
|
||||
requestAnimationFrame(() => {
|
||||
const { start, end } = this.component._lastNativeSelection;
|
||||
const cursor = Math.max(start, end);
|
||||
const lastNativeText = this.component._lastNativeText;
|
||||
const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
|
||||
const result = lastNativeText.substr(0, cursor).match(regexp);
|
||||
if (!result) {
|
||||
return this.stopTrackingMention();
|
||||
}
|
||||
const [, lastChar, name] = result;
|
||||
this.identifyMentionKeyword(name, lastChar);
|
||||
});
|
||||
if (!isTextEmpty) {
|
||||
const { start, end } = this.component._lastNativeSelection;
|
||||
const cursor = Math.max(start, end);
|
||||
const lastNativeText = this.component._lastNativeText;
|
||||
const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
|
||||
const result = lastNativeText.substr(0, cursor).match(regexp);
|
||||
if (!result) {
|
||||
return this.stopTrackingMention();
|
||||
}
|
||||
const [, lastChar, name] = result;
|
||||
this.identifyMentionKeyword(name, lastChar);
|
||||
}
|
||||
}, 100);
|
||||
}, 100)
|
||||
|
||||
onKeyboardResigned = () => {
|
||||
this.closeEmoji();
|
||||
|
@ -239,109 +243,9 @@ class MessageBox extends Component {
|
|||
this.setShowSend(true);
|
||||
}
|
||||
|
||||
get leftButtons() {
|
||||
const { showEmojiKeyboard } = this.state;
|
||||
const { editing } = this.props;
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<BorderlessButton
|
||||
onPress={this.editCancel}
|
||||
accessibilityLabel={I18n.t('Cancel_editing')}
|
||||
accessibilityTraits='button'
|
||||
style={styles.actionButton}
|
||||
testID='messagebox-cancel-editing'
|
||||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={COLOR_PRIMARY}
|
||||
name='cross'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
);
|
||||
}
|
||||
return !showEmojiKeyboard
|
||||
? (
|
||||
<BorderlessButton
|
||||
onPress={this.openEmoji}
|
||||
accessibilityLabel={I18n.t('Open_emoji_selector')}
|
||||
accessibilityTraits='button'
|
||||
style={styles.actionButton}
|
||||
testID='messagebox-open-emoji'
|
||||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={COLOR_PRIMARY}
|
||||
name='emoji'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
)
|
||||
: (
|
||||
<BorderlessButton
|
||||
onPress={this.closeEmoji}
|
||||
accessibilityLabel={I18n.t('Close_emoji_selector')}
|
||||
accessibilityTraits='button'
|
||||
style={styles.actionButton}
|
||||
testID='messagebox-close-emoji'
|
||||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
color={COLOR_PRIMARY}
|
||||
name='keyboard'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
);
|
||||
}
|
||||
|
||||
get rightButtons() {
|
||||
const { showSend } = this.state;
|
||||
const icons = [];
|
||||
|
||||
if (showSend) {
|
||||
icons.push(
|
||||
<BorderlessButton
|
||||
key='send-message'
|
||||
onPress={this.submit}
|
||||
style={styles.actionButton}
|
||||
testID='messagebox-send-message'
|
||||
accessibilityLabel={I18n.t('Send message')}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name='send1' size={23} color={COLOR_PRIMARY} />
|
||||
</BorderlessButton>
|
||||
);
|
||||
return icons;
|
||||
}
|
||||
icons.push(
|
||||
<BorderlessButton
|
||||
key='audio-message'
|
||||
onPress={this.recordAudioMessage}
|
||||
style={styles.actionButton}
|
||||
testID='messagebox-send-audio'
|
||||
accessibilityLabel={I18n.t('Send audio message')}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name='mic' size={23} color={COLOR_PRIMARY} />
|
||||
</BorderlessButton>
|
||||
);
|
||||
icons.push(
|
||||
<BorderlessButton
|
||||
key='file-message'
|
||||
onPress={this.toggleFilesActions}
|
||||
style={styles.actionButton}
|
||||
testID='messagebox-actions'
|
||||
accessibilityLabel={I18n.t('Message actions')}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name='plus' size={23} color={COLOR_PRIMARY} />
|
||||
</BorderlessButton>
|
||||
);
|
||||
return icons;
|
||||
}
|
||||
|
||||
getPermalink = async(message) => {
|
||||
try {
|
||||
return await RocketChat.getPermalink(message);
|
||||
return await RocketChat.getPermalinkMessage(message);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
@ -386,7 +290,7 @@ class MessageBox extends Component {
|
|||
try {
|
||||
database.create('users', user, true);
|
||||
} catch (e) {
|
||||
log('create users', e);
|
||||
log('err_create_users', e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -495,10 +399,6 @@ class MessageBox extends Component {
|
|||
this.setShowSend(false);
|
||||
}
|
||||
|
||||
toggleFilesActions = () => {
|
||||
this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction }));
|
||||
}
|
||||
|
||||
sendImageMessage = async(file) => {
|
||||
const { rid, tmid } = this.props;
|
||||
|
||||
|
@ -514,7 +414,7 @@ class MessageBox extends Component {
|
|||
try {
|
||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
|
||||
} catch (e) {
|
||||
log('sendImageMessage', e);
|
||||
log('err_send_image', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -523,7 +423,7 @@ class MessageBox extends Component {
|
|||
const image = await ImagePicker.openCamera(imagePickerConfig);
|
||||
this.showUploadModal(image);
|
||||
} catch (e) {
|
||||
log('takePhoto', e);
|
||||
log('err_take_photo', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -532,7 +432,7 @@ class MessageBox extends Component {
|
|||
const image = await ImagePicker.openPicker(imagePickerConfig);
|
||||
this.showUploadModal(image);
|
||||
} catch (e) {
|
||||
log('chooseFromLibrary', e);
|
||||
log('err_choose_from_library', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -540,6 +440,28 @@ class MessageBox extends Component {
|
|||
this.setState({ file: { ...file, isVisible: true } });
|
||||
}
|
||||
|
||||
showFileActions = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: fileOptions,
|
||||
cancelButtonIndex: FILE_CANCEL_INDEX
|
||||
}, (actionIndex) => {
|
||||
this.handleFileActionPress(actionIndex);
|
||||
});
|
||||
}
|
||||
|
||||
handleFileActionPress = (actionIndex) => {
|
||||
switch (actionIndex) {
|
||||
case FILE_PHOTO_INDEX:
|
||||
this.takePhoto();
|
||||
break;
|
||||
case FILE_LIBRARY_INDEX:
|
||||
this.chooseFromLibrary();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
editCancel = () => {
|
||||
const { editCancel } = this.props;
|
||||
editCancel();
|
||||
|
@ -570,7 +492,7 @@ class MessageBox extends Component {
|
|||
if (e && e.error === 'error-file-too-large') {
|
||||
return Alert.alert(I18n.t(e.error));
|
||||
}
|
||||
log('finishAudioMessage', e);
|
||||
log('err_finish_audio_message', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -585,6 +507,7 @@ class MessageBox extends Component {
|
|||
} = this.props;
|
||||
const message = this.text;
|
||||
|
||||
this.clearInput();
|
||||
this.closeEmoji();
|
||||
this.stopTrackingMention();
|
||||
this.handleTyping(false);
|
||||
|
@ -629,7 +552,6 @@ class MessageBox extends Component {
|
|||
} else {
|
||||
onSubmit(message);
|
||||
}
|
||||
this.clearInput();
|
||||
}
|
||||
|
||||
updateMentions = (keyword, type) => {
|
||||
|
@ -713,23 +635,27 @@ class MessageBox extends Component {
|
|||
testID={`mention-item-${ trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`}
|
||||
>
|
||||
{trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
|
||||
? [
|
||||
this.renderMentionEmoji(item),
|
||||
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
|
||||
]
|
||||
: [
|
||||
<Avatar
|
||||
key='mention-item-avatar'
|
||||
style={{ margin: 8 }}
|
||||
text={item.username || item.name}
|
||||
size={30}
|
||||
type={item.username ? 'd' : 'c'}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>,
|
||||
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
|
||||
]
|
||||
? (
|
||||
<React.Fragment>
|
||||
{this.renderMentionEmoji(item)}
|
||||
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
|
||||
</React.Fragment>
|
||||
)
|
||||
: (
|
||||
<React.Fragment>
|
||||
<Avatar
|
||||
key='mention-item-avatar'
|
||||
style={{ margin: 8 }}
|
||||
text={item.username || item.name}
|
||||
size={30}
|
||||
type={item.username ? 'd' : 'c'}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
@ -741,7 +667,7 @@ class MessageBox extends Component {
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<View key='messagebox-container' testID='messagebox-container'>
|
||||
<View testID='messagebox-container'>
|
||||
<FlatList
|
||||
style={styles.mentionList}
|
||||
data={mentions}
|
||||
|
@ -763,39 +689,30 @@ class MessageBox extends Component {
|
|||
return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} username={user.username} />;
|
||||
};
|
||||
|
||||
renderFilesActions = () => {
|
||||
const { showFilesAction } = this.state;
|
||||
|
||||
if (!showFilesAction) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FilesActions
|
||||
key='files-actions'
|
||||
hideActions={this.toggleFilesActions}
|
||||
takePhoto={this.takePhoto}
|
||||
chooseFromLibrary={this.chooseFromLibrary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
const { recording } = this.state;
|
||||
const { recording, showEmojiKeyboard, showSend } = this.state;
|
||||
const { editing } = this.props;
|
||||
|
||||
if (recording) {
|
||||
return (<Recording onFinish={this.finishAudioMessage} />);
|
||||
}
|
||||
return (
|
||||
[
|
||||
this.renderMentions(),
|
||||
<React.Fragment>
|
||||
{this.renderMentions()}
|
||||
<View style={styles.composer} key='messagebox'>
|
||||
{this.renderReplyPreview()}
|
||||
<View
|
||||
style={[styles.textArea, editing && styles.editing]}
|
||||
testID='messagebox'
|
||||
>
|
||||
{this.leftButtons}
|
||||
<LeftButtons
|
||||
showEmojiKeyboard={showEmojiKeyboard}
|
||||
editing={editing}
|
||||
showFileActions={this.showFileActions}
|
||||
editCancel={this.editCancel}
|
||||
openEmoji={this.openEmoji}
|
||||
closeEmoji={this.closeEmoji}
|
||||
/>
|
||||
<TextInput
|
||||
ref={component => this.component = component}
|
||||
style={styles.textBoxInput}
|
||||
|
@ -810,19 +727,23 @@ class MessageBox extends Component {
|
|||
placeholderTextColor={COLOR_TEXT_DESCRIPTION}
|
||||
testID='messagebox-input'
|
||||
/>
|
||||
{this.rightButtons}
|
||||
<RightButtons
|
||||
showSend={showSend}
|
||||
submit={this.submit}
|
||||
recordAudioMessage={this.recordAudioMessage}
|
||||
showFileActions={this.showFileActions}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
]
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { showEmojiKeyboard, file } = this.state;
|
||||
return (
|
||||
[
|
||||
<React.Fragment>
|
||||
<KeyboardAccessoryView
|
||||
key='input'
|
||||
renderContent={this.renderContent}
|
||||
kbInputRef={this.component}
|
||||
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
|
||||
|
@ -832,16 +753,14 @@ class MessageBox extends Component {
|
|||
// revealKeyboardInteractive
|
||||
requiresSameParentToManageScrollView
|
||||
addBottomView
|
||||
/>,
|
||||
this.renderFilesActions(),
|
||||
/>
|
||||
<UploadModal
|
||||
key='upload-modal'
|
||||
isVisible={(file && file.isVisible)}
|
||||
file={file}
|
||||
close={() => this.setState({ file: {} })}
|
||||
submit={this.sendImageMessage}
|
||||
/>
|
||||
]
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, FlatList, StyleSheet, SafeAreaView
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Emoji from './message/Emoji';
|
||||
import { getCustomEmoji } from './message/utils';
|
||||
import I18n from '../i18n';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_WHITE } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactCount: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 13,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
peopleReacted: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
peopleItemContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
emojiContainer: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
itemContainer: {
|
||||
height: 50,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 10,
|
||||
color: COLOR_WHITE
|
||||
}
|
||||
});
|
||||
const standardEmojiStyle = { fontSize: 20 };
|
||||
const customEmojiStyle = { width: 20, height: 20 };
|
||||
|
||||
const Item = React.memo(({ item, user, baseUrl }) => {
|
||||
const count = item.usernames.length;
|
||||
let usernames = item.usernames.slice(0, 3)
|
||||
.map(username => (username === user.username ? I18n.t('you') : username)).join(', ');
|
||||
if (count > 3) {
|
||||
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
|
||||
} else {
|
||||
usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
|
||||
}
|
||||
return (
|
||||
<View style={styles.itemContainer}>
|
||||
<View style={styles.emojiContainer}>
|
||||
<Emoji
|
||||
content={item.emoji}
|
||||
standardEmojiStyle={standardEmojiStyle}
|
||||
customEmojiStyle={customEmojiStyle}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.peopleItemContainer}>
|
||||
<Text style={styles.reactCount}>
|
||||
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
|
||||
</Text>
|
||||
<Text style={styles.peopleReacted}>{ usernames }</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const ModalContent = React.memo(({ message, onClose, ...props }) => {
|
||||
if (message && message.reactions) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<Touchable onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<CustomIcon
|
||||
style={styles.closeButton}
|
||||
name='cross'
|
||||
size={20}
|
||||
/>
|
||||
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
<FlatList
|
||||
style={styles.listContainer}
|
||||
data={message.reactions}
|
||||
renderItem={({ item }) => <Item item={item} {...props} />}
|
||||
keyExtractor={item => item.emoji}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const ReactionsModal = React.memo(({ isVisible, onClose, ...props }) => (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
backdropOpacity={0.8}
|
||||
onSwipeComplete={onClose}
|
||||
swipeDirection={['up', 'left', 'right', 'down']}
|
||||
>
|
||||
<ModalContent onClose={onClose} {...props} />
|
||||
</Modal>
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
|
||||
|
||||
ReactionsModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
ReactionsModal.displayName = 'ReactionsModal';
|
||||
|
||||
ModalContent.propTypes = {
|
||||
message: PropTypes.object,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
ModalContent.displayName = 'ReactionsModalContent';
|
||||
|
||||
Item.propTypes = {
|
||||
item: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
Item.displayName = 'ReactionsModalItem';
|
||||
|
||||
export default ReactionsModal;
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, ViewPropTypes } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { STATUS_COLORS } from '../../constants/colors';
|
||||
|
||||
const Status = React.memo(({ status, size, style }) => (
|
||||
|
@ -20,7 +20,7 @@ const Status = React.memo(({ status, size, style }) => (
|
|||
Status.propTypes = {
|
||||
status: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
style: ViewPropTypes.style
|
||||
style: PropTypes.any
|
||||
};
|
||||
Status.defaultProps = {
|
||||
status: 'offline',
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { ViewPropTypes } from 'react-native';
|
||||
|
||||
import Status from './Status';
|
||||
import database, { safeAddListener } from '../../lib/realm';
|
||||
|
@ -12,7 +11,7 @@ import database, { safeAddListener } from '../../lib/realm';
|
|||
export default class StatusContainer extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
style: ViewPropTypes.style,
|
||||
style: PropTypes.any,
|
||||
size: PropTypes.number,
|
||||
offline: PropTypes.bool
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, StyleSheet, Text, TextInput, ViewPropTypes
|
||||
View, StyleSheet, Text, TextInput
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
|
@ -73,7 +73,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
label: PropTypes.string,
|
||||
error: PropTypes.object,
|
||||
secureTextEntry: PropTypes.bool,
|
||||
containerStyle: ViewPropTypes.style,
|
||||
containerStyle: PropTypes.any,
|
||||
inputStyle: PropTypes.object,
|
||||
inputRef: PropTypes.func,
|
||||
testID: PropTypes.string,
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Image from './Image';
|
||||
import Audio from './Audio';
|
||||
import Video from './Video';
|
||||
import Reply from './Reply';
|
||||
|
||||
const Attachments = React.memo(({
|
||||
attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
}) => {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attachments.map((file, index) => {
|
||||
if (file.image_url) {
|
||||
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
}
|
||||
if (file.audio_url) {
|
||||
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
}
|
||||
if (file.video_url) {
|
||||
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
});
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments));
|
||||
|
||||
Attachments.propTypes = {
|
||||
attachments: PropTypes.array,
|
||||
timeFormat: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Attachments.displayName = 'MessageAttachments';
|
||||
|
||||
export default Attachments;
|
|
@ -56,20 +56,40 @@ const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
|
|||
const BUTTON_HIT_SLOP = {
|
||||
top: 12, right: 12, bottom: 12, left: 12
|
||||
};
|
||||
const sliderAnimationConfig = {
|
||||
duration: 250,
|
||||
easing: Easing.linear,
|
||||
delay: 0
|
||||
};
|
||||
|
||||
const Button = React.memo(({ paused, onPress }) => (
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={onPress}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
<CustomIcon name={paused ? 'play' : 'pause'} size={36} style={styles.playPauseImage} />
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
Button.propTypes = {
|
||||
paused: PropTypes.bool,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
Button.displayName = 'MessageAudioButton';
|
||||
|
||||
export default class Audio extends React.Component {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onLoad = this.onLoad.bind(this);
|
||||
this.onProgress = this.onProgress.bind(this);
|
||||
this.onEnd = this.onEnd.bind(this);
|
||||
const { baseUrl, file, user } = props;
|
||||
this.state = {
|
||||
currentTime: 0,
|
||||
|
@ -120,22 +140,26 @@ export default class Audio extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
getDuration = () => {
|
||||
get duration() {
|
||||
const { duration } = this.state;
|
||||
return formatTime(duration);
|
||||
}
|
||||
|
||||
setRef = ref => this.player = ref;
|
||||
|
||||
togglePlayPause = () => {
|
||||
const { paused } = this.state;
|
||||
this.setState({ paused: !paused });
|
||||
}
|
||||
|
||||
onValueChange = value => this.setState({ currentTime: value });
|
||||
|
||||
render() {
|
||||
const {
|
||||
uri, paused, currentTime, duration
|
||||
} = this.state;
|
||||
const {
|
||||
user, baseUrl, customEmojis, file
|
||||
user, baseUrl, file, getCustomEmoji, useMarkdown
|
||||
} = this.props;
|
||||
const { description } = file;
|
||||
|
||||
|
@ -144,12 +168,10 @@ export default class Audio extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
[
|
||||
<View key='audio' style={styles.audioContainer}>
|
||||
<React.Fragment>
|
||||
<View style={styles.audioContainer}>
|
||||
<Video
|
||||
ref={(ref) => {
|
||||
this.player = ref;
|
||||
}}
|
||||
ref={this.setRef}
|
||||
source={{ uri }}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
|
@ -157,39 +179,24 @@ export default class Audio extends React.Component {
|
|||
paused={paused}
|
||||
repeat={false}
|
||||
/>
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={this.togglePlayPause}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
{
|
||||
paused
|
||||
? <CustomIcon name='play' size={36} style={styles.playPauseImage} />
|
||||
: <CustomIcon name='pause' size={36} style={styles.playPauseImage} />
|
||||
}
|
||||
</Touchable>
|
||||
<Button paused={paused} onPress={this.togglePlayPause} />
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={currentTime}
|
||||
maximumValue={duration}
|
||||
minimumValue={0}
|
||||
animateTransitions
|
||||
animationConfig={{
|
||||
duration: 250,
|
||||
easing: Easing.linear,
|
||||
delay: 0
|
||||
}}
|
||||
animationConfig={sliderAnimationConfig}
|
||||
thumbTintColor={COLOR_PRIMARY}
|
||||
minimumTrackTintColor={COLOR_PRIMARY}
|
||||
onValueChange={value => this.setState({ currentTime: value })}
|
||||
onValueChange={this.onValueChange}
|
||||
thumbStyle={styles.thumbStyle}
|
||||
trackStyle={styles.trackStyle}
|
||||
/>
|
||||
<Text style={styles.duration}>{this.getDuration()}</Text>
|
||||
</View>,
|
||||
<Markdown key='description' msg={description} baseUrl={baseUrl} customEmojis={customEmojis} username={user.username} />
|
||||
]
|
||||
<Text style={styles.duration}>{this.duration}</Text>
|
||||
</View>
|
||||
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
const Broadcast = React.memo(({
|
||||
author, user, broadcast, replyBroadcast
|
||||
}) => {
|
||||
const isOwn = author._id === user.id;
|
||||
if (broadcast && !isOwn) {
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={replyBroadcast}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={styles.button}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, () => true);
|
||||
|
||||
Broadcast.propTypes = {
|
||||
author: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
broadcast: PropTypes.bool,
|
||||
replyBroadcast: PropTypes.func
|
||||
};
|
||||
Broadcast.displayName = 'MessageBroadcast';
|
||||
|
||||
export default Broadcast;
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import styles from './styles';
|
||||
import Markdown from './Markdown';
|
||||
import { getInfoMessage } from './utils';
|
||||
|
||||
const Content = React.memo((props) => {
|
||||
if (props.isInfo) {
|
||||
return <Text style={styles.textInfo}>{getInfoMessage({ ...props })}</Text>;
|
||||
}
|
||||
|
||||
if (props.tmid && !props.msg) {
|
||||
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
msg={props.msg}
|
||||
baseUrl={props.baseUrl}
|
||||
username={props.user.username}
|
||||
isEdited={props.isEdited}
|
||||
mentions={props.mentions}
|
||||
channels={props.channels}
|
||||
numberOfLines={props.tmid ? 1 : 0}
|
||||
getCustomEmoji={props.getCustomEmoji}
|
||||
useMarkdown={props.useMarkdown}
|
||||
/>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.msg === nextProps.msg);
|
||||
|
||||
Content.propTypes = {
|
||||
isInfo: PropTypes.bool,
|
||||
isEdited: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
tmid: PropTypes.string,
|
||||
msg: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Content.displayName = 'MessageContent';
|
||||
|
||||
export default Content;
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { DISCUSSION } from './constants';
|
||||
|
||||
const Discussion = React.memo(({
|
||||
msg, dcount, dlm, onDiscussionPress
|
||||
}) => {
|
||||
const time = formatLastMessage(dlm);
|
||||
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
||||
<Text style={styles.text}>{msg}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onDiscussionPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.msg !== nextProps.msg) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.dcount !== nextProps.dcount) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.dlm !== nextProps.dlm) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Discussion.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
dcount: PropTypes.number,
|
||||
dlm: PropTypes.string,
|
||||
onDiscussionPress: PropTypes.func
|
||||
};
|
||||
Discussion.displayName = 'MessageDiscussion';
|
||||
|
||||
export default Discussion;
|
|
@ -1,31 +1,28 @@
|
|||
import React from 'react';
|
||||
import { Text, ViewPropTypes } from 'react-native';
|
||||
import { Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { emojify } from 'react-emojione';
|
||||
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
|
||||
export default class Emoji extends React.PureComponent {
|
||||
static propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
standardEmojiStyle: Text.propTypes.style,
|
||||
customEmojiStyle: ViewPropTypes.style,
|
||||
customEmojis: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
])
|
||||
const Emoji = React.memo(({
|
||||
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
|
||||
}) => {
|
||||
const parsedContent = content.replace(/^:|:$/g, '');
|
||||
const emoji = getCustomEmoji(parsedContent);
|
||||
if (emoji) {
|
||||
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
||||
}
|
||||
return <Text style={standardEmojiStyle}>{ emojify(content, { output: 'unicode' }) }</Text>;
|
||||
}, () => true);
|
||||
|
||||
render() {
|
||||
const {
|
||||
content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const parsedContent = content.replace(/^:|:$/g, '');
|
||||
const emojiExtension = customEmojis[parsedContent];
|
||||
if (emojiExtension) {
|
||||
const emoji = { extension: emojiExtension, content: parsedContent };
|
||||
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
||||
}
|
||||
return <Text style={standardEmojiStyle}>{ emojify(`${ content }`, { output: 'unicode' }) }</Text>;
|
||||
}
|
||||
}
|
||||
Emoji.propTypes = {
|
||||
content: PropTypes.string,
|
||||
standardEmojiStyle: PropTypes.object,
|
||||
customEmojiStyle: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Emoji.displayName = 'MessageEmoji';
|
||||
|
||||
export default Emoji;
|
||||
|
|
|
@ -1,95 +1,79 @@
|
|||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import PhotoModal from './PhotoModal';
|
||||
import Markdown from './Markdown';
|
||||
import styles from './styles';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
|
||||
export default class extends Component {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
customEmojis: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
])
|
||||
const Button = React.memo(({ children, onPress }) => (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
{children}
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
const Image = React.memo(({ img }) => (
|
||||
<FastImage
|
||||
style={styles.image}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
));
|
||||
|
||||
const ImageContainer = React.memo(({
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
}) => {
|
||||
const img = formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||
if (!img) {
|
||||
return null;
|
||||
}
|
||||
|
||||
state = { modalVisible: false, isPressed: false };
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { modalVisible, isPressed } = this.state;
|
||||
const { file } = this.props;
|
||||
if (nextState.modalVisible !== modalVisible) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.isPressed !== isPressed) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextProps.file, file)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onPressButton = () => {
|
||||
this.setState({
|
||||
modalVisible: true
|
||||
});
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
const {
|
||||
file, customEmojis, baseUrl, user
|
||||
} = this.props;
|
||||
if (file.description) {
|
||||
return <Markdown msg={file.description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />;
|
||||
}
|
||||
}
|
||||
|
||||
isPressed = (state) => {
|
||||
this.setState({ isPressed: state });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { modalVisible, isPressed } = this.state;
|
||||
const { baseUrl, file, user } = this.props;
|
||||
const img = file.image_url.includes('http') ? file.image_url : `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
|
||||
if (!img) {
|
||||
return null;
|
||||
}
|
||||
const onPress = () => onOpenFileModal(file);
|
||||
|
||||
if (file.description) {
|
||||
return (
|
||||
[
|
||||
<Touchable
|
||||
key='image'
|
||||
onPress={this.onPressButton}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<React.Fragment>
|
||||
<FastImage
|
||||
style={[styles.image, isPressed && { opacity: 0.5 }]}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{this.getDescription()}
|
||||
</React.Fragment>
|
||||
</Touchable>,
|
||||
<PhotoModal
|
||||
key='modal'
|
||||
title={file.title}
|
||||
description={file.description}
|
||||
image={img}
|
||||
isVisible={modalVisible}
|
||||
onClose={() => this.setState({ modalVisible: false })}
|
||||
/>
|
||||
]
|
||||
<Button onPress={onPress}>
|
||||
<View>
|
||||
<Image img={img} />
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onPress={onPress}>
|
||||
<Image img={img} />
|
||||
</Button>
|
||||
);
|
||||
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file));
|
||||
|
||||
ImageContainer.propTypes = {
|
||||
file: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
ImageContainer.displayName = 'MessageImageContainer';
|
||||
|
||||
Image.propTypes = {
|
||||
img: PropTypes.string
|
||||
};
|
||||
ImageContainer.displayName = 'MessageImage';
|
||||
|
||||
Button.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
ImageContainer.displayName = 'MessageButton';
|
||||
|
||||
export default ImageContainer;
|
||||
|
|
|
@ -4,9 +4,15 @@ import PropTypes from 'prop-types';
|
|||
import { emojify } from 'react-emojione';
|
||||
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
|
||||
import MarkdownFlowdock from 'markdown-it-flowdock';
|
||||
|
||||
import styles from './styles';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
const EmojiPlugin = new PluginContainer(MarkdownEmojiPlugin);
|
||||
const MentionsPlugin = new PluginContainer(MarkdownFlowdock);
|
||||
const plugins = [EmojiPlugin, MentionsPlugin];
|
||||
|
||||
// Support <http://link|Text>
|
||||
const formatText = text => text.replace(
|
||||
|
@ -15,7 +21,7 @@ const formatText = text => text.replace(
|
|||
);
|
||||
|
||||
const Markdown = React.memo(({
|
||||
msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
|
||||
msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true
|
||||
}) => {
|
||||
if (!msg) {
|
||||
return null;
|
||||
|
@ -28,14 +34,18 @@ const Markdown = React.memo(({
|
|||
if (numberOfLines > 0) {
|
||||
m = m.replace(/[\n]+/g, '\n').trim();
|
||||
}
|
||||
|
||||
if (!useMarkdown) {
|
||||
return <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
rules={{
|
||||
paragraph: (node, children) => (
|
||||
// eslint-disable-next-line
|
||||
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
|
||||
{isEdited ? <Text style={styles.edited}> ({I18n.t('edited')})</Text> : null}
|
||||
</Text>
|
||||
),
|
||||
mention: (node) => {
|
||||
|
@ -52,23 +62,31 @@ const Markdown = React.memo(({
|
|||
...styles.mentionLoggedUser
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Text style={mentionStyle} key={key}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
|
||||
return (
|
||||
<Text style={mentionStyle} key={key}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return `@${ content }`;
|
||||
},
|
||||
hashtag: (node) => {
|
||||
const { content, key } = node;
|
||||
if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) {
|
||||
return (
|
||||
<Text key={key} style={styles.mention}>
|
||||
#{content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return `#${ content }`;
|
||||
},
|
||||
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 };
|
||||
const emoji = getCustomEmoji && getCustomEmoji(content);
|
||||
if (emoji) {
|
||||
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
|
||||
}
|
||||
return <Text key={node.key}>:{content}:</Text>;
|
||||
|
@ -90,10 +108,7 @@ const Markdown = React.memo(({
|
|||
link: styles.link,
|
||||
...style
|
||||
}}
|
||||
plugins={[
|
||||
new PluginContainer(MarkdownFlowdock),
|
||||
new PluginContainer(MarkdownEmojiPlugin)
|
||||
]}
|
||||
plugins={plugins}
|
||||
>{m}
|
||||
</MarkdownRenderer>
|
||||
);
|
||||
|
@ -101,13 +116,17 @@ const Markdown = React.memo(({
|
|||
|
||||
Markdown.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
username: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
style: PropTypes.any,
|
||||
rules: PropTypes.object,
|
||||
edited: PropTypes.bool,
|
||||
numberOfLines: PropTypes.number
|
||||
isEdited: PropTypes.bool,
|
||||
numberOfLines: PropTypes.number,
|
||||
useMarkdown: PropTypes.bool,
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Markdown.displayName = 'MessageMarkdown';
|
||||
|
||||
export default Markdown;
|
||||
|
|
|
@ -1,609 +1,129 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, Text, ViewPropTypes, TouchableWithoutFeedback
|
||||
} from 'react-native';
|
||||
import moment from 'moment';
|
||||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||
import { View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { emojify } from 'react-emojione';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
|
||||
import Image from './Image';
|
||||
import User from './User';
|
||||
import Avatar from '../Avatar';
|
||||
import Audio from './Audio';
|
||||
import Video from './Video';
|
||||
import Markdown from './Markdown';
|
||||
import Url from './Url';
|
||||
import Reply from './Reply';
|
||||
import ReactionsModal from './ReactionsModal';
|
||||
import Emoji from './Emoji';
|
||||
import MessageError from './MessageError';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import messagesStatus from '../../constants/messagesStatus';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_DANGER } from '../../constants/colors';
|
||||
import debounce from '../../utils/debounce';
|
||||
import DisclosureIndicator from '../DisclosureIndicator';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import RepliedThread from './RepliedThread';
|
||||
import MessageAvatar from './MessageAvatar';
|
||||
import Attachments from './Attachments';
|
||||
import Urls from './Urls';
|
||||
import Thread from './Thread';
|
||||
import Reactions from './Reactions';
|
||||
import Broadcast from './Broadcast';
|
||||
import Discussion from './Discussion';
|
||||
import Content from './Content';
|
||||
|
||||
const SYSTEM_MESSAGES = [
|
||||
'r',
|
||||
'au',
|
||||
'ru',
|
||||
'ul',
|
||||
'uj',
|
||||
'ut',
|
||||
'rm',
|
||||
'user-muted',
|
||||
'user-unmuted',
|
||||
'message_pinned',
|
||||
'subscription-role-added',
|
||||
'subscription-role-removed',
|
||||
'room_changed_description',
|
||||
'room_changed_announcement',
|
||||
'room_changed_topic',
|
||||
'room_changed_privacy',
|
||||
'message_snippeted',
|
||||
'thread-created'
|
||||
];
|
||||
|
||||
const getInfoMessage = ({
|
||||
type, role, msg, author
|
||||
}) => {
|
||||
const { username } = author;
|
||||
if (type === 'rm') {
|
||||
return I18n.t('Message_removed');
|
||||
} else if (type === 'uj') {
|
||||
return I18n.t('Has_joined_the_channel');
|
||||
} else if (type === 'ut') {
|
||||
return I18n.t('Has_joined_the_conversation');
|
||||
} else if (type === 'r') {
|
||||
return I18n.t('Room_name_changed', { name: msg, userBy: username });
|
||||
} else if (type === 'message_pinned') {
|
||||
return I18n.t('Message_pinned');
|
||||
} else if (type === 'ul') {
|
||||
return I18n.t('Has_left_the_channel');
|
||||
} else if (type === 'ru') {
|
||||
return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
|
||||
} else if (type === 'au') {
|
||||
return I18n.t('User_added_by', { userAdded: msg, userBy: username });
|
||||
} else if (type === 'user-muted') {
|
||||
return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
|
||||
} else if (type === 'user-unmuted') {
|
||||
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
|
||||
} else if (type === 'subscription-role-added') {
|
||||
return `${ msg } was set ${ role } by ${ username }`;
|
||||
} else if (type === 'subscription-role-removed') {
|
||||
return `${ msg } is no longer ${ role } by ${ username }`;
|
||||
} else if (type === 'room_changed_description') {
|
||||
return I18n.t('Room_changed_description', { description: msg, userBy: username });
|
||||
} else if (type === 'room_changed_announcement') {
|
||||
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
|
||||
} else if (type === 'room_changed_topic') {
|
||||
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
|
||||
} else if (type === 'room_changed_privacy') {
|
||||
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
|
||||
} else if (type === 'message_snippeted') {
|
||||
return I18n.t('Created_snippet');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const BUTTON_HIT_SLOP = {
|
||||
top: 4, right: 4, bottom: 4, left: 4
|
||||
};
|
||||
|
||||
export default class Message extends PureComponent {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
msg: PropTypes.string,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired
|
||||
}),
|
||||
author: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
name: PropTypes.string
|
||||
}),
|
||||
status: PropTypes.any,
|
||||
reactions: PropTypes.any,
|
||||
editing: PropTypes.bool,
|
||||
style: ViewPropTypes.style,
|
||||
archived: PropTypes.bool,
|
||||
broadcast: PropTypes.bool,
|
||||
reactionsModal: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
header: PropTypes.bool,
|
||||
isThreadReply: PropTypes.bool,
|
||||
isThreadSequential: PropTypes.bool,
|
||||
avatar: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Date),
|
||||
PropTypes.string
|
||||
]),
|
||||
edited: PropTypes.bool,
|
||||
attachments: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
]),
|
||||
urls: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
]),
|
||||
useRealName: PropTypes.bool,
|
||||
dcount: PropTypes.number,
|
||||
dlm: PropTypes.instanceOf(Date),
|
||||
tmid: PropTypes.string,
|
||||
tcount: PropTypes.number,
|
||||
tlm: PropTypes.instanceOf(Date),
|
||||
tmsg: PropTypes.string,
|
||||
// methods
|
||||
closeReactions: PropTypes.func,
|
||||
onErrorPress: PropTypes.func,
|
||||
onLongPress: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
onReactionPress: PropTypes.func,
|
||||
onDiscussionPress: PropTypes.func,
|
||||
onThreadPress: PropTypes.func,
|
||||
replyBroadcast: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
fetchThreadName: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
archived: false,
|
||||
broadcast: false,
|
||||
attachments: [],
|
||||
urls: [],
|
||||
reactions: [],
|
||||
onLongPress: () => {}
|
||||
}
|
||||
|
||||
onPress = debounce(() => {
|
||||
KeyboardUtils.dismiss();
|
||||
|
||||
const { onThreadPress, tlm, tmid } = this.props;
|
||||
if ((tlm || tmid) && onThreadPress) {
|
||||
onThreadPress();
|
||||
}
|
||||
}, 300, true)
|
||||
|
||||
onLongPress = () => {
|
||||
const { archived, onLongPress } = this.props;
|
||||
if (this.isInfoMessage() || this.hasError() || archived) {
|
||||
return;
|
||||
}
|
||||
onLongPress();
|
||||
}
|
||||
|
||||
formatLastMessage = (lm) => {
|
||||
const { customThreadTimeFormat } = this.props;
|
||||
if (customThreadTimeFormat) {
|
||||
return moment(lm).format(customThreadTimeFormat);
|
||||
}
|
||||
return lm ? moment(lm).calendar(null, {
|
||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'h:mm A',
|
||||
lastWeek: 'dddd',
|
||||
sameElse: 'MMM D'
|
||||
}) : null;
|
||||
}
|
||||
|
||||
formatMessageCount = (count, type) => {
|
||||
const discussion = type === 'discussion';
|
||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||
if (count === 1) {
|
||||
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
|
||||
} else if (count > 1 && count < 1000) {
|
||||
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
} else if (count > 999) {
|
||||
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
isInfoMessage = () => {
|
||||
const { type } = this.props;
|
||||
return SYSTEM_MESSAGES.includes(type);
|
||||
}
|
||||
|
||||
isOwn = () => {
|
||||
const { author, user } = this.props;
|
||||
return author._id === user.id;
|
||||
}
|
||||
|
||||
isDeleted() {
|
||||
const { type } = this.props;
|
||||
return type === 'rm';
|
||||
}
|
||||
|
||||
isTemp() {
|
||||
const { status } = this.props;
|
||||
return status === messagesStatus.TEMP || status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
hasError() {
|
||||
const { status } = this.props;
|
||||
return status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
renderAvatar = (small = false) => {
|
||||
const {
|
||||
header, avatar, author, baseUrl, user
|
||||
} = this.props;
|
||||
if (header) {
|
||||
return (
|
||||
<Avatar
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
avatar={avatar}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderUsername = () => {
|
||||
const {
|
||||
header, timeFormat, author, alias, ts, useRealName
|
||||
} = this.props;
|
||||
if (header) {
|
||||
return (
|
||||
<User
|
||||
onPress={this.onPress}
|
||||
timeFormat={timeFormat}
|
||||
username={(useRealName && author.name) || author.username}
|
||||
alias={alias}
|
||||
ts={ts}
|
||||
temp={this.isTemp()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.isInfoMessage()) {
|
||||
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
|
||||
}
|
||||
|
||||
const {
|
||||
customEmojis, msg, baseUrl, user, edited, tmid
|
||||
} = this.props;
|
||||
|
||||
if (tmid && !msg) {
|
||||
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
msg={msg}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
edited={edited}
|
||||
numberOfLines={tmid ? 1 : 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderAttachment() {
|
||||
const { attachments, timeFormat } = this.props;
|
||||
|
||||
if (attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attachments.map((file, index) => {
|
||||
const { user, baseUrl, customEmojis } = this.props;
|
||||
if (file.image_url) {
|
||||
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
}
|
||||
if (file.audio_url) {
|
||||
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
}
|
||||
if (file.video_url) {
|
||||
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
});
|
||||
}
|
||||
|
||||
renderUrl = () => {
|
||||
const { urls, user, baseUrl } = this.props;
|
||||
if (urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urls.map((url, index) => (
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
|
||||
));
|
||||
}
|
||||
|
||||
renderError = () => {
|
||||
if (!this.hasError()) {
|
||||
return null;
|
||||
}
|
||||
const { onErrorPress } = this.props;
|
||||
return (
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton}>
|
||||
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
renderReaction = (reaction) => {
|
||||
const {
|
||||
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
|
||||
return (
|
||||
<Touchable
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
onLongPress={onReactionLongPress}
|
||||
key={reaction.emoji}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
customEmojis={customEmojis}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
renderReactions() {
|
||||
const { reactions, toggleReactionPicker } = this.props;
|
||||
if (reactions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.reactionsContainer}>
|
||||
{reactions.map(this.renderReaction)}
|
||||
<Touchable
|
||||
onPress={toggleReactionPicker}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={styles.reactionButton}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={styles.reactionContainer}>
|
||||
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
|
||||
</View>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderBroadcastReply() {
|
||||
const { broadcast, replyBroadcast } = this.props;
|
||||
if (broadcast && !this.isOwn()) {
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={replyBroadcast}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={styles.button}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderDiscussion = () => {
|
||||
const {
|
||||
msg, dcount, dlm, onDiscussionPress
|
||||
} = this.props;
|
||||
const time = this.formatLastMessage(dlm);
|
||||
const buttonText = this.formatMessageCount(dcount, 'discussion');
|
||||
const MessageInner = React.memo((props) => {
|
||||
if (props.type === 'discussion-created') {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
||||
<Text style={styles.text}>{msg}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onDiscussionPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
<User {...props} />
|
||||
<Discussion {...props} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<User {...props} />
|
||||
<Content {...props} />
|
||||
<Attachments {...props} />
|
||||
<Urls {...props} />
|
||||
<Thread {...props} />
|
||||
<Reactions {...props} />
|
||||
<Broadcast {...props} />
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
MessageInner.displayName = 'MessageInner';
|
||||
|
||||
renderThread = () => {
|
||||
const {
|
||||
tcount, tlm, onThreadPress, msg
|
||||
} = this.props;
|
||||
|
||||
if (!tlm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const time = this.formatLastMessage(tlm);
|
||||
const buttonText = this.formatMessageCount(tcount, 'thread');
|
||||
const Message = React.memo((props) => {
|
||||
if (props.isThreadReply || props.isThreadSequential || props.isInfo) {
|
||||
const thread = props.isThreadReply ? <RepliedThread isTemp={props.isTemp} {...props} /> : null;
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onThreadPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
testID={`message-thread-button-${ msg }`}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderRepliedThread = () => {
|
||||
const {
|
||||
tmid, tmsg, header, fetchThreadName
|
||||
} = this.props;
|
||||
if (!tmid || !header || this.isTemp()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tmsg) {
|
||||
fetchThreadName(tmid);
|
||||
return null;
|
||||
}
|
||||
|
||||
let msg = emojify(tmsg, { output: 'unicode' });
|
||||
msg = removeMarkdown(msg);
|
||||
|
||||
return (
|
||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} />
|
||||
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
|
||||
<DisclosureIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderInner = () => {
|
||||
const { type } = this.props;
|
||||
if (type === 'discussion-created') {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderDiscussion()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderContent()}
|
||||
{this.renderAttachment()}
|
||||
{this.renderUrl()}
|
||||
{this.renderThread()}
|
||||
{this.renderReactions()}
|
||||
{this.renderBroadcastReply()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessage = () => {
|
||||
const { header, isThreadReply, isThreadSequential } = this.props;
|
||||
|
||||
if (isThreadReply || isThreadSequential || this.isInfoMessage()) {
|
||||
const thread = isThreadReply ? this.renderRepliedThread() : null;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{thread}
|
||||
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
|
||||
{this.renderAvatar(true)}
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
header && styles.messageContentWithHeader,
|
||||
this.hasError() && header && styles.messageContentWithHeader,
|
||||
this.hasError() && !header && styles.messageContentWithError,
|
||||
this.isTemp() && styles.temp
|
||||
]}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</View>
|
||||
<View style={[styles.container, props.style, props.isTemp && styles.temp]}>
|
||||
{thread}
|
||||
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
|
||||
<MessageAvatar small {...props} />
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
props.isHeader && styles.messageContentWithHeader
|
||||
]}
|
||||
>
|
||||
<Content {...props} />
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={[styles.container, props.style, props.isTemp && styles.temp]}>
|
||||
<View style={styles.flex}>
|
||||
{this.renderAvatar()}
|
||||
<MessageAvatar {...props} />
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
header && styles.messageContentWithHeader,
|
||||
this.hasError() && header && styles.messageContentWithHeader,
|
||||
this.hasError() && !header && styles.messageContentWithError,
|
||||
this.isTemp() && styles.temp
|
||||
props.isHeader && styles.messageContentWithHeader
|
||||
]}
|
||||
>
|
||||
{this.renderInner()}
|
||||
<MessageInner {...props} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
editing, style, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const accessibilityLabel = I18n.t('Message_accessibility', { user: author.username, time: moment(ts).format(timeFormat), message: msg });
|
||||
</View>
|
||||
);
|
||||
});
|
||||
Message.displayName = 'Message';
|
||||
|
||||
const MessageTouchable = React.memo((props) => {
|
||||
if (props.hasError) {
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
{this.renderError()}
|
||||
<TouchableWithoutFeedback
|
||||
onLongPress={this.onLongPress}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View
|
||||
style={[styles.container, editing && styles.editing, style]}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
{this.renderMessage()}
|
||||
{reactionsModal
|
||||
? (
|
||||
<ReactionsModal
|
||||
isVisible={reactionsModal}
|
||||
reactions={reactions}
|
||||
user={user}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
close={closeReactions}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<MessageError {...props} />
|
||||
<Message {...props} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Touchable
|
||||
onLongPress={props.onLongPress}
|
||||
onPress={props.onPress}
|
||||
disabled={props.isInfo || props.archived || props.isTemp}
|
||||
>
|
||||
<View>
|
||||
<Message {...props} />
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
});
|
||||
MessageTouchable.displayName = 'MessageTouchable';
|
||||
|
||||
MessageTouchable.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
archived: PropTypes.bool,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
Message.propTypes = {
|
||||
isThreadReply: PropTypes.bool,
|
||||
isThreadSequential: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
isHeader: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
style: PropTypes.any,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
MessageInner.propTypes = {
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
export default MessageTouchable;
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
import styles from './styles';
|
||||
|
||||
const MessageAvatar = React.memo(({
|
||||
isHeader, avatar, author, baseUrl, user, small
|
||||
}) => {
|
||||
if (isHeader) {
|
||||
return (
|
||||
<Avatar
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
avatar={avatar}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, (prevProps, nextProps) => prevProps.isHeader === nextProps.isHeader);
|
||||
|
||||
MessageAvatar.propTypes = {
|
||||
isHeader: PropTypes.bool,
|
||||
avatar: PropTypes.string,
|
||||
author: PropTypes.obj,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.obj,
|
||||
small: PropTypes.bool
|
||||
};
|
||||
MessageAvatar.displayName = 'MessageAvatar';
|
||||
|
||||
export default MessageAvatar;
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_DANGER } from '../../constants/colors';
|
||||
import styles from './styles';
|
||||
|
||||
const MessageError = React.memo(({ hasError, onErrorPress }) => {
|
||||
if (!hasError) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton}>
|
||||
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
|
||||
</Touchable>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError);
|
||||
|
||||
MessageError.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
onErrorPress: PropTypes.func
|
||||
};
|
||||
MessageError.displayName = 'MessageError';
|
||||
|
||||
export default MessageError;
|
|
@ -1,94 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import { responsive } from 'react-native-responsive-ui';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imageWrapper: {
|
||||
flex: 1
|
||||
},
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
marginVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
description: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
indicatorContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
const margin = 40;
|
||||
|
||||
@responsive
|
||||
export default class PhotoModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
image: PropTypes.string.isRequired,
|
||||
isVisible: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
window: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
image, isVisible, onClose, title, description, window: { width, height }
|
||||
} = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
style={{ alignItems: 'center' }}
|
||||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
animationIn='fadeIn'
|
||||
animationOut='fadeOut'
|
||||
>
|
||||
<View style={{ width: width - margin, height: height - margin }}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<View style={styles.imageWrapper}>
|
||||
<ImageViewer
|
||||
imageUrls={[{ url: encodeURI(image) }]}
|
||||
onClick={onClose}
|
||||
backgroundColor='transparent'
|
||||
enableSwipeDown
|
||||
onSwipeDown={onClose}
|
||||
renderIndicator={() => {}}
|
||||
renderImage={props => <FastImage {...props} />}
|
||||
loadingRender={() => (
|
||||
<View style={[styles.indicatorContainer, { width, height }]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
import Emoji from './Emoji';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
|
||||
const AddReaction = React.memo(({ toggleReactionPicker }) => (
|
||||
<Touchable
|
||||
onPress={toggleReactionPicker}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={styles.reactionButton}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={styles.reactionContainer}>
|
||||
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
|
||||
</View>
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
const Reaction = React.memo(({
|
||||
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji
|
||||
}) => {
|
||||
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
|
||||
return (
|
||||
<Touchable
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
onLongPress={onReactionLongPress}
|
||||
key={reaction.emoji}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
});
|
||||
|
||||
const Reactions = React.memo(({
|
||||
reactions, user, baseUrl, onReactionPress, toggleReactionPicker, onReactionLongPress, getCustomEmoji
|
||||
}) => {
|
||||
if (!reactions || reactions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.reactionsContainer}>
|
||||
{reactions.map(reaction => (
|
||||
<Reaction
|
||||
key={reaction.emoji}
|
||||
reaction={reaction}
|
||||
user={user}
|
||||
baseUrl={baseUrl}
|
||||
onReactionLongPress={onReactionLongPress}
|
||||
onReactionPress={onReactionPress}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
))}
|
||||
<AddReaction toggleReactionPicker={toggleReactionPicker} />
|
||||
</View>
|
||||
);
|
||||
});
|
||||
// FIXME: can't compare because it's a Realm object (it may be fixed by JSON.parse(JSON.stringify(reactions)))
|
||||
|
||||
Reaction.propTypes = {
|
||||
reaction: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onReactionPress: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Reaction.displayName = 'MessageReaction';
|
||||
|
||||
Reactions.propTypes = {
|
||||
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onReactionPress: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Reactions.displayName = 'MessageReactions';
|
||||
|
||||
AddReaction.propTypes = {
|
||||
toggleReactionPicker: PropTypes.func
|
||||
};
|
||||
AddReaction.displayName = 'MessageAddReaction';
|
||||
|
||||
export default Reactions;
|
|
@ -1,140 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, FlatList, StyleSheet
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
|
||||
import Emoji from './Emoji';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactCount: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 13,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
peopleReacted: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
peopleItemContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
emojiContainer: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
itemContainer: {
|
||||
height: 50,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 10,
|
||||
color: COLOR_WHITE
|
||||
}
|
||||
});
|
||||
const standardEmojiStyle = { fontSize: 20 };
|
||||
const customEmojiStyle = { width: 20, height: 20 };
|
||||
|
||||
export default class ReactionsModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
reactions: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
])
|
||||
}
|
||||
|
||||
renderItem = (item) => {
|
||||
const { user, customEmojis, baseUrl } = this.props;
|
||||
const count = item.usernames.length;
|
||||
let usernames = item.usernames.slice(0, 3)
|
||||
.map(username => (username.value === user.username ? I18n.t('you') : username.value)).join(', ');
|
||||
if (count > 3) {
|
||||
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
|
||||
} else {
|
||||
usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
|
||||
}
|
||||
return (
|
||||
<View style={styles.itemContainer}>
|
||||
<View style={styles.emojiContainer}>
|
||||
<Emoji
|
||||
content={item.emoji}
|
||||
standardEmojiStyle={standardEmojiStyle}
|
||||
customEmojiStyle={customEmojiStyle}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.peopleItemContainer}>
|
||||
<Text style={styles.reactCount}>
|
||||
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
|
||||
</Text>
|
||||
<Text style={styles.peopleReacted}>{ usernames }</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isVisible, close, reactions
|
||||
} = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={close}
|
||||
onBackButtonPress={close}
|
||||
backdropOpacity={0.9}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={close}>
|
||||
<View style={styles.titleContainer}>
|
||||
<CustomIcon
|
||||
style={styles.closeButton}
|
||||
name='cross'
|
||||
size={20}
|
||||
onPress={close}
|
||||
/>
|
||||
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<View style={styles.listContainer}>
|
||||
<FlatList
|
||||
data={reactions}
|
||||
renderItem={({ item }) => this.renderItem(item)}
|
||||
keyExtractor={item => item.emoji}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
import { emojify } from 'react-emojione';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import DisclosureIndicator from '../DisclosureIndicator';
|
||||
import styles from './styles';
|
||||
|
||||
const RepliedThread = React.memo(({
|
||||
tmid, tmsg, isHeader, isTemp, fetchThreadName
|
||||
}) => {
|
||||
if (!tmid || !isHeader || isTemp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tmsg) {
|
||||
fetchThreadName(tmid);
|
||||
return null;
|
||||
}
|
||||
|
||||
let msg = emojify(tmsg, { output: 'unicode' });
|
||||
msg = removeMarkdown(msg);
|
||||
|
||||
return (
|
||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} />
|
||||
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
|
||||
<DisclosureIndicator />
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.tmid !== nextProps.tmid) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.tmsg !== nextProps.tmsg) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isHeader !== nextProps.isHeader) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isTemp !== nextProps.isTemp) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
RepliedThread.propTypes = {
|
||||
tmid: PropTypes.string,
|
||||
tmsg: PropTypes.string,
|
||||
isHeader: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
fetchThreadName: PropTypes.func
|
||||
};
|
||||
RepliedThread.displayName = 'MessageRepliedThread';
|
||||
|
||||
export default RepliedThread;
|
|
@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
|
|||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
|
@ -69,98 +70,130 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const onPress = (attachment, baseUrl, user) => {
|
||||
let url = attachment.title_link || attachment.author_link;
|
||||
if (!url) {
|
||||
return;
|
||||
const Title = React.memo(({ attachment, timeFormat }) => {
|
||||
if (!attachment.author_name) {
|
||||
return null;
|
||||
}
|
||||
if (attachment.type === 'file') {
|
||||
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
openLink(url);
|
||||
};
|
||||
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
|
||||
return (
|
||||
<View style={styles.authorContainer}>
|
||||
{attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null}
|
||||
{time ? <Text style={styles.time}>{ time }</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}, () => true);
|
||||
|
||||
const Reply = ({
|
||||
attachment, timeFormat, baseUrl, customEmojis, user, index
|
||||
const Description = React.memo(({
|
||||
attachment, baseUrl, user, getCustomEmoji, useMarkdown
|
||||
}) => {
|
||||
const text = attachment.text || attachment.title;
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Markdown
|
||||
msg={text}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
useMarkdown={useMarkdown}
|
||||
/>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.attachment.text !== nextProps.attachment.text) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.attachment.title !== nextProps.attachment.title) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const Fields = React.memo(({ attachment }) => {
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.fieldsContainer}>
|
||||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text style={styles.fieldTitle}>{field.title}</Text>
|
||||
<Text style={styles.fieldValue}>{field.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields));
|
||||
|
||||
const Reply = React.memo(({
|
||||
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown
|
||||
}) => {
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderAuthor = () => (
|
||||
attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null
|
||||
);
|
||||
|
||||
const renderTime = () => {
|
||||
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
|
||||
return time ? <Text style={styles.time}>{ time }</Text> : null;
|
||||
};
|
||||
|
||||
const renderTitle = () => {
|
||||
if (!attachment.author_name) {
|
||||
return null;
|
||||
const onPress = () => {
|
||||
let url = attachment.title_link || attachment.author_link;
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<View style={styles.authorContainer}>
|
||||
{renderAuthor()}
|
||||
{renderTime()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderText = () => {
|
||||
const text = attachment.text || attachment.title;
|
||||
if (text) {
|
||||
return (
|
||||
<Markdown
|
||||
msg={text}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
/>
|
||||
);
|
||||
if (attachment.type === 'file') {
|
||||
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderFields = () => {
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.fieldsContainer}>
|
||||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text style={styles.fieldTitle}>{field.title}</Text>
|
||||
<Text style={styles.fieldValue}>{field.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
openLink(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Touchable
|
||||
onPress={() => onPress(attachment, baseUrl, user)}
|
||||
onPress={onPress}
|
||||
style={[styles.button, index > 0 && styles.marginTop]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<View style={styles.attachmentContainer}>
|
||||
{renderTitle()}
|
||||
{renderText()}
|
||||
{renderFields()}
|
||||
<Title attachment={attachment} timeFormat={timeFormat} />
|
||||
<Description
|
||||
attachment={attachment}
|
||||
timeFormat={timeFormat}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
useMarkdown={useMarkdown}
|
||||
/>
|
||||
<Fields attachment={attachment} />
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment));
|
||||
|
||||
Reply.propTypes = {
|
||||
attachment: PropTypes.object.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
index: PropTypes.number
|
||||
attachment: PropTypes.object,
|
||||
timeFormat: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Reply.displayName = 'MessageReply';
|
||||
|
||||
Title.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
timeFormat: PropTypes.string
|
||||
};
|
||||
Title.displayName = 'MessageReplyTitle';
|
||||
|
||||
Description.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Description.displayName = 'MessageReplyDescription';
|
||||
|
||||
Fields.propTypes = {
|
||||
attachment: PropTypes.object
|
||||
};
|
||||
Fields.displayName = 'MessageReplyFields';
|
||||
|
||||
export default Reply;
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatLastMessage, formatMessageCount } from './utils';
|
||||
import styles from './styles';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { THREAD } from './constants';
|
||||
|
||||
const Thread = React.memo(({
|
||||
msg, tcount, tlm, customThreadTimeFormat
|
||||
}) => {
|
||||
if (!tlm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const time = formatLastMessage(tlm, customThreadTimeFormat);
|
||||
const buttonText = formatMessageCount(tcount, THREAD);
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<View
|
||||
style={[styles.button, styles.smallButton]}
|
||||
testID={`message-thread-button-${ msg }`}
|
||||
>
|
||||
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</View>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.tcount !== nextProps.tcount) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Thread.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
tcount: PropTypes.string,
|
||||
tlm: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string
|
||||
};
|
||||
Thread.displayName = 'MessageThread';
|
||||
|
||||
export default Thread;
|
|
@ -57,14 +57,22 @@ const UrlImage = React.memo(({ image, user, baseUrl }) => {
|
|||
}
|
||||
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
|
||||
});
|
||||
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
|
||||
|
||||
const UrlContent = React.memo(({ title, description }) => (
|
||||
<View style={styles.textContainer}>
|
||||
{title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null}
|
||||
{description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null}
|
||||
</View>
|
||||
));
|
||||
), (prevProps, nextProps) => {
|
||||
if (prevProps.title !== nextProps.title) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.description !== nextProps.description) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const Url = React.memo(({
|
||||
url, index, user, baseUrl
|
||||
|
@ -89,16 +97,28 @@ const Url = React.memo(({
|
|||
);
|
||||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
|
||||
|
||||
const Urls = React.memo(({ urls, user, baseUrl }) => {
|
||||
if (!urls || urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urls.map((url, index) => (
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
|
||||
));
|
||||
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls));
|
||||
|
||||
UrlImage.propTypes = {
|
||||
image: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
UrlImage.displayName = 'MessageUrlImage';
|
||||
|
||||
UrlContent.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string
|
||||
};
|
||||
UrlContent.displayName = 'MessageUrlContent';
|
||||
|
||||
Url.propTypes = {
|
||||
url: PropTypes.object.isRequired,
|
||||
|
@ -106,5 +126,13 @@ Url.propTypes = {
|
|||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
Url.displayName = 'MessageUrl';
|
||||
|
||||
export default Url;
|
||||
Urls.propTypes = {
|
||||
urls: PropTypes.array,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
Urls.displayName = 'MessageUrls';
|
||||
|
||||
export default Urls;
|
|
@ -30,28 +30,11 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export default class User extends React.PureComponent {
|
||||
static propTypes = {
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Date),
|
||||
PropTypes.string
|
||||
]),
|
||||
temp: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
username, alias, ts, temp, timeFormat
|
||||
} = this.props;
|
||||
|
||||
const extraStyle = {};
|
||||
if (temp) {
|
||||
extraStyle.opacity = 0.3;
|
||||
}
|
||||
|
||||
const User = React.memo(({
|
||||
isHeader, useRealName, author, alias, ts, timeFormat
|
||||
}) => {
|
||||
if (isHeader) {
|
||||
const username = (useRealName && author.name) || author.username;
|
||||
const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
|
||||
const time = moment(ts).format(timeFormat);
|
||||
|
||||
|
@ -67,4 +50,17 @@ export default class User extends React.PureComponent {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
User.propTypes = {
|
||||
isHeader: PropTypes.bool,
|
||||
useRealName: PropTypes.bool,
|
||||
author: PropTypes.object,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.instanceOf(Date),
|
||||
timeFormat: PropTypes.string
|
||||
};
|
||||
User.displayName = 'MessageUser';
|
||||
|
||||
export default User;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import Modal from 'react-native-modal';
|
||||
import VideoPlayer from 'react-native-video-controls';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
|
||||
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/webm', 'video/3gp', 'video/mkv'])];
|
||||
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
|
||||
|
@ -32,77 +32,46 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export default class Video extends React.PureComponent {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired
|
||||
const Video = React.memo(({
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
}) => {
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
state = { isVisible: false };
|
||||
|
||||
get uri() {
|
||||
const { baseUrl, user, file } = this.props;
|
||||
const { video_url } = file;
|
||||
return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
|
||||
toggleModal = () => {
|
||||
this.setState(prevState => ({
|
||||
isVisible: !prevState.isVisible
|
||||
}));
|
||||
}
|
||||
|
||||
open = () => {
|
||||
const { file } = this.props;
|
||||
const onPress = () => {
|
||||
if (isTypeSupported(file.video_type)) {
|
||||
return this.toggleModal();
|
||||
return onOpenFileModal(file);
|
||||
}
|
||||
openLink(this.uri);
|
||||
}
|
||||
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
|
||||
openLink(uri);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isVisible } = this.state;
|
||||
const {
|
||||
baseUrl, user, customEmojis, file
|
||||
} = this.props;
|
||||
const { description } = file;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={styles.button}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<CustomIcon
|
||||
name='play'
|
||||
size={54}
|
||||
style={styles.image}
|
||||
/>
|
||||
</Touchable>
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file));
|
||||
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
Video.propTypes = {
|
||||
file: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
|
||||
return (
|
||||
[
|
||||
<View key='button'>
|
||||
<Touchable
|
||||
onPress={this.open}
|
||||
style={styles.button}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<CustomIcon
|
||||
name='play'
|
||||
size={54}
|
||||
style={styles.image}
|
||||
/>
|
||||
</Touchable>
|
||||
<Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />
|
||||
</View>,
|
||||
<Modal
|
||||
key='modal'
|
||||
isVisible={isVisible}
|
||||
style={styles.modal}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onBackButtonPress={() => this.toggleModal()}
|
||||
>
|
||||
<VideoPlayer
|
||||
source={{ uri: this.uri }}
|
||||
onBack={this.toggleModal}
|
||||
disableVolume
|
||||
/>
|
||||
</Modal>
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Video;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export const DISCUSSION = 'discussion';
|
||||
export const THREAD = 'thread';
|
|
@ -1,30 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ViewPropTypes } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import equal from 'deep-equal';
|
||||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||
|
||||
import Message from './Message';
|
||||
import {
|
||||
errorActionsShow as errorActionsShowAction,
|
||||
toggleReactionPicker as toggleReactionPickerAction,
|
||||
replyBroadcast as replyBroadcastAction
|
||||
} from '../../actions/messages';
|
||||
import { vibrate } from '../../utils/vibration';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { SYSTEM_MESSAGES, getCustomEmoji } from './utils';
|
||||
import messagesStatus from '../../constants/messagesStatus';
|
||||
|
||||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
customEmojis: state.customEmojis,
|
||||
Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
editingMessage: state.messages.message,
|
||||
useRealName: state.settings.UI_Use_Real_Name
|
||||
}), dispatch => ({
|
||||
errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
|
||||
replyBroadcast: message => dispatch(replyBroadcastAction(message)),
|
||||
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message))
|
||||
}))
|
||||
export default class MessageContainer extends React.Component {
|
||||
static propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
|
@ -33,31 +15,28 @@ export default class MessageContainer extends React.Component {
|
|||
username: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired
|
||||
}),
|
||||
customTimeFormat: PropTypes.string,
|
||||
timeFormat: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
style: ViewPropTypes.style,
|
||||
style: PropTypes.any,
|
||||
archived: PropTypes.bool,
|
||||
broadcast: PropTypes.bool,
|
||||
previousItem: PropTypes.object,
|
||||
_updatedAt: PropTypes.instanceOf(Date),
|
||||
// redux
|
||||
baseUrl: PropTypes.string,
|
||||
customEmojis: PropTypes.object,
|
||||
Message_GroupingPeriod: PropTypes.number,
|
||||
Message_TimeFormat: PropTypes.string,
|
||||
editingMessage: PropTypes.object,
|
||||
useRealName: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
status: PropTypes.number,
|
||||
navigation: PropTypes.object,
|
||||
// methods - props
|
||||
onLongPress: PropTypes.func,
|
||||
onReactionPress: PropTypes.func,
|
||||
onDiscussionPress: PropTypes.func,
|
||||
// methods - redux
|
||||
onThreadPress: PropTypes.func,
|
||||
errorActionsShow: PropTypes.func,
|
||||
replyBroadcast: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
fetchThreadName: PropTypes.func
|
||||
fetchThreadName: PropTypes.func,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -67,21 +46,11 @@ export default class MessageContainer extends React.Component {
|
|||
broadcast: false
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { reactionsModal: false };
|
||||
this.closeReactions = this.closeReactions.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { reactionsModal } = this.state;
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const {
|
||||
status, editingMessage, item, _updatedAt, navigation
|
||||
status, item, _updatedAt
|
||||
} = this.props;
|
||||
|
||||
if (reactionsModal !== nextState.reactionsModal) {
|
||||
return true;
|
||||
}
|
||||
if (status !== nextProps.status) {
|
||||
return true;
|
||||
}
|
||||
|
@ -89,65 +58,64 @@ export default class MessageContainer extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
|
||||
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
|
||||
return true;
|
||||
} else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
|
||||
}
|
||||
|
||||
onPress = debounce(() => {
|
||||
const { item } = this.props;
|
||||
KeyboardUtils.dismiss();
|
||||
|
||||
if ((item.tlm || item.tmid)) {
|
||||
this.onThreadPress();
|
||||
}
|
||||
}, 300, true);
|
||||
|
||||
onLongPress = () => {
|
||||
const { onLongPress } = this.props;
|
||||
onLongPress(this.parseMessage());
|
||||
const { archived, onLongPress } = this.props;
|
||||
if (this.isInfo || this.hasError || archived) {
|
||||
return;
|
||||
}
|
||||
if (onLongPress) {
|
||||
onLongPress(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
onErrorPress = () => {
|
||||
const { errorActionsShow } = this.props;
|
||||
errorActionsShow(this.parseMessage());
|
||||
if (errorActionsShow) {
|
||||
errorActionsShow(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
onReactionPress = (emoji) => {
|
||||
const { onReactionPress, item } = this.props;
|
||||
onReactionPress(emoji, item._id);
|
||||
if (onReactionPress) {
|
||||
onReactionPress(emoji, item._id);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionLongPress = () => {
|
||||
this.setState({ reactionsModal: true });
|
||||
vibrate();
|
||||
const { onReactionLongPress, item } = this.props;
|
||||
if (onReactionLongPress) {
|
||||
onReactionLongPress(item);
|
||||
}
|
||||
}
|
||||
|
||||
onDiscussionPress = () => {
|
||||
const { onDiscussionPress, item } = this.props;
|
||||
onDiscussionPress(item);
|
||||
}
|
||||
|
||||
onThreadPress = debounce(() => {
|
||||
const { navigation, item } = this.props;
|
||||
if (item.tmid) {
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
|
||||
});
|
||||
} else if (item.tlm) {
|
||||
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item._id, name: title, t: 'thread'
|
||||
});
|
||||
if (onDiscussionPress) {
|
||||
onDiscussionPress(item);
|
||||
}
|
||||
}, 1000, true)
|
||||
|
||||
get timeFormat() {
|
||||
const { customTimeFormat, Message_TimeFormat } = this.props;
|
||||
return customTimeFormat || Message_TimeFormat;
|
||||
}
|
||||
|
||||
closeReactions = () => {
|
||||
this.setState({ reactionsModal: false });
|
||||
onThreadPress = () => {
|
||||
const { onThreadPress, item } = this.props;
|
||||
if (onThreadPress) {
|
||||
onThreadPress(item);
|
||||
}
|
||||
}
|
||||
|
||||
isHeader = () => {
|
||||
get isHeader() {
|
||||
const {
|
||||
item, previousItem, broadcast, Message_GroupingPeriod
|
||||
} = this.props;
|
||||
|
@ -163,7 +131,7 @@ export default class MessageContainer extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
isThreadReply = () => {
|
||||
get isThreadReply() {
|
||||
const {
|
||||
item, previousItem
|
||||
} = this.props;
|
||||
|
@ -173,7 +141,7 @@ export default class MessageContainer extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
isThreadSequential = () => {
|
||||
get isThreadSequential() {
|
||||
const {
|
||||
item, previousItem
|
||||
} = this.props;
|
||||
|
@ -183,6 +151,21 @@ export default class MessageContainer extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
get isInfo() {
|
||||
const { item } = this.props;
|
||||
return SYSTEM_MESSAGES.includes(item.t);
|
||||
}
|
||||
|
||||
get isTemp() {
|
||||
const { item } = this.props;
|
||||
return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
const { item } = this.props;
|
||||
return item.status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
parseMessage = () => {
|
||||
const { item } = this.props;
|
||||
return JSON.parse(JSON.stringify(item));
|
||||
|
@ -190,23 +173,26 @@ export default class MessageContainer extends React.Component {
|
|||
|
||||
toggleReactionPicker = () => {
|
||||
const { toggleReactionPicker } = this.props;
|
||||
toggleReactionPicker(this.parseMessage());
|
||||
if (toggleReactionPicker) {
|
||||
toggleReactionPicker(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
replyBroadcast = () => {
|
||||
const { replyBroadcast } = this.props;
|
||||
replyBroadcast(this.parseMessage());
|
||||
if (replyBroadcast) {
|
||||
replyBroadcast(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { reactionsModal } = this.state;
|
||||
const {
|
||||
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast, fetchThreadName, customThreadTimeFormat
|
||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown
|
||||
} = this.props;
|
||||
const {
|
||||
_id, msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg
|
||||
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels
|
||||
} = item;
|
||||
const isEditing = editingMessage._id === item._id;
|
||||
|
||||
return (
|
||||
<Message
|
||||
id={_id}
|
||||
|
@ -214,26 +200,18 @@ export default class MessageContainer extends React.Component {
|
|||
author={u}
|
||||
ts={ts}
|
||||
type={t}
|
||||
status={status}
|
||||
attachments={attachments}
|
||||
urls={urls}
|
||||
reactions={reactions}
|
||||
alias={alias}
|
||||
editing={isEditing}
|
||||
header={this.isHeader()}
|
||||
isThreadReply={this.isThreadReply()}
|
||||
isThreadSequential={this.isThreadSequential()}
|
||||
avatar={avatar}
|
||||
user={user}
|
||||
edited={editedBy && !!editedBy.username}
|
||||
timeFormat={this.timeFormat}
|
||||
timeFormat={timeFormat}
|
||||
customThreadTimeFormat={customThreadTimeFormat}
|
||||
style={style}
|
||||
archived={archived}
|
||||
broadcast={broadcast}
|
||||
baseUrl={baseUrl}
|
||||
customEmojis={customEmojis}
|
||||
reactionsModal={reactionsModal}
|
||||
useRealName={useRealName}
|
||||
role={role}
|
||||
drid={drid}
|
||||
|
@ -243,16 +221,27 @@ export default class MessageContainer extends React.Component {
|
|||
tcount={tcount}
|
||||
tlm={tlm}
|
||||
tmsg={tmsg}
|
||||
useMarkdown={useMarkdown}
|
||||
fetchThreadName={fetchThreadName}
|
||||
closeReactions={this.closeReactions}
|
||||
mentions={mentions}
|
||||
channels={channels}
|
||||
isEdited={editedBy && !!editedBy.username}
|
||||
isHeader={this.isHeader}
|
||||
isThreadReply={this.isThreadReply}
|
||||
isThreadSequential={this.isThreadSequential}
|
||||
isInfo={this.isInfo}
|
||||
isTemp={this.isTemp}
|
||||
hasError={this.hasError}
|
||||
onErrorPress={this.onErrorPress}
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onLongPress}
|
||||
onReactionLongPress={this.onReactionLongPress}
|
||||
onReactionPress={this.onReactionPress}
|
||||
replyBroadcast={this.replyBroadcast}
|
||||
toggleReactionPicker={this.toggleReactionPicker}
|
||||
onDiscussionPress={this.onDiscussionPress}
|
||||
onThreadPress={this.onThreadPress}
|
||||
onOpenFileModal={onOpenFileModal}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,8 +18,7 @@ export default StyleSheet.create({
|
|||
paddingVertical: 4,
|
||||
width: '100%',
|
||||
paddingHorizontal: 14,
|
||||
flexDirection: 'column',
|
||||
flex: 1
|
||||
flexDirection: 'column'
|
||||
},
|
||||
messageContent: {
|
||||
flex: 1,
|
||||
|
@ -32,8 +31,8 @@ export default StyleSheet.create({
|
|||
marginLeft: 0
|
||||
},
|
||||
flex: {
|
||||
flexDirection: 'row',
|
||||
flex: 1
|
||||
flexDirection: 'row'
|
||||
// flex: 1
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
|
@ -46,9 +45,6 @@ export default StyleSheet.create({
|
|||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
editing: {
|
||||
backgroundColor: '#fff5df'
|
||||
},
|
||||
customEmoji: {
|
||||
width: 20,
|
||||
height: 20
|
||||
|
@ -161,7 +157,7 @@ export default StyleSheet.create({
|
|||
justifyContent: 'flex-start'
|
||||
},
|
||||
imageContainer: {
|
||||
flex: 1,
|
||||
// flex: 1,
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4
|
||||
},
|
||||
|
@ -173,6 +169,9 @@ export default StyleSheet.create({
|
|||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1
|
||||
},
|
||||
imagePressed: {
|
||||
opacity: 0.5
|
||||
},
|
||||
inlineImage: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
|
@ -220,7 +219,7 @@ export default StyleSheet.create({
|
|||
},
|
||||
repliedThread: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
// flex: 1,
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
marginBottom: 12
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
import moment from 'moment';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import database from '../../lib/realm';
|
||||
import { DISCUSSION } from './constants';
|
||||
|
||||
export const formatLastMessage = (lm, customFormat) => {
|
||||
if (customFormat) {
|
||||
return moment(lm).format(customFormat);
|
||||
}
|
||||
return lm ? moment(lm).calendar(null, {
|
||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'h:mm A',
|
||||
lastWeek: 'dddd',
|
||||
sameElse: 'MMM D'
|
||||
}) : null;
|
||||
};
|
||||
|
||||
export const formatMessageCount = (count, type) => {
|
||||
const discussion = type === DISCUSSION;
|
||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||
if (count === 1) {
|
||||
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
|
||||
} else if (count > 1 && count < 1000) {
|
||||
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
} else if (count > 999) {
|
||||
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
export const BUTTON_HIT_SLOP = {
|
||||
top: 4, right: 4, bottom: 4, left: 4
|
||||
};
|
||||
|
||||
export const SYSTEM_MESSAGES = [
|
||||
'r',
|
||||
'au',
|
||||
'ru',
|
||||
'ul',
|
||||
'uj',
|
||||
'ut',
|
||||
'rm',
|
||||
'user-muted',
|
||||
'user-unmuted',
|
||||
'message_pinned',
|
||||
'subscription-role-added',
|
||||
'subscription-role-removed',
|
||||
'room_changed_description',
|
||||
'room_changed_announcement',
|
||||
'room_changed_topic',
|
||||
'room_changed_privacy',
|
||||
'message_snippeted',
|
||||
'thread-created'
|
||||
];
|
||||
|
||||
export const getInfoMessage = ({
|
||||
type, role, msg, author
|
||||
}) => {
|
||||
const { username } = author;
|
||||
if (type === 'rm') {
|
||||
return I18n.t('Message_removed');
|
||||
} else if (type === 'uj') {
|
||||
return I18n.t('Has_joined_the_channel');
|
||||
} else if (type === 'ut') {
|
||||
return I18n.t('Has_joined_the_conversation');
|
||||
} else if (type === 'r') {
|
||||
return I18n.t('Room_name_changed', { name: msg, userBy: username });
|
||||
} else if (type === 'message_pinned') {
|
||||
return I18n.t('Message_pinned');
|
||||
} else if (type === 'ul') {
|
||||
return I18n.t('Has_left_the_channel');
|
||||
} else if (type === 'ru') {
|
||||
return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
|
||||
} else if (type === 'au') {
|
||||
return I18n.t('User_added_by', { userAdded: msg, userBy: username });
|
||||
} else if (type === 'user-muted') {
|
||||
return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
|
||||
} else if (type === 'user-unmuted') {
|
||||
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
|
||||
} else if (type === 'subscription-role-added') {
|
||||
return `${ msg } was set ${ role } by ${ username }`;
|
||||
} else if (type === 'subscription-role-removed') {
|
||||
return `${ msg } is no longer ${ role } by ${ username }`;
|
||||
} else if (type === 'room_changed_description') {
|
||||
return I18n.t('Room_changed_description', { description: msg, userBy: username });
|
||||
} else if (type === 'room_changed_announcement') {
|
||||
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
|
||||
} else if (type === 'room_changed_topic') {
|
||||
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
|
||||
} else if (type === 'room_changed_privacy') {
|
||||
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
|
||||
} else if (type === 'message_snippeted') {
|
||||
return I18n.t('Created_snippet');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getCustomEmoji = (content) => {
|
||||
// search by name
|
||||
const data = database.objects('customEmojis').filtered('name == $0', content);
|
||||
if (data.length) {
|
||||
return data[0];
|
||||
}
|
||||
|
||||
// searches by alias
|
||||
// RealmJS doesn't support IN operator: https://github.com/realm/realm-js/issues/450
|
||||
const emojis = database.objects('customEmojis');
|
||||
const findByAlias = emojis.find((emoji) => {
|
||||
if (emoji.aliases.length && emoji.aliases.findIndex(alias => alias === content) !== -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return findByAlias;
|
||||
};
|
|
@ -308,7 +308,6 @@ export default {
|
|||
This_room_is_blocked: 'Dieser Raum ist gesperrt',
|
||||
This_room_is_read_only: 'Dieser Raum kann nur gelesen werden',
|
||||
Timezone: 'Zeitzone',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: 'Thema',
|
||||
Topic: 'Thema',
|
||||
Try_again: 'Versuchen Sie es nochmal',
|
||||
|
|
|
@ -81,6 +81,7 @@ export default {
|
|||
Add_Reaction: 'Add Reaction',
|
||||
Add_Server: 'Add Server',
|
||||
Add_user: 'Add user',
|
||||
Admin_Panel: 'Admin Panel',
|
||||
Alert: 'Alert',
|
||||
alert: 'alert',
|
||||
alerts: 'alerts',
|
||||
|
@ -147,13 +148,15 @@ export default {
|
|||
Dont_Have_An_Account: 'Don\'t have an account?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
|
||||
edit: 'edit',
|
||||
erasing_room: 'erasing room',
|
||||
edited: 'edited',
|
||||
Edit: 'Edit',
|
||||
Email_or_password_field_is_empty: 'Email or password field is empty',
|
||||
Email: 'Email',
|
||||
email: 'e-mail',
|
||||
Enable_markdown: 'Enable markdown',
|
||||
Enable_notifications: 'Enable notifications',
|
||||
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
||||
erasing_room: 'erasing room',
|
||||
Error_uploading: 'Error uploading',
|
||||
Favorites: 'Favorites',
|
||||
Files: 'Files',
|
||||
|
@ -203,6 +206,7 @@ export default {
|
|||
message: 'message',
|
||||
messages: 'messages',
|
||||
Messages: 'Messages',
|
||||
Message_Reported: 'Message reported',
|
||||
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
|
||||
Microphone_Permission: 'Microphone Permission',
|
||||
Mute: 'Mute',
|
||||
|
@ -266,6 +270,7 @@ export default {
|
|||
replies: 'replies',
|
||||
reply: 'reply',
|
||||
Reply: 'Reply',
|
||||
Report: 'Report',
|
||||
Resend: 'Resend',
|
||||
Reset_password: 'Reset password',
|
||||
resetting_password: 'resetting password',
|
||||
|
@ -324,7 +329,6 @@ export default {
|
|||
Thread: 'Thread',
|
||||
Threads: 'Threads',
|
||||
Timezone: 'Timezone',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: 'topic',
|
||||
Topic: 'Topic',
|
||||
Try_again: 'Try again',
|
||||
|
|
|
@ -309,7 +309,6 @@ export default {
|
|||
This_room_is_blocked: 'Cette canal est bloquée',
|
||||
This_room_is_read_only: 'Cette canal est en lecture seule',
|
||||
Timezone: 'Fuseau horaire',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: 'sujet',
|
||||
Topic: 'Sujet',
|
||||
Try_again: 'Réessayer',
|
||||
|
|
|
@ -154,11 +154,13 @@ export default {
|
|||
Dont_Have_An_Account: 'Não tem uma conta?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
|
||||
edit: 'editar',
|
||||
edited: 'editado',
|
||||
erasing_room: 'apagando sala',
|
||||
Edit: 'Editar',
|
||||
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
||||
Email: 'Email',
|
||||
email: 'e-mail',
|
||||
Enable_markdown: 'Habilitar markdown',
|
||||
Enable_notifications: 'Habilitar notificações',
|
||||
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
|
||||
Error_uploading: 'Erro subindo',
|
||||
|
|
|
@ -311,7 +311,6 @@ export default {
|
|||
This_room_is_blocked: 'Esta sala está bloqueada',
|
||||
This_room_is_read_only: 'Esta sala é apenas de leitura',
|
||||
Timezone: 'Fuso Horário',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: 'tópico',
|
||||
Topic: 'Tópico',
|
||||
Try_again: 'Tente novamente',
|
||||
|
|
|
@ -270,7 +270,6 @@ export default {
|
|||
This_room_is_blocked: 'Этот канал заблокирован',
|
||||
This_room_is_read_only: 'Этот канал доступен только для чтения',
|
||||
Timezone: 'Часовой пояс',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: 'топик',
|
||||
Topic: 'Топик',
|
||||
Try_again: 'Попробуйте еще раз',
|
||||
|
|
|
@ -305,7 +305,6 @@ export default {
|
|||
This_room_is_blocked: '这个房间被锁了',
|
||||
This_room_is_read_only: '这个房间是只读的',
|
||||
Timezone: '时区',
|
||||
Toggle_Drawer: 'Toggle_Drawer',
|
||||
topic: '主题',
|
||||
Topic: '主题',
|
||||
Try_again: '再试一次',
|
||||
|
|
46
app/index.js
46
app/index.js
|
@ -5,6 +5,7 @@ import {
|
|||
import { Provider } from 'react-redux';
|
||||
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
|
||||
import { Linking } from 'react-native';
|
||||
import firebase from 'react-native-firebase';
|
||||
|
||||
import { appInit } from './actions';
|
||||
import { deepLinkingOpen } from './actions/deepLinking';
|
||||
|
@ -20,16 +21,14 @@ import Navigation from './lib/Navigation';
|
|||
import Sidebar from './views/SidebarView';
|
||||
import ProfileView from './views/ProfileView';
|
||||
import SettingsView from './views/SettingsView';
|
||||
import AdminPanelView from './views/AdminPanelView';
|
||||
import RoomActionsView from './views/RoomActionsView';
|
||||
import RoomInfoView from './views/RoomInfoView';
|
||||
import RoomInfoEditView from './views/RoomInfoEditView';
|
||||
import RoomMembersView from './views/RoomMembersView';
|
||||
import RoomFilesView from './views/RoomFilesView';
|
||||
import MentionedMessagesView from './views/MentionedMessagesView';
|
||||
import StarredMessagesView from './views/StarredMessagesView';
|
||||
import SearchMessagesView from './views/SearchMessagesView';
|
||||
import PinnedMessagesView from './views/PinnedMessagesView';
|
||||
import ThreadMessagesView from './views/ThreadMessagesView';
|
||||
import MessagesView from './views/MessagesView';
|
||||
import SelectedUsersView from './views/SelectedUsersView';
|
||||
import CreateChannelView from './views/CreateChannelView';
|
||||
import LegalView from './views/LegalView';
|
||||
|
@ -108,13 +107,10 @@ const ChatsStack = createStackNavigator({
|
|||
RoomInfoView,
|
||||
RoomInfoEditView,
|
||||
RoomMembersView,
|
||||
RoomFilesView,
|
||||
MentionedMessagesView,
|
||||
StarredMessagesView,
|
||||
SearchMessagesView,
|
||||
PinnedMessagesView,
|
||||
SelectedUsersView,
|
||||
ThreadMessagesView
|
||||
ThreadMessagesView,
|
||||
MessagesView
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
@ -151,6 +147,12 @@ const SettingsStack = createStackNavigator({
|
|||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
||||
const AdminPanelStack = createStackNavigator({
|
||||
AdminPanelView
|
||||
}, {
|
||||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
||||
SettingsStack.navigationOptions = ({ navigation }) => {
|
||||
let drawerLockMode = 'unlocked';
|
||||
if (navigation.state.index > 0) {
|
||||
|
@ -164,7 +166,8 @@ SettingsStack.navigationOptions = ({ navigation }) => {
|
|||
const ChatsDrawer = createDrawerNavigator({
|
||||
ChatsStack,
|
||||
ProfileStack,
|
||||
SettingsStack
|
||||
SettingsStack,
|
||||
AdminPanelStack
|
||||
}, {
|
||||
contentComponent: Sidebar
|
||||
});
|
||||
|
@ -202,6 +205,28 @@ const App = createAppContainer(createSwitchNavigator(
|
|||
}
|
||||
));
|
||||
|
||||
// gets the current screen from navigation state
|
||||
const getActiveRouteName = (navigationState) => {
|
||||
if (!navigationState) {
|
||||
return null;
|
||||
}
|
||||
const route = navigationState.routes[navigationState.index];
|
||||
// dive into nested navigators
|
||||
if (route.routes) {
|
||||
return getActiveRouteName(route);
|
||||
}
|
||||
return route.routeName;
|
||||
};
|
||||
|
||||
const onNavigationStateChange = (prevState, currentState) => {
|
||||
const currentScreen = getActiveRouteName(currentState);
|
||||
const prevScreen = getActiveRouteName(prevState);
|
||||
|
||||
if (prevScreen !== currentScreen) {
|
||||
firebase.analytics().setCurrentScreen(currentScreen);
|
||||
}
|
||||
};
|
||||
|
||||
export default class Root extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -242,6 +267,7 @@ export default class Root extends React.Component {
|
|||
ref={(navigatorRef) => {
|
||||
Navigation.setTopLevelNavigator(navigatorRef);
|
||||
}}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,6 @@ import semver from 'semver';
|
|||
|
||||
import reduxStore from '../createStore';
|
||||
import database from '../realm';
|
||||
import * as actions from '../../actions';
|
||||
import log from '../../utils/log';
|
||||
|
||||
const getUpdatedSince = () => {
|
||||
|
@ -17,7 +16,7 @@ const create = (customEmojis) => {
|
|||
try {
|
||||
database.create('customEmojis', emoji, true);
|
||||
} catch (e) {
|
||||
log('getEmojis create', e);
|
||||
// log('getEmojis create', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -40,7 +39,6 @@ export default async function() {
|
|||
database.write(() => {
|
||||
create(emojis);
|
||||
});
|
||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(result.emojis)));
|
||||
});
|
||||
} else {
|
||||
const params = {};
|
||||
|
@ -68,17 +66,14 @@ export default async function() {
|
|||
database.delete(emojiRecord);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getEmojis delete', e);
|
||||
log('err_get_emojis_delete', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const allEmojis = database.objects('customEmojis');
|
||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(allEmojis)));
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getCustomEmojis', e);
|
||||
log('err_get_custom_emojis', e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ const create = (permissions) => {
|
|||
try {
|
||||
database.create('permissions', permission, true);
|
||||
} catch (e) {
|
||||
log('getPermissions create', e);
|
||||
log('err_get_permissions_create', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ export default async function() {
|
|||
database.delete(permission);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getPermissions delete', e);
|
||||
log('err_get_permissions_delete', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -71,6 +71,6 @@ export default async function() {
|
|||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log('getPermissions', e);
|
||||
log('err_get_permissions', e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,12 +20,12 @@ export default async function() {
|
|||
try {
|
||||
database.create('roles', role, true);
|
||||
} catch (e) {
|
||||
log('getRoles create', e);
|
||||
log('err_get_roles_create', e);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('getRoles', e);
|
||||
log('err_get_roles', e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ function updateServer(param) {
|
|||
try {
|
||||
database.databases.serversDB.create('servers', { id: reduxStore.getState().server.server, ...param }, true);
|
||||
} catch (e) {
|
||||
log('updateServer', e);
|
||||
log('err_get_settings_update_server', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export default async function() {
|
|||
try {
|
||||
database.create('settings', { ...setting, _updatedAt: new Date() }, true);
|
||||
} catch (e) {
|
||||
log('create settings', e);
|
||||
log('err_get_settings_create', e);
|
||||
}
|
||||
|
||||
if (setting._id === 'Site_Name') {
|
||||
|
@ -52,6 +52,6 @@ export default async function() {
|
|||
updateServer.call(this, { iconURL });
|
||||
}
|
||||
} catch (e) {
|
||||
log('getSettings', e);
|
||||
log('err_get_settings', e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
export default function(message) {
|
||||
if (/image/.test(message.type)) {
|
||||
return { image_url: message.url };
|
||||
}
|
||||
if (/audio/.test(message.type)) {
|
||||
return { audio_url: message.url };
|
||||
}
|
||||
if (/video/.test(message.type)) {
|
||||
return { video_url: message.url };
|
||||
}
|
||||
return {
|
||||
title_link: message.url,
|
||||
type: 'file'
|
||||
};
|
||||
}
|
|
@ -27,9 +27,8 @@ export const merge = (subscription, room) => {
|
|||
if (!subscription.roles || !subscription.roles.length) {
|
||||
subscription.roles = [];
|
||||
}
|
||||
|
||||
if (room.muted && room.muted.length) {
|
||||
subscription.muted = room.muted.filter(user => user).map(user => ({ value: user }));
|
||||
subscription.muted = room.muted.filter(muted => !!muted);
|
||||
} else {
|
||||
subscription.muted = [];
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export default (msg) => {
|
|||
// msg.reactions = Object.keys(msg.reactions).map(key => ({ emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
|
||||
// }
|
||||
if (!Array.isArray(msg.reactions)) {
|
||||
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
|
||||
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames }));
|
||||
}
|
||||
msg.urls = msg.urls ? parseUrls(msg.urls) : [];
|
||||
msg._updatedAt = new Date();
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { Answers } from 'react-native-fabric';
|
||||
|
||||
export default fn => (...params) => {
|
||||
try {
|
||||
fn(...params);
|
||||
|
@ -8,7 +6,6 @@ export default fn => (...params) => {
|
|||
if (typeof error !== 'object') {
|
||||
error = { error };
|
||||
}
|
||||
Answers.logCustom('error', error);
|
||||
if (__DEV__) {
|
||||
alert(error);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ export default function loadMessagesForRoom(...args) {
|
|||
database.create('threadMessages', message, true);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadMessagesForRoom -> create messages', e);
|
||||
log('err_load_messages_for_room_create', e);
|
||||
}
|
||||
}));
|
||||
return resolve(data);
|
||||
|
@ -61,7 +61,7 @@ export default function loadMessagesForRoom(...args) {
|
|||
return resolve([]);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadMessagesForRoom', e);
|
||||
log('err_load_messages_for_room', e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ export default function loadMissedMessages(...args) {
|
|||
database.create('threadMessages', message, true);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadMissedMessages -> create messages', e);
|
||||
log('err_load_missed_messages_create', e);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
@ -65,14 +65,14 @@ export default function loadMissedMessages(...args) {
|
|||
});
|
||||
});
|
||||
} catch (e) {
|
||||
log('loadMissedMessages -> delete message', e);
|
||||
log('err_load_missed_messages_delete', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
} catch (e) {
|
||||
log('loadMissedMessages', e);
|
||||
log('err_load_missed_messages', e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
|
|||
message.rid = tmid;
|
||||
database.create('threadMessages', message, true);
|
||||
} catch (e) {
|
||||
log('loadThreadMessages -> create messages', e);
|
||||
log('err_load_thread_messages_create', e);
|
||||
}
|
||||
}));
|
||||
return resolve(data);
|
||||
|
@ -43,7 +43,7 @@ export default function loadThreadMessages({ tmid, offset = 0 }) {
|
|||
return resolve([]);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadThreadMessages', e);
|
||||
log('err_load_thread_messages', e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,6 +18,6 @@ export default async function readMessages(rid) {
|
|||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
log('readMessages', e);
|
||||
log('err_read_messages', e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
|
|||
try {
|
||||
database.create('uploads', fileInfo, true);
|
||||
} catch (e) {
|
||||
return log('sendFileMessage -> create uploads 1', e);
|
||||
return log('err_send_file_message_create_upload_1', e);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -69,7 +69,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
|
|||
try {
|
||||
database.create('uploads', fileInfo, true);
|
||||
} catch (e) {
|
||||
return log('sendFileMessage -> create uploads 2', e);
|
||||
return log('err_send_file_message_create_upload_2', e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -95,7 +95,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
|
|||
try {
|
||||
database.delete(upload);
|
||||
} catch (e) {
|
||||
log('sendFileMessage -> delete uploads', e);
|
||||
log('err_send_file_message_delete_upload', e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -104,7 +104,7 @@ export async function sendFileMessage(rid, fileInfo, tmid) {
|
|||
try {
|
||||
database.create('uploads', fileInfo, true);
|
||||
} catch (err) {
|
||||
log('sendFileMessage -> create uploads 3', err);
|
||||
log('err_send_file_message_create_upload_3', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -66,6 +66,6 @@ export default async function(rid, msg, tmid) {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log('sendMessage', e);
|
||||
log('err_send_message', e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ export default function subscribeRoom({ rid }) {
|
|||
typingTimeouts[username] = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('TCL: removeUserTyping -> error', error);
|
||||
log('err_remove_user_typing', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -85,7 +85,7 @@ export default function subscribeRoom({ rid }) {
|
|||
removeUserTyping(username);
|
||||
}, 10000);
|
||||
} catch (error) {
|
||||
console.log('TCL: addUserTyping -> error', error);
|
||||
log('err_add_user_typing', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -125,7 +125,7 @@ export default function subscribeRoom({ rid }) {
|
|||
|
||||
const read = debounce(() => {
|
||||
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||
if (room._id) {
|
||||
if (room && room._id) {
|
||||
this.readMessages(rid);
|
||||
}
|
||||
}, 300);
|
||||
|
@ -198,7 +198,7 @@ export default function subscribeRoom({ rid }) {
|
|||
try {
|
||||
promises = this.sdk.subscribeRoom(rid);
|
||||
} catch (e) {
|
||||
log('subscribeRoom', e);
|
||||
log('err_subscribe_room', e);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -59,7 +59,7 @@ export default async function subscribeRooms() {
|
|||
database.delete(subscription);
|
||||
});
|
||||
} catch (e) {
|
||||
log('handleStreamMessageReceived -> subscriptions removed', e);
|
||||
log('err_stream_msg_received_sub_removed', e);
|
||||
}
|
||||
} else {
|
||||
const rooms = database.objects('rooms').filtered('_id == $0', data.rid);
|
||||
|
@ -70,7 +70,7 @@ export default async function subscribeRooms() {
|
|||
database.delete(rooms);
|
||||
});
|
||||
} catch (e) {
|
||||
log('handleStreamMessageReceived -> subscriptions updated', e);
|
||||
log('err_stream_msg_received_sub_updated', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ export default async function subscribeRooms() {
|
|||
database.create('subscriptions', tmp, true);
|
||||
});
|
||||
} catch (e) {
|
||||
log('handleStreamMessageReceived -> rooms updated', e);
|
||||
log('err_stream_msg_received_room_updated', e);
|
||||
}
|
||||
} else if (type === 'inserted') {
|
||||
try {
|
||||
|
@ -91,7 +91,7 @@ export default async function subscribeRooms() {
|
|||
database.create('rooms', data, true);
|
||||
});
|
||||
} catch (e) {
|
||||
log('handleStreamMessageReceived -> rooms inserted', e);
|
||||
log('err_stream_msg_received_room_inserted', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ export default async function subscribeRooms() {
|
|||
database.create('messages', message, true);
|
||||
});
|
||||
} catch (e) {
|
||||
log('handleStreamMessageReceived -> message', e);
|
||||
log('err_stream_msg_received_message', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ export default async function subscribeRooms() {
|
|||
try {
|
||||
await this.sdk.subscribeNotifyUser();
|
||||
} catch (e) {
|
||||
log('subscribeRooms', e);
|
||||
log('err_subscribe_rooms', e);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -43,18 +43,11 @@ const roomsSchema = {
|
|||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
name: 'string?',
|
||||
broadcast: { type: 'bool', optional: true }
|
||||
}
|
||||
};
|
||||
|
||||
const userMutedInRoomSchema = {
|
||||
name: 'usersMuted',
|
||||
primaryKey: 'value',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const subscriptionSchema = {
|
||||
name: 'subscriptions',
|
||||
primaryKey: '_id',
|
||||
|
@ -85,7 +78,7 @@ const subscriptionSchema = {
|
|||
archived: { type: 'bool', optional: true },
|
||||
joinCodeRequired: { type: 'bool', optional: true },
|
||||
notifications: { type: 'bool', optional: true },
|
||||
muted: { type: 'list', objectType: 'usersMuted' },
|
||||
muted: 'string[]',
|
||||
broadcast: { type: 'bool', optional: true },
|
||||
prid: { type: 'string', optional: true },
|
||||
draftMessage: { type: 'string', optional: true },
|
||||
|
@ -99,8 +92,7 @@ const usersSchema = {
|
|||
properties: {
|
||||
_id: 'string',
|
||||
username: 'string',
|
||||
name: { type: 'string', optional: true },
|
||||
avatarVersion: { type: 'int', optional: true }
|
||||
name: { type: 'string', optional: true }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -155,21 +147,13 @@ const url = {
|
|||
}
|
||||
};
|
||||
|
||||
const messagesReactionsUsernamesSchema = {
|
||||
name: 'messagesReactionsUsernames',
|
||||
primaryKey: 'value',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const messagesReactionsSchema = {
|
||||
name: 'messagesReactions',
|
||||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
emoji: 'string',
|
||||
usernames: { type: 'list', objectType: 'messagesReactionsUsernames' }
|
||||
usernames: 'string[]'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -211,7 +195,9 @@ const messagesSchema = {
|
|||
tmid: { type: 'string', optional: true },
|
||||
tcount: { type: 'int', optional: true },
|
||||
tlm: { type: 'date', optional: true },
|
||||
replies: 'string[]'
|
||||
replies: 'string[]',
|
||||
mentions: { type: 'list', objectType: 'users' },
|
||||
channels: { type: 'list', objectType: 'rooms' }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -359,9 +345,7 @@ const schema = [
|
|||
frequentlyUsedEmojiSchema,
|
||||
customEmojisSchema,
|
||||
messagesReactionsSchema,
|
||||
messagesReactionsUsernamesSchema,
|
||||
rolesSchema,
|
||||
userMutedInRoomSchema,
|
||||
uploadsSchema
|
||||
];
|
||||
|
||||
|
@ -374,9 +358,9 @@ class DB {
|
|||
schema: [
|
||||
serversSchema
|
||||
],
|
||||
schemaVersion: 6,
|
||||
schemaVersion: 8,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
|
||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 8) {
|
||||
const newServers = newRealm.objects('servers');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
|
@ -431,16 +415,11 @@ class DB {
|
|||
return this.databases.activeDB = new Realm({
|
||||
path: `${ path }.realm`,
|
||||
schema,
|
||||
schemaVersion: 9,
|
||||
schemaVersion: 11,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < newSubs.length; i++) {
|
||||
newSubs[i].lastOpen = null;
|
||||
newSubs[i].ls = null;
|
||||
}
|
||||
newRealm.delete(newSubs);
|
||||
const newMessages = newRealm.objects('messages');
|
||||
newRealm.delete(newMessages);
|
||||
const newThreads = newRealm.objects('threads');
|
||||
|
@ -449,8 +428,6 @@ class DB {
|
|||
newRealm.delete(newThreadMessages);
|
||||
}
|
||||
if (newRealm.schemaVersion === 9) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
newRealm.delete(newSubs);
|
||||
const newEmojis = newRealm.objects('customEmojis');
|
||||
newRealm.delete(newEmojis);
|
||||
const newSettings = newRealm.objects('settings');
|
||||
|
|
|
@ -40,9 +40,12 @@ import { roomsRequest } from '../actions/rooms';
|
|||
|
||||
const TOKEN_KEY = 'reactnativemeteor_usertoken';
|
||||
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
|
||||
export const MARKDOWN_KEY = 'RC_MARKDOWN_KEY';
|
||||
const returnAnArray = obj => obj || [];
|
||||
const MIN_ROCKETCHAT_VERSION = '0.70.0';
|
||||
|
||||
const STATUSES = ['offline', 'online', 'away', 'busy'];
|
||||
|
||||
const RocketChat = {
|
||||
TOKEN_KEY,
|
||||
subscribeRooms,
|
||||
|
@ -95,7 +98,7 @@ const RocketChat = {
|
|||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
log('getServerInfo', e);
|
||||
log('err_get_server_info', e);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
|
@ -168,14 +171,7 @@ const RocketChat = {
|
|||
this.getCustomEmoji();
|
||||
this.getRoles();
|
||||
this.registerPushToken().catch(e => console.log(e));
|
||||
|
||||
if (this.activeUsersSubTimeout) {
|
||||
clearTimeout(this.activeUsersSubTimeout);
|
||||
this.activeUsersSubTimeout = false;
|
||||
}
|
||||
this.activeUsersSubTimeout = setTimeout(() => {
|
||||
this.sdk.subscribe('activeUsers');
|
||||
}, 5000);
|
||||
this.getUserPresence();
|
||||
},
|
||||
connect({ server, user }) {
|
||||
database.setActiveDB(server);
|
||||
|
@ -213,6 +209,10 @@ const RocketChat = {
|
|||
|
||||
this.sdk.onStreamData('connected', () => {
|
||||
reduxStore.dispatch(connectSuccess());
|
||||
const { isAuthenticated } = reduxStore.getState().login;
|
||||
if (isAuthenticated) {
|
||||
this.getUserPresence();
|
||||
}
|
||||
});
|
||||
|
||||
this.sdk.onStreamData('close', () => {
|
||||
|
@ -220,6 +220,25 @@ const RocketChat = {
|
|||
});
|
||||
|
||||
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
|
||||
|
||||
this.sdk.onStreamData('stream-notify-logged', protectedFunction((ddpMessage) => {
|
||||
const { eventName } = ddpMessage.fields;
|
||||
if (eventName === 'user-status') {
|
||||
const userStatus = ddpMessage.fields.args[0];
|
||||
const [id, username, status] = userStatus;
|
||||
if (username) {
|
||||
database.memoryDatabase.write(() => {
|
||||
try {
|
||||
database.memoryDatabase.create('activeUsers', {
|
||||
id, username, status: STATUSES[status]
|
||||
}, true);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
register(credentials) {
|
||||
|
@ -386,7 +405,7 @@ const RocketChat = {
|
|||
database.create('messages', message, true);
|
||||
});
|
||||
} catch (e) {
|
||||
log('resendMessage error', e);
|
||||
log('err_resend_message', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -472,19 +491,6 @@ const RocketChat = {
|
|||
return setting;
|
||||
});
|
||||
},
|
||||
parseEmojis: emojis => emojis.reduce((ret, item) => {
|
||||
ret[item.name] = item.extension;
|
||||
item.aliases.forEach((alias) => {
|
||||
ret[alias.value] = item.extension;
|
||||
});
|
||||
return ret;
|
||||
}, {}),
|
||||
_prepareEmojis(emojis) {
|
||||
emojis.forEach((emoji) => {
|
||||
emoji.aliases = emoji.aliases.map(alias => ({ value: alias }));
|
||||
});
|
||||
return emojis;
|
||||
},
|
||||
deleteMessage(message) {
|
||||
const { _id, rid } = message;
|
||||
// RC 0.48.0
|
||||
|
@ -511,6 +517,9 @@ const RocketChat = {
|
|||
// RC 0.59.0
|
||||
return this.sdk.post('chat.pinMessage', { messageId: message._id });
|
||||
},
|
||||
reportMessage(messageId) {
|
||||
return this.sdk.post('chat.reportMessage', { messageId, description: 'Message reported by user' });
|
||||
},
|
||||
getRoom(rid) {
|
||||
const [result] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||
if (!result) {
|
||||
|
@ -518,12 +527,12 @@ const RocketChat = {
|
|||
}
|
||||
return Promise.resolve(result);
|
||||
},
|
||||
async getPermalink(message) {
|
||||
async getPermalinkMessage(message) {
|
||||
let room;
|
||||
try {
|
||||
room = await RocketChat.getRoom(message.rid);
|
||||
} catch (e) {
|
||||
log('Rocketchat.getPermalink', e);
|
||||
log('err_get_permalink', e);
|
||||
return null;
|
||||
}
|
||||
const { server } = reduxStore.getState().server;
|
||||
|
@ -534,6 +543,15 @@ const RocketChat = {
|
|||
}[room.t];
|
||||
return `${ server }/${ roomType }/${ room.name }?msg=${ message._id }`;
|
||||
},
|
||||
getPermalinkChannel(channel) {
|
||||
const { server } = reduxStore.getState().server;
|
||||
const roomType = {
|
||||
p: 'group',
|
||||
c: 'channel',
|
||||
d: 'direct'
|
||||
}[channel.t];
|
||||
return `${ server }/${ roomType }/${ channel.name }`;
|
||||
},
|
||||
subscribe(...args) {
|
||||
return this.sdk.subscribe(...args);
|
||||
},
|
||||
|
@ -695,6 +713,13 @@ const RocketChat = {
|
|||
// RC 0.51.0
|
||||
return this.sdk.methodCall('setAvatarFromService', data, contentType, service);
|
||||
},
|
||||
async getUseMarkdown() {
|
||||
const useMarkdown = await AsyncStorage.getItem(MARKDOWN_KEY);
|
||||
if (useMarkdown === null) {
|
||||
return true;
|
||||
}
|
||||
return JSON.parse(useMarkdown);
|
||||
},
|
||||
async getSortPreferences() {
|
||||
const prefs = await AsyncStorage.getItem(SORT_PREFS_KEY);
|
||||
return JSON.parse(prefs);
|
||||
|
@ -769,9 +794,9 @@ const RocketChat = {
|
|||
toggleFollowMessage(mid, follow) {
|
||||
// RC 1.0
|
||||
if (follow) {
|
||||
return this.sdk.methodCall('followMessage', { mid });
|
||||
return this.sdk.post('chat.followMessage', { mid });
|
||||
}
|
||||
return this.sdk.methodCall('unfollowMessage', { mid });
|
||||
return this.sdk.post('chat.unfollowMessage', { mid });
|
||||
},
|
||||
getThreadsList({ rid, count, offset }) {
|
||||
// RC 1.0
|
||||
|
@ -784,6 +809,42 @@ const RocketChat = {
|
|||
return this.sdk.get('chat.syncThreadsList', {
|
||||
rid, updatedSince
|
||||
});
|
||||
},
|
||||
async getUserPresence() {
|
||||
const serverVersion = reduxStore.getState().server.version;
|
||||
|
||||
// if server is lower than 1.1.0
|
||||
if (semver.lt(semver.coerce(serverVersion), '1.1.0')) {
|
||||
if (this.activeUsersSubTimeout) {
|
||||
clearTimeout(this.activeUsersSubTimeout);
|
||||
this.activeUsersSubTimeout = false;
|
||||
}
|
||||
this.activeUsersSubTimeout = setTimeout(() => {
|
||||
this.sdk.subscribe('activeUsers');
|
||||
}, 5000);
|
||||
} else {
|
||||
const params = {};
|
||||
if (this.lastUserPresenceFetch) {
|
||||
params.from = this.lastUserPresenceFetch.toISOString();
|
||||
}
|
||||
|
||||
// RC 1.1.0
|
||||
const result = await this.sdk.get('users.presence', params);
|
||||
if (result.success) {
|
||||
this.lastUserPresenceFetch = new Date();
|
||||
database.memoryDatabase.write(() => {
|
||||
result.users.forEach((item) => {
|
||||
try {
|
||||
item.id = item._id;
|
||||
database.memoryDatabase.create('activeUsers', item, true);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.sdk.subscribe('stream-notify-logged', 'user-status');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const formatAttachmentUrl = (attachmentUrl, userId, token, server) => (
|
||||
encodeURI(attachmentUrl.includes('http') ? attachmentUrl : `${ server }${ attachmentUrl }?rc_uid=${ userId }&rc_token=${ token }`)
|
||||
);
|
|
@ -1,13 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ViewPropTypes } from 'react-native';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import scrollPersistTaps from '../utils/scrollPersistTaps';
|
||||
|
||||
export default class KeyboardView extends React.PureComponent {
|
||||
static propTypes = {
|
||||
style: ViewPropTypes.style,
|
||||
contentContainerStyle: ViewPropTypes.style,
|
||||
style: PropTypes.any,
|
||||
contentContainerStyle: PropTypes.any,
|
||||
keyboardVerticalOffset: PropTypes.number,
|
||||
scrollEnabled: PropTypes.bool,
|
||||
children: PropTypes.oneOfType([
|
||||
|
|
|
@ -46,6 +46,12 @@ export default class RoomItem extends React.Component {
|
|||
avatarSize: 48
|
||||
}
|
||||
|
||||
// Making jest happy: https://github.com/facebook/react-native/issues/22175
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { lastMessage, _updatedAt } = this.props;
|
||||
const oldlastMessage = lastMessage;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Text, View, StyleSheet, ViewPropTypes
|
||||
} from 'react-native';
|
||||
import { Text, View, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Avatar from '../containers/Avatar';
|
||||
|
@ -70,7 +68,7 @@ UserItem.propTypes = {
|
|||
onPress: PropTypes.func.isRequired,
|
||||
testID: PropTypes.string.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
style: ViewPropTypes.style,
|
||||
style: PropTypes.any,
|
||||
icon: PropTypes.string
|
||||
};
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import * as types from '../constants/types';
|
||||
|
||||
const initialState = {
|
||||
customEmojis: {}
|
||||
};
|
||||
|
||||
|
||||
export default function customEmojis(state = initialState.customEmojis, action) {
|
||||
if (action.type === types.SET_CUSTOM_EMOJIS) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -8,8 +8,8 @@ import server from './server';
|
|||
import selectedUsers from './selectedUsers';
|
||||
import createChannel from './createChannel';
|
||||
import app from './app';
|
||||
import customEmojis from './customEmojis';
|
||||
import sortPreferences from './sortPreferences';
|
||||
import markdown from './markdown';
|
||||
|
||||
export default combineReducers({
|
||||
settings,
|
||||
|
@ -21,6 +21,6 @@ export default combineReducers({
|
|||
createChannel,
|
||||
app,
|
||||
rooms,
|
||||
customEmojis,
|
||||
sortPreferences
|
||||
sortPreferences,
|
||||
markdown
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue