From 557e485613fa07ef78e5b20fa757bfef5559cba7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Apr 2018 16:34:03 -0300 Subject: [PATCH] Beta (#265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .circleci/changelog.sh | 0 .circleci/config.yml | 5 +- README.md | 10 +- __tests__/__snapshots__/RoomItem.js.snap | 75 +- .../__snapshots__/Storyshots.test.js.snap | 237 +- android/app/build.gradle | 17 +- android/app/proguard-rules.pro | 25 +- .../src/debug/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 8614 bytes .../src/debug/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 4456 bytes .../debug/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 12222 bytes .../debug/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 23056 bytes .../debug/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 35420 bytes android/app/src/debug/res/values/colors.xml | 1 + android/app/src/debug/res/values/strings.xml | 5 + android/app/src/debug/res/values/styles.xml | 9 + android/app/src/main/AndroidManifest.xml | 4 +- app/ReactotronConfig.js | 2 + app/actions/actionsTypes.js | 22 +- app/actions/createChannel.js | 48 - app/actions/mentionedMessages.js | 12 +- app/actions/messages.js | 4 +- app/actions/pinnedMessages.js | 12 +- app/actions/roomFiles.js | 11 +- app/actions/selectedUsers.js | 29 + app/actions/snippetedMessages.js | 11 +- app/actions/starredMessages.js | 11 +- app/constants/colors.js | 2 + app/containers/ActivityIndicator.js | 12 + app/containers/Avatar.js | 2 +- app/containers/Banner.js | 24 +- app/containers/Button/index.js | 85 + app/containers/CloseModalButton.js | 36 + app/containers/EmojiPicker/index.js | 6 +- app/containers/Loading.js | 103 + app/containers/MessageBox/index.js | 12 +- app/containers/MessageBox/styles.js | 7 +- app/containers/TextInput.js | 65 +- app/containers/Typing.js | 19 +- app/containers/message/Audio.js | 15 +- app/containers/message/Emoji.js | 4 + app/containers/message/Image.js | 53 +- app/containers/message/Markdown.js | 248 +- app/containers/message/ReactionsModal.js | 5 + app/containers/message/User.js | 13 +- app/containers/message/Video.js | 16 +- app/containers/message/index.js | 250 +- app/containers/message/styles.js | 32 +- app/containers/routes/AuthRoutes.js | 23 +- app/containers/routes/NavigationService.js | 7 +- app/containers/routes/PublicRoutes.js | 144 +- app/lib/createStore.js | 7 +- app/lib/ddp.js | 236 +- app/lib/methods/getCustomEmojis.js | 23 + app/lib/methods/getPermissions.js | 22 + app/lib/methods/getRooms.js | 51 + app/lib/methods/getSettings.js | 24 + app/lib/methods/helpers/buildMessage.js | 7 + .../helpers/mergeSubscriptionsRooms.js | 51 + app/lib/methods/helpers/normalizeMessage.js | 31 + app/lib/methods/helpers/parseUrls.js | 14 + app/lib/methods/helpers/protectedFunction.js | 12 + app/lib/methods/helpers/rest.js | 40 + app/lib/methods/helpers/toQuery.js | 3 + app/lib/methods/loadMessagesForRoom.js | 68 + app/lib/methods/loadMissedMessages.js | 60 + app/lib/methods/readMessages.js | 33 + app/lib/methods/sendMessage.js | 72 + app/lib/methods/subscriptions/room.js | 71 + app/lib/methods/subscriptions/rooms.js | 58 + app/lib/realm.js | 61 +- app/lib/rocketchat.js | 536 +- app/presentation/RoomItem.js | 62 +- app/reducers/createChannel.js | 19 +- app/reducers/index.js | 2 + app/reducers/mentionedMessages.js | 13 +- app/reducers/pinnedMessages.js | 11 +- app/reducers/roomFiles.js | 13 +- app/reducers/selectedUsers.js | 30 + app/reducers/server.js | 14 +- app/reducers/snippetedMessages.js | 13 +- app/reducers/starredMessages.js | 11 +- app/sagas/connect.js | 54 +- app/sagas/createChannel.js | 1 + app/sagas/hello.js | 14 - app/sagas/index.js | 2 - app/sagas/init.js | 25 +- app/sagas/login.js | 156 +- app/sagas/mentionedMessages.js | 29 +- app/sagas/messages.js | 18 +- app/sagas/pinnedMessages.js | 29 +- app/sagas/roomFiles.js | 29 +- app/sagas/rooms.js | 96 +- app/sagas/selectServer.js | 39 +- app/sagas/snippetedMessages.js | 29 +- app/sagas/starredMessages.js | 29 +- {static => app/static}/images/logo.png | Bin .../static}/images/logo_with_text.png | Bin app/static/images/planet.png | Bin 0 -> 20718 bytes app/utils/throttle.js | 6 +- app/views/CreateChannelView.js | 98 +- app/views/ForgotPasswordView.js | 85 +- app/views/ListServerView.js | 168 +- app/views/LoginSignupView.js | 334 + app/views/LoginView.js | 405 +- app/views/MentionedMessagesView/index.js | 50 +- app/views/NewServerView.js | 98 +- app/views/PinnedMessagesView/index.js | 50 +- app/views/RegisterView.js | 224 +- app/views/RoomActionsView/index.js | 196 +- app/views/RoomFilesView/index.js | 56 +- app/views/RoomInfoEditView/index.js | 24 +- app/views/RoomInfoView/index.js | 3 +- app/views/RoomMembersView/index.js | 121 +- app/views/RoomMembersView/styles.js | 6 +- app/views/RoomView/DateSeparator.js | 2 +- app/views/RoomView/Header/index.js | 99 +- app/views/RoomView/Header/styles.js | 34 +- app/views/RoomView/ListView.js | 39 +- app/views/RoomView/UnreadSeparator.js | 2 +- app/views/RoomView/index.js | 24 +- app/views/RoomView/styles.js | 2 +- app/views/RoomsListView/Header/index.js | 61 +- app/views/RoomsListView/Header/styles.js | 28 +- app/views/RoomsListView/index.js | 41 +- app/views/RoomsListView/styles.js | 4 +- app/views/SearchMessagesView/index.js | 131 + app/views/SearchMessagesView/styles.js | 25 + ...electUsersView.js => SelectedUsersView.js} | 192 +- app/views/SnippetedMessagesView/index.js | 76 +- app/views/StarredMessagesView/index.js | 50 +- app/views/Styles.js | 94 +- app/views/View.js | 3 + package-lock.json | 20387 ++++++++++++++++ package.json | 63 +- yarn.lock | 10242 -------- 135 files changed, 24794 insertions(+), 12680 deletions(-) mode change 100644 => 100755 .circleci/changelog.sh create mode 100755 android/app/src/debug/res/mipmap-hdpi/ic_launcher.png create mode 100755 android/app/src/debug/res/mipmap-mdpi/ic_launcher.png create mode 100755 android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png create mode 100755 android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/debug/res/values/colors.xml create mode 100644 android/app/src/debug/res/values/strings.xml create mode 100644 android/app/src/debug/res/values/styles.xml create mode 100644 app/actions/selectedUsers.js create mode 100644 app/containers/ActivityIndicator.js create mode 100644 app/containers/Button/index.js create mode 100644 app/containers/CloseModalButton.js create mode 100644 app/containers/Loading.js create mode 100644 app/lib/methods/getCustomEmojis.js create mode 100644 app/lib/methods/getPermissions.js create mode 100644 app/lib/methods/getRooms.js create mode 100644 app/lib/methods/getSettings.js create mode 100644 app/lib/methods/helpers/buildMessage.js create mode 100644 app/lib/methods/helpers/mergeSubscriptionsRooms.js create mode 100644 app/lib/methods/helpers/normalizeMessage.js create mode 100644 app/lib/methods/helpers/parseUrls.js create mode 100644 app/lib/methods/helpers/protectedFunction.js create mode 100644 app/lib/methods/helpers/rest.js create mode 100644 app/lib/methods/helpers/toQuery.js create mode 100644 app/lib/methods/loadMessagesForRoom.js create mode 100644 app/lib/methods/loadMissedMessages.js create mode 100644 app/lib/methods/readMessages.js create mode 100644 app/lib/methods/sendMessage.js create mode 100644 app/lib/methods/subscriptions/room.js create mode 100644 app/lib/methods/subscriptions/rooms.js create mode 100644 app/reducers/selectedUsers.js delete mode 100644 app/sagas/hello.js rename {static => app/static}/images/logo.png (100%) rename {static => app/static}/images/logo_with_text.png (100%) create mode 100644 app/static/images/planet.png create mode 100644 app/views/LoginSignupView.js create mode 100644 app/views/SearchMessagesView/index.js create mode 100644 app/views/SearchMessagesView/styles.js rename app/views/{SelectUsersView.js => SelectedUsersView.js} (57%) create mode 100644 package-lock.json delete mode 100644 yarn.lock diff --git a/.circleci/changelog.sh b/.circleci/changelog.sh old mode 100644 new mode 100755 diff --git a/.circleci/config.yml b/.circleci/config.yml index 95ae45cbd..94faa75cd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,7 @@ jobs: - run: name: Test command: | - npm test + npm run test - run: name: Codecov @@ -151,9 +151,7 @@ jobs: name: Install NPM modules command: | rm -rf node_modules - # npm install --save react-native@0.51 npm install - # npm install react-native - run: name: Fix known build error @@ -227,6 +225,7 @@ workflows: filters: branches: only: + - beta - develop - master # - ios-testflight: diff --git a/README.md b/README.md index 321649b79..e764f1a20 100644 --- a/README.md +++ b/README.md @@ -22,20 +22,20 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react $ git clone git@github.com:RocketChat/Rocket.Chat.ReactNative.git $ cd Rocket.Chat.ReactNative $ npm install -g react-native-cli - $ yarn + $ npm install ``` - Configuration ```bash - $ yarn fabric-ios --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" - $ yarn fabric-android --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" + $ npm run fabric-ios --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" + $ npm run fabric-android --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" ``` - Run application ```bash - $ yarn ios + $ npm run ios ``` ```bash - $ yarn android + $ npm run android ``` # Storybook diff --git a/__tests__/__snapshots__/RoomItem.js.snap b/__tests__/__snapshots__/RoomItem.js.snap index 81cea847b..7322a46b6 100644 --- a/__tests__/__snapshots__/RoomItem.js.snap +++ b/__tests__/__snapshots__/RoomItem.js.snap @@ -46,7 +46,7 @@ exports[`render channel 1`] = ` }, Object { "backgroundColor": "#00BCD4", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -147,10 +147,8 @@ exports[`render channel 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -206,7 +204,7 @@ exports[`render no icon 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -307,10 +305,8 @@ exports[`render no icon 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -366,7 +362,7 @@ exports[`render private group 1`] = ` }, Object { "backgroundColor": "#FF9800", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -467,10 +463,8 @@ exports[`render private group 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -527,7 +521,7 @@ exports[`render unread +999 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -561,13 +555,16 @@ exports[`render unread +999 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -639,10 +636,8 @@ exports[`render unread +999 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -721,7 +716,7 @@ exports[`render unread 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -755,13 +750,16 @@ exports[`render unread 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -833,10 +831,8 @@ exports[`render unread 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -915,7 +911,7 @@ exports[`renders correctly 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -949,13 +945,16 @@ exports[`renders correctly 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1027,10 +1026,8 @@ exports[`renders correctly 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 86e9a4e19..a735dc7f5 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -12,7 +12,7 @@ exports[`Storyshots Avatar avatar 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 25, "width": 25, }, @@ -48,7 +48,7 @@ exports[`Storyshots Avatar avatar 1`] = ` }, Object { "backgroundColor": "#9C27B0", - "borderRadius": 4, + "borderRadius": 2, "height": 40, "width": 40, }, @@ -84,7 +84,7 @@ exports[`Storyshots Avatar avatar 1`] = ` }, Object { "backgroundColor": "#9C27B0", - "borderRadius": 4, + "borderRadius": 2, "height": 30, "width": 30, }, @@ -198,7 +198,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#8BC34A", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -232,13 +232,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -310,10 +313,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -364,7 +365,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#8BC34A", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -398,13 +399,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -480,10 +484,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -534,7 +536,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#8BC34A", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -568,13 +570,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -646,10 +651,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -723,7 +726,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -757,13 +760,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -839,10 +845,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -916,7 +920,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -950,13 +954,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1028,10 +1035,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1105,7 +1110,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1139,13 +1144,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1217,10 +1225,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1294,7 +1300,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1328,13 +1334,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1406,10 +1415,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1483,7 +1490,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1517,13 +1524,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1595,10 +1605,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1672,7 +1680,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#E91E63", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1706,13 +1714,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1784,10 +1795,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -1838,7 +1847,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#9C27B0", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1872,13 +1881,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1950,10 +1962,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -2004,7 +2014,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": undefined, - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -2038,13 +2048,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -2116,10 +2129,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> diff --git a/android/app/build.gradle b/android/app/build.gradle index 1183adc62..7efa4b2a2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -82,12 +82,12 @@ apply from: "../../node_modules/react-native/react.gradle" * Upload all the APKs to the Play Store and people will download * the correct one based on the CPU architecture of their device. */ -def enableSeparateBuildPerCPUArchitecture = false +def enableSeparateBuildPerCPUArchitecture = true /** * Run Proguard to shrink the Java bytecode in release builds. */ -def enableProguardInReleaseBuilds = false +def enableProguardInReleaseBuilds = true android { compileSdkVersion 25 @@ -98,11 +98,12 @@ android { minSdkVersion 16 targetSdkVersion 22 versionCode VERSIONCODE as Integer - versionName "1.1" + versionName "1" ndk { abiFilters "armeabi-v7a", "x86" } } + signingConfigs { release { if (project.hasProperty('KEYSTORE')) { @@ -123,10 +124,16 @@ android { } buildTypes { release { - minifyEnabled enableProguardInReleaseBuilds - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" signingConfig signingConfigs.release + shrinkResources enableProguardInReleaseBuilds + zipAlignEnabled enableProguardInReleaseBuilds + minifyEnabled enableProguardInReleaseBuilds + useProguard enableProguardInReleaseBuilds + setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro']) } + debug { + applicationIdSuffix ".debug" + } } // applicationVariants are e.g. debug, release applicationVariants.all { variant -> diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 6e8516c8d..072b6d4c9 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -18,7 +18,7 @@ # Disabling obfuscation is useful if you collect stack traces from production crashes # (unless you are using a system that supports de-obfuscate the stack traces). --dontobfuscate +# -dontobfuscate # React Native @@ -49,6 +49,7 @@ -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } -dontwarn com.facebook.react.** +-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; } # TextLayoutBuilder uses a non-public Android constructor within StaticLayout. # See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details. @@ -68,3 +69,25 @@ -dontwarn java.nio.file.* -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn okio.** + +# Fresco +# Keep our interfaces so they can be used by other ProGuard rules. +# See http://sourceforge.net/p/proguard/bugs/466/ +-keep,allowobfuscation @interface com.facebook.soloader.DoNotOptimize + +# Do not strip any method/class that is annotated with @DoNotOptimize +-keep @com.facebook.soloader.DoNotOptimize class * +-keepclassmembers class * { + @com.facebook.soloader.DoNotOptimize *; +} + +# Keep native methods +-keepclassmembers class * { + native ; +} + +# For Fabric to properly de-obfuscate your crash reports, you need to remove this line from your ProGuard config: + -printmapping mapping.txt + +-dontwarn javax.annotation.** +-dontwarn com.facebook.infer.** diff --git a/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..ea0f58ac03eaac2f9d55072164775f8b9dda8274 GIT binary patch literal 8614 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 literal 0 HcmV?d00001 diff --git a/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..f98ca51c4deb86324602bb0ad285f469988e81eb GIT binary patch literal 4456 zcmV-u5tr_XP)q(Uk?3E99v2t-g3yIERnaT)DaPB$Ye(l|6SgBv0)J#N$3+B&%4 z0?HW0jn;MpoDr0fO$C7nWD!C__PtV-+N<6+=iXNpDghK5{A2Dpb&`7Z?tS0yTYum8 z2>zch@-xam{A~5-c-A=qaM@oET1+GJrVNN=nk194GEl6l#HoPTLB%E*0F)jcsOKTq z*mY#P*wOs;mV3$rXAA0V0FW!*_FU?d`CCMpToaO1GnJL8S&jXTe;;;@el_w4>Uia2 z8^V=a=cjK?y!EGm`bPke*>Ch;<%90wfQSku<`{ARI2`~r!GRL*;Rs+BlmiwLeR%sV zX`6m3ke>wL>eo8P3<%a|{UR=-f)5uqrs$I`@FNzX=WqZlI%TU_cHh3^*8I9NwZfSK zAhX`+yVQUtbpS=h@ygndwuYSbpYW9<+Tao zEBhBZ7XJ{49|Az8RrdeItLT;wx(O9T7Mbu#HV%hBuY#xB2b0Z&j3Otp#*73f@aSrf zps`+rtndsp&nYM@(ZiXkgDgsDt!Y6=LoXtMDD2K8jGt8wPhSWJ_XWTiZOjsJ7qfBU zCl4%2yAObJDodOez_eF8ZuIfC*HuL&+F>eGMTTCGaKSH=(OKVtn%#{6Cor?q-wXy7 z=Uq|^M~)K#pNQ)3f*^#TbbY87*G;jT>V&aqlW~{Cm>obr1MpltTO>GalP})(2!oDMLNFF3Rbm$JK zI3%ayaN1b(nH`#s%Mw&6g8Z>g6r5j#XhdP!6m$f~zO6^g5f{K40YM)F4k%I-+4&ZX znNo=Fwl~4mCxf#%Ky;>fu7E&PpV8Ae<=~15HN&lNDggXN|KlSpjM?rWUh{?u4#Iu>5w>10GK#L-; zWiRggW))0|sHLI!iUl_P8s>CZKl)mHkuwEGy^4#bjf7dCjz{2^Iqccqf^byOgy(41 z*B-4%B2q_iD4$V`+HacC*%k$OJ^Nh)Vbm<40+vYEyu)`Ezn$nx!vW|fuk-8|!Ix>9 zw4hZZK}+MvoZ zoLPE|n>q@Ib{w=@WVren@%1q2O*4r29@+Z>YsMB+Le0$|GX$F2>T(?1;4aV+Ok zmrz4q+`01!RJf07X))1`!>q!Y&CodykC6< zB@}`Jf@9+@#McY&!RF7qpzsz3;DmxSAjDt=(HX!?!HYB3O57(wm=zjA}NG=zG zW-Fc;w*b|d6G12d%JO3LLPJ163`yVxeJHT;_PnnDeyw*1fb*ZKy;e-lrKU*;(m3Cd zk!(|ORn-P`fYuh;V_eP+C zx*(+(CqyNB3-sQm-yHqZq>rg_;}+n`pKp6gHQDbxkzi;5D6m=y3X1g@dwefut$qlC z(SU-LD`BmuI1^=`(LI!&^laFGwr8F}D9MiZAAJ?=f&=x3-SG5F30X@MmzRRiHr_Yp zZb~uY0CeRqc5hPj=F2pvrNYa|8>K^bo*p_eia8J70;{W=2~Yj~TrcWY9WT9vuF6Ur zJ%1Xu-2NYoPqow!prOte<479eBhld2ru#=q#nC>LZrA|*h!H~`c>Iw^;MuyBxgE_&*^4Rr1saww z$H1;#?ElO~ix>#HjxJsde^nLxKWF)JIDh-wA?JB^?nJ{~cj3eQU*YxAIWSuYrd%@) zolRbRf6&J=6d`J)>F(T20PZ*d+t{@Oy#z$BND&kl^T-%!!JbbJ!tagX@x9NXNOWQB zd+)IVmAD%2yAR$SJ6NVmyZ(CQ-l^q*`lU)$U}fM9JcyPmyt zDbjDaVJLvzyU}pR9r%a>D7yisB!VedmY}oIjk-gAaI7k;(dK2jSpZxo0KDAa3?d{e zas+mB2;~=Nz}@Y|!EHzJZwD%IabFFJUU&i4a_V{~7Ig_4eAGp_n>5xLU+u5GhOQT1#7m{W!{=jWqkL*M92pi= zZ99(s0RbH4MzZK`UY=hBpg#^Esq}^JLqum9MO{b<2T)YPBfG>2Z=WA&@2 z0hmXxYTd|Nl4mm7AVq+P4|$_Fv zf+Krd;2GdS^kzn@3GMy}f#hm>x&6;m?xLO=2f*juz47&o+aaW;qU@uOSV}va6^TU9-QEt@JMTc*xDk?~V#8I7u`Q(xt{yR_bt7wn z;=(h)-+wW`ckMjd7l?5H#6JGA{LDXYs3SaWnPMDBQDSXk7?P(G8GZG5cK`Dbj7F4i z*#eS4;HV2kqfr=*#vhE4P$&ey-w%)54d2e4P&aM_+S?H|nDEr7f5M)TQ)3!8#JV>f z2%S{KnXMv4x?h|-KGL}B8107=>9nor$zvM?d-~-=w5lpGIvv5-`mK0y|4T6D=cDv( zssL?Cl7z;_M)+G=Kr9v*>~;u(0FHHDv~gD;iy}e;15lfqfSMX$|9-&bV(GW7a2$3l zeh{C0mstAZaE9vZw2_wnrw+r8R(_od`gmy=22&YuW6|L9}rRsU@T^ zmTq5(DQ$a@e$!2Z1&jJ`b8|C7yLSVXl^XF%O@(Tt%{%QWRUjIrj?M&z#ArNZ5Y!}P z;p3c3;QH0CQ9SiL9Qd*Y?tU=dPkE5GVj{*Y5Inf)551KOsrJId0nj_0MXOqO@aE)_ zLG?0jBx7RJF5JCiC4?jk3M(t2SS)b6DO>D^`q^i|=FNaC!^`Pml?OY%!48aSx=Uw8 zlQ|7F$pzS#R)(&WJgB@5>A6X)sju1FfvygPRolcVU0k6`$aA#y{xLHFbjIgI!+T;= z*1!IJp>^D>=QQNnL@mqJh=;cj3!U3LXxC+Ra(;rxZU54$3DFSX~90tC314K!@IkIKmZCMsIg?> zje^-YmQkin9b^glr70+yq@~)115IeG^|0og2~K@GmJ%uRrNw*Vv^*Ty--WhTNef=q z$Y|ox)>Ink{I>0(36wZd?GMgNPc;`Zre3+?l9buYKIV9xSrNlAVGLVj08QK|8kx8%NKW0LnJ1dQvsmY>{++{?T_YhmpxAD zHLc9D-vuS;fliPy{f3Lt+0cW-yPC8D6PvUdOajJKq#-rag5W>^Ra_*+*c8Ch)`UE^glvFs8JvtkTs-m+wfX>zsf_@41WDaE)I^lN*@a>n~+Vp|O2oqjK zr1`+>UC&(cAk6{sNyLv?ApSdjdPdIuyH*);%C2J}t;moA{V?+bnDpy05cE1ok^)}f z*aQ9jJ~ULdpr#Ze}mvzSr*4Y4{d#O7)tkV`#zAHPqD8*?4 zd)4IcIUcIp_NwdMyZ#>2y6?x{jr}74;(Mq|(f%+s<+{f&HJ1GHYMm|p7vKdGOO2<| z{NQyuE~Er88VpIUmYty^yEb_?KJ-OQ4yqQKQyaQHc7Qjum5Dh&n1B2X_>|f u6`Wcr&WWjjG-~6*p7M?3j|J?he*ZrT?varB1BO&EQp;We@CgvS27}UVHOL1{5Dy>{sTmyXtzL80v^s$8 zslC24fP7Z~U<5z!#e-#Lhv(!W$#`-|Hk=d`mHdzdge3)%j&vG(>!}$1O#QT&(;-PH9f|-E zIhn)?eMEPwewDaKA^^jEZ!!Q*vjDqM+2=BgFAw!J{rQ8XGaJ55Fun}{1Yn-&M}PU} z;(%iLm0vWR;0r4TSx5B4A#Rk7#{!6UQ~;6?msOBcOv*Z&IQZCGKRf3QV5|5|OT{+{ zfH3K@+pY7?Tzauzaa`9I#57VbN*qfIvHugSDcenzfbpoHQ9%>{CQ<5UGXZHdYje3qz_AwiP zbUSKNovf6??q|B=7)8A~Gz>N9LjC8{S_sZKn#v*J7-{E&zm?m)_?nKIh7N`$W@~13}qD z9M9EF%#w~pv^c_yA|+#;CrKidIH8N;l4wWVHbG1?iBGq@@z70sR^2|N8tWqp#Mc3U z;p~-F)1NGn;JWgOW;W7A!D6wSH84#@4w`O zWtCM&S{jZt0H*U_>Aa*@ay;SAJF!101yu9GWBWW9s6aaJ{!{&R-mlH3QxK@$&kfywgL?JB4}%lpsR}mAw;EtI5c*6k^xq^ zU6Ey&%s_Uo5%~ocI8uzs#x}tx>+1|)-<~kSq7e{CLSi83?~m+4o=sfc{OWHnJMiwK zgI@)NuL6L8k`l{VcW!yLBWV1gOjJp*WK7$V8c<{iiiGm%He}{lVqC8%kVJ_Q*I?oV zin9p>M1L3!bs_ZitHjWVl4v*?%gfxYg^Y|$BPLFuXzk zgBR6R5hx}b1O|dZAExl#RL?or8hPW>TW9_R&CQ1`35N>+9cMo0&wY=#g-kz}$yz3I zzNYU909iy@nt*9@Qlby*>+D7CroHfchuCK}mmOIXvXNPw2b-Jri#{s@sl7Rf#s(RY zhyZ3lm>9&YG*dT<0<&4cq$w6;%UZ+KsxV`w!;B{8Qim9?!KclY`a+; zPVGd~3o;a0f+9(nInROAbk6+&-ypWVwFLnR_yv{oRiMl!2^obROqyE`XSzF%fP>yJ zYO6!=4UihrKp?u4q{&G22q>9qkE$D$S>fyGMctPD=9f|6Tj@+&d4)Fdq&8rgNXIJHNx2{c42t z&TyR1o}8;5NFpp28AqR#7A0oQ=T&If+eI!3BP26=QBEaJmLW(1OqiC3$;V8Gfu2$| zEn-;0fxUiobdnyR&-sx#`6f&$x5H?RJ(47fs9v`VEeCo59XA7-@h9ozYHQN8G?X4Q zm0d?x6nwh68j@^;!IcTYWQzfy-l$$8%tk@U47Xpr?!QW218_(Qy+Z}Sc; zH~1rhS`5+nOrr&NGZ|MB_sbHpvkfSpmC6W3x_8T)TQE2vK`>B&ta3Bybd{5dC=5U; z0;fa9^o6tF%1Vza93s%c{XR4|a*3ZX*@g*I>@m?-6!f-qVb9v#2u7?7Xh8*A5sWJH zbG4~4Ns7yW%5!GJWHB)ic79rqu8uGSqZx*j3HWLTxGHIHAhuy5-s@{A}N8gm;j0LNIIYW&Al4UcoVqn8d8Kw+niG;BBL;q zf!SW)gFPGD0iy*1fo?Ft;P5~&T9UjXy*kP@4L!d8ik#~K(8DL%?j&N27Xag;hqoLz zkUDj3SW;pv$V6Dd%;qof;~g)ez}L+6kS@EKbj8FpB!C`kDt2T{#Rn5kM1!kJwkYSQ*p!s-cx7IDKtc^4SW^^w!5$Iub8E4Ds16>gNMy2R_L=8gEWs1fSI% z)zM@}Ktz&67z{FIo>Gd8qU<;#^fq^4=f^dW6jC*2MmPpSRQIcDPRovPOU2ck$R(hB zZYGK<3fS#<5&qrW!w@KWx{nw@6LwC1-tRQVX0GLmE zy5XvBQ~D!{qRVQk0S_mR1Y>_I{(HmynCWXA9`O-~Pn@pNgx<-lwm5U}T*Zahn_j}t zMXEv~=R!i+yh0RA&EvTu9RcV(&DHIw+1dg{APLYomlK`{r>`kTIantF<#RGoSW&?C z^L7vbjZBqs2~f{NF%Jcf_Ec^Y5}uYGlj4J{NbA*~|El~M0O8S8(O3W&Jg5B7J?*j= z_YFygoVa2Nb9Tr`lAkY|GvHeO!T^KA9jA_T!VITLZaw(a4`vN)UE4pJS2Pv? zmW5B&|Dwm7^-y9ZOUr6936Mm{Vi=IZ$Zp+*UvGO786wp$V(MiiTcFPzCGRg_UG7|@ zMEof8HXtQ3G)!6`9zG&=voiT+UUQ3V?Pw6;y-G9aTq(kDD28UG~K_m!XnuQcOhy# z#n_lP4+m2xV8H6)QX%tH3E6G`nXf5iJ+Pyb+7lEQ>~=V3%z$muBsgcyf_?I2 z2qqJZ4hM`*C;NLi&R0Sqhyw#q!eK~5L-1|fh)`1#26yaWz_6ezQBea38qC-;VJ51t z_!YcG#f;FK-r9wMK?9QjrvE9L)@_W5)P!Cv6Hk)hV)2K@WCnwvIQPDO+?Nmkbi3*i z#hqt30O&L8f|U(-_$}!-#)v<%GsP~(vYD#Cu1difyH??n+IM0VswA?-jI_aGf%}9L zkh)+2T=VC{n34hm5n7jp9PaS3J|lo-zaQbQF7$o$5&A#+2*LVc;uVqzmj_>5@N-n1 zd^SG%%a@Rib{@mR1=Gpi3*7gU(-fl zkh&V$yBA%5{VV!E`GiSJTwjtPpr&Fz?#R0kK^p~J6i=u&fTg8Lg(j1Y3$j+NBS}c% zzyYuek=m+Xq|XPCWQ>Ocz$x^lS`Ce69#do4K_5O?YZaBzjiH1ed)JGXD;rYzZ7lmLY}pa zrZe;%6sktkbWoI z)leb;O#M_o(Yh*Zah)+Hb-3b-Ce9271?lMqLMsaTx`NohyAOl?5!F7Z{Y2~n$pEkhfa-sovhSIt zoAg1Meovr8Z+Nt71gE0 z@g;2u%B)%68RCxjMkr{jzx7u1tyvR2Wb^#*W5bopVRpn9gW4JguzzPi zA|jav9S8|AjM3k5cinB3_voYIL;#$#9%=Z4>`4D{az=}*9HStkY?=+3xiR9)qJ);+ z2T=L?^H|*UMf8R<&p#i9S6|IbdjEo3sI?XQF25XNUmv^sL8A@V&c6zor_4kCv`H`; zjnOcvtHqD1ZN03=l*KY?#x?rbl&To&T2+7RoF)2rKo0;>DP`{CE$>Gx?qlNU#ZnEt z@KRiALE)qr2Za0q?EQEfx|+OrY{xUm4tv?5Mz+@ZNkPlXmFW26AES@( zMEUu6KerND`F2b{btWvj(r9z77u7qd7NJ(q^fd?omauno&9A4Q1%QaeTrE8Slx|I0r_IU@DM6PedD zUhWTfb|Tc$!Y*r{G6g1gtSKv59V!SmHX_o~!*r~pq5?*nZA6?&rw8lmK&~d`k!b(% z27iItS|$Z@kh;i#4H?sM-|Wko&QD2EFmuriIMZD0JG##Hb?xZuCc{iM1g*c6-%r+IVI+zo{$~@3P;_up6cEq8LWlTh*zz8QM!3nWeQTMu~q@X!4P)5^98(pJi)i9 zZ8NT{dzW8HQBZW_jmSLr+;K5~--jQf<%uT{?&x6qJEl%W(Y4pYIemIE?PVEVfBPHb z$_S|jL12lyiMQSgTVY|+{^U+lwA=mKYmj_Cz-WYf!2(RY`DU2YV$EVB-6$pXs+TN5 zpt_npSBKSuU!L?^h-%i;?UFI)j9IWac;Aq>CxFds+gV+jmoj4{Hc7#j+8ax#u1-3Q zl`8cBkWx>b`E=iYSupV!fO$P!_1LTuDv$TTK-@0?2R2rreot4lJMGfC_i%3OMs|}1 zn+;PQcmOF!9W^EZfrAIJ|H>;N52+P2^$1%@2~%N-k}$A!D{61LDQW+dxpPtSzyqM9 zo__0Hvjz>f-_BFkx-ISOvr&BOtz+IFaWyl>@4wIXrIwmY7Tt${*`e~9j7eoKlpR|Z zm4eUTZ$uzKwm?l`ux|tyXGMs512{S6wnfi%@ z!r1!G4(huPdAapm#U`i0G-) zpYk>e27?X&UT#eHh?p@ylbwv(3%9=WB?i3_o`HperMq9j$z8kHg-is%V~@c$G3GOm zgan2E?ax0SO-<;#BuehTAMRt1jXSjA?z_?R=9@{UBdIHU;t8g{^jpof*J9xF&y)72 z>kt40+*n)lqmQEFMOvA}wlQdkpLwEdC}RHRVD z^%ICg2mneG#7O`V-7{llcQs5_-uJ0>J&0l0i~&@BFCPXq5mmeGAP(;C;9Q`{xUBl` zIIER{Jg|Za1z*ktVhs171KYOYz_Mk4ntx&UNd_w;1Le;>2TNvV++j?mP?V~v6g6c_ z#hdbHo?&$a{nqiyD`iSELn6fB-U*$z?Evo?30RMb?}tBuhWZ7AGSiU z*ti6cA!c;~0YZ4N_2BX{a`A_i08(I{8IQHCSF9-|aS|ZW1dT9KQv#TE(xfO+{JsG8 ztgD5udk`f()wpZd3;bym1?lIWiwQU0IIgTGO9^k=#?rx5`z7zkURE~Zv2r-f_V@4E z150)`oJSqS)SZ5%i=M2kJh^2H0{i!~WMInd*{rra(*F8m2OAo&|MJVB3{Wiv*y1U{ z|1P*5iqQgBnvJzHx;#8CqUYGLW*>U`jA}7o1wgX_LEpamo2M=s$p$blXvRYg?w%ZzHVour_o%!r~2)!h?-pg)A_O$X6)pczkYS^JbUtj(nAg?3w;6l4_p$W^O#NuRsTu%7JzMH; zIqC-^c|j}=sJO5C8AEFRc`7UGT283rAq0cuN5w2RvL@xkTEZn6t^3<>%F7R6Q8N{N zxE?R~#V?S3@x|lfwC@@U#Lc}eEofM}6iQngyS1yb} z?hP#v1!@|g7(nBGT`=ZtTitNSyrpU&#=55y12NZ>JGcJYmRojxtY3$>oUrCt*2Efy zG2%t(QK`r+)dxNbJPozD@XjSLs0NbSNJ^i0V%PxbyGHQD`|~T!hQQL++8=D~+Qli9X&0N|76n1g};kK#=+wNOP$v8?yuo`GmE@;ZN zt1q=yEP6D0L(x?$yp0MdH3qpXaD2Xk#a$_w{y~kzdPRX{cF^mf=V(i8AqMO)PgAUc5XR10Ef_x z@&!}j$j+pzDh3s$PYaPkm03g z7X<7)^WU)H-~S5=HTx z3-`l7O znvhEZfiJ#e{h%Zb1cJ962HB772_8%=FTkSpf5)sfuQRozZK)@mz|_&#+52;-;EAHh zxW2Qk4Z-#6fmN$G=d0U5dKTWj<{s2Iv(a4Dh>oTKNX*bu(=I1C_jc69Aj1%!k=G{9 z{T7aG-xDoQ8dFm-)%}z~`1i`1&Ns z20Lp-=X##68Hg^2()A4uR}&7`e{oOSBa0tVUC`+Iv0=I*kWz5L?I*auzwAwY?_;b& zrZ(7Ws~8CYZ3Rmt0MXaUJL9AB{NCMQ62Kw0&(NnP)K*8Yq_nzPL z-hhl_~;{`t1D?; zTgQYl+>*Wsompk-B0Y7Tn7ZIkOEX7HkJ0kuaNQ7q?C)!7x~1Y&T7<9m1+n36v?WS_ zW(PczZ~5$YYtgJFV|K==BViPpjHT8Nm^$*d;q z>gqs7QQJ%=6YO?7dr>lwYO!pWUX(R5|UZGHk5uDTvUqnI`q%hFj3~bo~Y}y1Q z)F0>^lF5u+i_XWUi>|^)|Ja8RW$c;CQCD56`TaPKk21&b#HaIyo3=mI_Q08UCF=j- z0HDuUM4WZ{we!=@yZs$Ot3QsWGvcIyI4lB$2Jx#;@4<0BH06tX6ImBsgraM%8TL#> zC?Z;YeLW)_8X7o}a&rOd9i=#8gyMcui#rjUN{IAcRRuIQ@^`hkp172SiY(0;h1mWd z%g{ReXjX3A@lg%B+QYm(Jz>QqU)`VB(W84T{dbD2`2KO*;=Z-dY}S<`qj7Gc1khP1 zzNV-tqvYQGZ<$i_D1AES#`5^o3eK{p5ftK zdwbE+(gN~IaPS~Nt{~}UB0klJ^Ya0x6N=3S!D4|zQeZYS*H{oq^+h<*gFy(ry->)o z`u#j(8b^@W@(@ZbAFyZQoy?ioQgSS2#k@16000kpNkl8gTU4)8NkL)!3HWUhLi4qe=iR)QeTNV(c@r+R@aD zf9vbZyZ*HF&$<+JT&wtS5QyRaCUMZANryTywg8#*Bvt$dkMG_uvMF$VDk0n%VkOVd3^pMk+a z>NcSKk8Zuzu<{dq0JcVqg0h0EU%M)0#%VO9ZsbkJ{hq;2-2CBfDDtVjp%h%qn}>os z?qDH38CP-z$=H%d!ZuPJl=MpQ!|AKNwoK%Q2aqZS$IbbqTIy}-XtH?FX7yladI>rl z+3>ouAz664I3sAZi;X87XB?A<{IXnjOTL~#eDP763V0OxH%2U#8qcs3G+`!+} zqD7orxpgZiz83nchmks3l{E*iPClL46q^{p^s-D;ROG`t=!0$02V*$M3}(cUf}^HWz8*v@PAC>9U&E`-C1UTo*%6d1(=9A1pPhyY)AHFJ4EX}s@Ln?nvzphZ z)%T3<<0HN_*w@{D_le(+_`4{Vtz|#swDvG(i(_)08v@euuX_3CuG#0_7I$%809D;Q z2?jBY%fEO6iw>xnP&ySgOD$Ofl<}4gwvmw~?`GF7fVvgbW-RS%mgq9s@qGRXSU2e; zHoHx$I~$ZRDi#zYe`0njeqMB1MkOAOCheO-XJX%O_3-0;!RBe{jGZd zB7DSP!Zv3iUYfKBEm>u}tf!5aVdHBg7?mLAoHh;V1$@kq(x8s$ zj5*e+rk{wYG7*(rB@v}ZxiE2BUIGBB4PdJzb>td>_>9MT8+SL~dHhd+Fa=&Ca{pKo zK;LUp;7LF8h7&T*UjApnr1qlot}RBC;|_d)%QvrtI}+bq6(>)l{1X(6sJ3V0&D=_C z&zlRGrjbMo8fs2m9Vy_+wqg2$VmMr}Ax(7Z@K6|48xNwpbqER@F{90j&<)z4!nkf; z5Lb!XBJ(gR=6Fz0%8l?~pC237@KFWYya@f8;5aVUfgy__ssF8ee&$>I1f`vc`-v#S za{qV$pnb>|P_IXN(G6?<*FO1}i(;z^`7(b52a(gb8M9ix!2I6*C6GH~; zHvXmk(I3*1RL$@IO4Cv_kjNu)n>`cr3vYP)RddF~GCtidHZ6ssUwNPlg0Bspfp%p2 zyOAC2Lq^z(Np2G|3$rnlnvPz3Dn8%Vh&Gc4y{;UDZEijkPt7j!@nULYs|q18?WCz` zm{6Vuqi()vM@N_9*scq_d^K{a>-yrFeGFZx-jRgO7{4Xa0Hv*+QA00GDD~@ z-$loZ#fW8dnlN>47SggEaY;Fv3s=3lfu=lQVtEEikDtzSLtL-G*B?Ul_FnkBG#pUV z`+A{QhxW0$Mi>riXMnE(vT~Mg5tY z;f+Iwyh>ah#h($N?W@0)fhxiUgw|;?$p> z5G7#E=6W35)5#NEOx;j&EoK}wL!V#lM#;?h?gLqpQL}L`8ms%DunuJ+q=c+oGiEHD z6|GOx^U9Kf#sfYatoA{YN&l73lCjBI#wABI9^odm=>AQFjB z>T7V*tGD#5x`CQjiTIRqIHEJ-G}fVt88z;wChAoYvj-#z1=s!aZu^v@ z&lQ-nq|I%P%?xFd5FB72c+vz^o;6eTLV&)`0UY?U3&A0pXkvibYD8w96Iq2RaCxk8 z>W0$H-SzF*x2YaMQPat)#Au~|<|T8wFIqk0p=ES%s~qq$m86IP%s4h5d8M&+!ZfEj6o{~vcZ-z_ zZ?Z-l@(!YIOD)>iC`yMq4KX&wQ1i%GFs06~Qz8MRXBbgZS&Y=&v|$T6lEm+kl5l#a z?rrYD_Rrd&(4Y?H0jc%gM9x=agc^3NZGZen*CR4ST)3BQ?6%Jmvgx1JRATtdq5VDK=8oqNYDG_Uv74k zEVzIbtW{5hfZ+835i$c|07k`+;^|q)pE>~+du$vq9U?_U^tE=QqqZGgEdvP4l!sAs zIc&)f&*KOJ&F>+is6m=8C88OAEi+616A+$EEApmfBfT&KRwsE>F>-_hA++sp!NI+~ z2!p!Yc=|!>?bLU15U_#T4XZn!`S-gJiIBl1wL{|vi1WXZ>FL^?Yn~Cgr(_d6Iaj@U zwR76{{!=j9`Qz!bqy&N#=!X&^HAoUv3GqbrL? zG(-fp%947BxQnWU{5($3IG5dtE=`~jSsOR3Rim^H1l41TyF0c}BsMsNsU)>V&0S?a z5L|0Ai@vKJ4Ll}M`#Iw}9yl+F16_muEwA0v_r`5+tHdX7hlqbzoUiBNF{wPp4s?f_ zVWyNts(tPS6&YvWbeA=^v|Jl8&$*I&D$Fwq4{=UqL{US0HfWoVvu6o3MsS)|sHz)n z`05xbVU5uI{OWbJzoQ_iPsQrHYIancHLXb;xw*20NXLQdzJK0(OJMt3G#Q<^p8R~x z;2sv|A4vc-Ng`P#7nMM8!l*M{FsK?`Ip|Y@^;=hW{`u+$G0;Vx4sksZ zpZxqo@^(gI;=?tVfvhmu1yV_F*U3xfx=*IY+)gj@awxGzZsxcm-0_0(bpW98 zvnCA$1gWJIQ*L=y+G)$qw@p6w0%KZUF3W-vLy|p+`>lbmgZMgidMN7MPLHx>V&BY-JOpeSAJZ>tJaul-B^yDL6~7$6lx zFCsd<(+!cXAF-e^UQYj}Y`{n$lY!QNAc>$C0b;jQEG%#xebMRmqS@aU?CvR1AAC4s zensN5rs1%k8i?z{4cp%z*tU92c>gCfA%yfgsTd+Wy=zAHkbaQoaKQMwB_L7ZYCw=R zCjFy-S?x2=DzX(Xs5IwJnP*BXoMW)3X0p{MW;Jac_k^RXDfa6~WLv}#3HIJ((^}L-FzD=Xf%n?O9cT!YD#;Nw3zMFzF%a? zpFYv($tyCtvxx3;C~iat@{!|Je;rU?RQ-fcCexpLX2gKF47n_f4vx(QZ<8#93j1UV2b8 z!V^H+ciR4%$E7De5tCjT@iidy|7-gk5nukWsXuI$`m3Dn$Q`Q@SpTj~!_y=nk$HF_ zA|?fTB5NG3?Q`Vr^edj}yCMPOc?^A&rg47aaT-S_zK`ep-^xD!2cRD#t2)L;7XSbN M07*qoM6N<$f+!JgkN^Mx literal 0 HcmV?d00001 diff --git a/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..1ee4de388c2295deeff51034e1f913a3854ec82b GIT binary patch literal 23056 zcmV)AK*Ya^P)ud@=%iF#^(F zocqbCpNs%L0njHP*sBrv1VDQ=e4m{B34pR=AlJAUpFk#k1pb!+B;GyqJB`p-<03$0 z1yi=){ZC-+e*r+|J1)CbIsC-MZkM;*Ascq5p=d6rtFl~iJLb!ZV~%d1-9=#eC20J&!CK;V}S>L5->Xt|> zyrr{w=Z^Ka9uu^H>0TmK#?nSJPylHGosCfN=OCr}nrje@8uc{SZo z2oyAf1QIrrmgyXCX4C$j@!=$}BndL0yKETo+A-v@p~o(1D-)6MYaN{}|6Xed9pGi&rK-%3%5^0*GmoTq0|J(55`)gh^juBOX^u_QxshV}pm zndqF*eKHU&6MvFm_#Dz;o&!4-HFE#+4_@;q-gty0l2CCUdLVri03>1QQrmG~yrm?2mxO@B zXi#G#w*_}?{MYiQzTJz>n^g{`4=s>B%m88%=d^q4Ym}n0bHlprw5X;m4#o|KO6!xe z-%KJbMtUqlyjMW5++b3q+aiH-oku|OIV9Di=SkeV91XhXp{4=bU!slZz#H;QWwkKXX`> zs$vO4N^zN#)EQ-AO#%qHK=6TEZ4*#oqd?Dp;jl?Yp55s2E80_fxZ|;x?>=kwhlL+~ z7yu;K)-H1_`RSj}h{@g`^vC47m})3YvOuzs;s0F#5%x&P+GXfPZmHR4gnzO2t{We2 zUvvL|T^jv20mMl1k=Osf!jeVDUKW(xH};1OH+hJKsxqZd7PTdQpJ~qIvbM&u!ua1N ztnx-{%bXU{gjAkI1?RF5GhegNEyePs=$(UWUjFrpyU*|YZ<6f)7JwwoKihu#b$1>V zcNW|{99NDHC4>Vox74xvU((o4nhN8ADl7IfBbr&zG9dtE=(%yv$v{MQyo(3r?=AS; zI4aNQkdSXv-*m)-xBv5@tKY=CPm(mEi;ZU-?o*e*e*-|o4V5z=?>;}GcyH;67*kb^ z!bjr0a{wqgEZq9aA$`ohT3<`}g|de&#xwhK@scq)QCg-bI`fK(>ENCW_D@<+*$**l%u>zh%s%y6&%7_*@L*3=IVGHc+m!MtwKYnGaW!Djgct@J@WLV) zmDP55dv{XUWR41G+b5&+LvWj{G80?Fnr$@(NzbTbyh0}k8z!Yc7F}b4n zY^bVn!e-A7p7G2M`n#s;2n99tbj2Bv!V&r{zn)~dZZ_4HNrXfYm+>+aNFI-b;u0H* zN^S6XO$jMLG2Tkde10`XAPSpHZA z21C~&D+*-0k|d{DB^2?bgofZy0v)Xh^bY_`LrO9~FGajZKPIQFTtYH`m7z2KMW89K zP>`2zgF-p3W2hM-&52kXx~f5vCD>g~$hOh%7L6pZeSI$mh8!>yXHuP+QZvW2SsvP< zK&^DCudlxQ?6U{fyb}JPK=eTXh=FI}ZRi>l zS;zD_E|gZd(k4YyH4Jw2qH}8#qQNlZHpG$aZYPSTRwBQ=2u_cSO$<%as0lTIXgH3Z zju<*RfmqBWu4EY*OafW0A#t`z!gJ#Qzq&OiFTE(*^)$iz~B80e; zq_-#jRM$TrzT*8yuj&6Fi1h~nAZ6*mJ1)=$0gnhWq1a zZ%bfskW6WspOmUdColvCQmY0O*+8JkhKedX@&dM`x=I78VIVv-g8tpz=xyo2aDNDD zf-EoD91aFl6O1mmgeeQEQL~^fO&TR)DmJ~_ivC^|vZnx&-33V!q&kaLmPMDl>{7f? zjy%%-?CpMt6dbts))4VxoHYE6M?dcR{) z4V|rFbhZHrm6KJABw{>=OGld>sI0c3yvhlO)0A5263##(nm~Jf6WZ!`BNmgP88#Ra zcK`_q!YA9jwhot5$LvFKY);7D0A#x>Mf@}Gor4T|?NYc< z3f}(umn-fhp1v>rMEepzGBCq_%Ka}KAM#W_HxTCxbyViMAeWZ66>7`0XW2@3It(0m zWFQS6v2YX(?`=W*t|3SYsf?6mLRM8OcE-sqT~F6ws3DYA2QclB`S1qvQfiIU8Ug^B z{oQRLG&k#r#pyS)S-)e7CDR(T15@f8D6e)x$xw{~kip&|?09Dr`a45_-3!Pz)62}I zP8#40omGdhL)TGST&}p=JhMO;b^IHqw z;*w~u9gX_}K+IfU^tF2nD~>q#-aggwIjXs8>>PWMRoLDC|Of%^LIKoQiQi zU4ty?n7)5GYUbB4t5S?%TSE`(S9hBLqD4dMj5rZVqDcTqmY^5f|<8>oXALLz2l)G^!vagO{cL z>9WBn)CX?)_YKG2h2CwEz2!rD3n0=V_R}71y{O+P_*F1rIL$}3=#pgR1TT9aexpW_ zAL&OyxF4m#PE-#yp)lMBcRUP-8iiAf!RIv07?)y$W_KWAcVIBAp(k#`ZciCHyrmd$ z7oy)4K-lhv#<@oF-VC1{I2=Hxg~!ctEdfQ2qiSXusutA3lkXeVQy~(JK~faBJf`GH z0mwjjAc)q@&1i4vLBeowiAcOjL`&1ZGs(e5n|r^7&x@W(S2>7O6qYzK|EO7TdYsAo z#li`!{AUB=#LXm|$>HcevK_M9&rJOE=S#!Ss9Fza)Mha*Cc{oA5M+~u8VdiTIwc?EhQP;N}b-g<8?jjfjBumOYQ;9sDX2#V_54TzTu0oa)D6aHl z+LD>@78B>nJtRz~8KklX(71X#<4Zb-(9&cI@NjM>ewQTr1fY~;7GsihTWAC#jX5J1 z95n-;d{1)X$*NoX>Q0OVbp{-!76nihJ7iZLB*i{v)IJ?jK!F1-?w$?jzx}-HI}q2$K0+h2t@l$nVTgUKJ!IW z1SL1P;*t&93Tm-4e=1g#@6V)DOs0ty^hcQCrXPn$Isr%k3Tl**<7o%ZMSewbPH#5x zu;K0j?0Byp13d{y4(?wf>B1y}!h)YH{vu*nOl@(goP1yYE9Q_CDokY@&FfdN;D|c- z3jN9FCjh2{Xtmec@x&j|ZDlu_m zekLK?(B>_}`r^4*U9teX^Qw_hIKSc1@|2^Jb0Y>O`Zg& zccHI4VY&qrl0w1<#;-{P+vy)otj8E-k4^7)^*3C$>bj*b0U!lQ40ck-o&bnhU6{7i z`J~Q;e zrH(0mX!X-~vb;ndkML7j(rm5Qnl(CI0L&P-|!c9J~YX-Ur(DD`6I0aM{G@@EB)Dd;FX5y6f%yV2J+#3z%;vXU~f zg=>NNr$%e_)Wx~ull)#h_GhRGxI7Z(A6$)sia>HWDLTL9oksL@#sHG2!V+WG=i|>^ z1Oy_S0o$j>$?O`n>&^pT4$GtvukVpeol| z6Ty-{C)Jm!<7su%Dl^Db4|e0&O@GDN9q+=S=a85a0M0~;2i6Np9zLfEmU6$3qCZdwacYi0wm@SF&-j7pLh z7Agl2kNq(&q$OC^;yeK$A%Wti1nTJ`tB?ao0K^*n6=;za$Syya>pX{1U>#z=-RSW2 ztUmAE?;pFuVwH`zyxHF;3m_ZH7CDdn(ciC(*ouCr8nBHEAnxNZRRs-FoPp?s4S&X& zJ?oI+aWo%#9|DM7PLeQe_u=uGC*!TE1I=^@PNsbx1q&CKq1Y?I841B24Z{|XK~hza z%2J4LxSY^z4kVl|#64cbJbv_t4QyH4f#D&}OSo5@BIafWAw!WRuy4uDPD)Jf=p?nu z#zuw53(PxY3i8YHlS8Y<6WGW(5R)v-0wW?Vn^I03R&m3KwcnIjSq|$ED-zT^ZSdYT zcYO91?A|ys87VXw0I@*nqHjDV){3Jt#whnUVKaUnPc|dD!i*!p?%J%;8BstGLa{C5C0# z1*zu}$d|8R;Ssg)ngA+%6;^$c;ywg4$?4;&FFn3z zeJ@3Z9*DGEzw+C2?j!ohRoS?f&O`ykq)+L==LHsgJO5|diwCrWy- zv#dahkW^}IH@)?ZId^j@G_FN95dhI=YzJTSSgn2jNq-&Dlm%(pASZXQFgxylVg44j z(#*jkRiQo%=>Er!fu!5mLB&U2`e zf+h;T6x1<5k*XVT_?XYEvNjL14yb`*Pm$ljo?)#1*Dez4tO>MMoe(JQfcV9LkE;JDUk@T@2dH0sn!v31yX$JBp2Oy!(c<6wEhfI2v zk!ezA&hQ{UxB36!GaakpApcAbzA~;to+yB*-?M}AYCN#z<-t)|31?LmY(5_(sv+XO z9FhE$48oEj5Q`z+(ScY?3lakZ(1Sr}lvK@J0-Pw1_Z+;+mOSUfjy!BFsKdJzi?P0B z9xCUTV(P-`wEXDS?d_;v)x&s93P9vc6gkov1UYR0Q*HycIvI#a5~Zi6h_O{*L;}(F z>sMVp=OOaRj%SgL2OuVW@)sWAoA>o6ZizYzuhb3hIW(c39?wdX>6U+RzfdyvM45OD zKxX_0i3sMmti;#0`~`soIYjx9A$_tr422hblq}#9g-l(^>4f6*!CqJh_pDj)A9N6G zem`KhLvcD`D=2`Hmj~HFQS~wE&S>IQ6z8jsTXU3|mSy0cM|B*T>$e!Az;OTXE(>V<*vywl?Wh(^;f1+r-kdlaeDD73W@H;zLBJwFHU>nsM2>hcQ3cl2KDWrqaslGa?Y!3Jc*~umH}Q z8hB>Sf@|6|$SxQAQoPNYp-Q5Un*noIvC&Kmhi0<0O{yyH3 zkQ5A;R-xgDPh;y*pGCN!hyh{6-_|2ILf&oyP|9h^_)KcGrI-6y1J%}AHxpf45_z+F ztX)VnsOz3Od&dJ`**;#3Y%BoL-xzs5><6zMAD?p2lL?)w+$^BUZG@$3A*)0Yd?;zw z6M$4WB7?Ya-6L4mwQi3AL`2aucP@%gKiyPI6%|bKm}alc({aXIGviJ3*nc;p{idX% z3X()u7vf!A7<~P848Hj$v>`HZavXG?Br%6&*Yx>Vb;c#=m^K%$|9Kq@*~J35srVa( zG14WFz(z8|EL$aO>O+hR$xd2K>B(|7396%W?Uy(I;OJ+^GwH_z5GhLcaSwI=p%>oM z(!evuI9g*;S*;}Qc{K^O3PX)EDRkC`$8k#Y>#%WaY~1T`u0lMbCdk2xppUtX25(%dIx}JCf1FyXXwXcu;E}3#`CG32S28+t@;z^g_sg6?Ulw(a^ zWm*i(3vEdm;X);6!6AQSQ-ks;7c)=JMk3bMnlk_aec*3vzcJ+!092GHlJG~p{8#{D z>TAI{*Hn2=yk%olHPS=W;-UEW2??Xj7#Wj7!lX~&q)kuajGZsTX^bBuBfjFAG6ng^ z9D}?=4uO06^nI$9CJ&Q|pF#X+;MG^zS7_5F=%G-~@Qo_Tcz((RyNubYd{4-A5^>S{15Dknh!%e zT*jOi(bmR#eT)2H9&1ybiKDQ0-Q{a;KI#cdh05ib&Lx4E^+iBj_E6_<2Mq58;-ZrR zjP+UBi4@i_LBzruELTW_Y^ccBgE)8P!#H^N3P!v*)KXWViqO0~_!ccf$$96&H-G*| zM&gWxZ3d*C9&|naIELPOi}9tTOF)bRHP4b2tgSm3&mMFxI-JD_k5FSO4h&{VSqmb? z?=$KfEDvJV(PbRWI`i^mH3@~r;8Sa^sQDTIGK~q{qsoAC0f=QBPCMnQvck{(;H5Z} zxqH08h1%lYTLvCOhs25p0k01z_r$Pt<109H`7`hiQwg_q`54SM^o zUR1QlkMm(YQHw+n+PD$D&pdkoF^U>t3L~mD=HHwIabx^vk zS+!wp17j_=OqV=4DF;a+uFf2i+Bo?5WuNML`{^!=Pbh5&a{-81UkBazw?ku7kABRM zdSTD|Gj^nT2ObF zW*`+Bp2*-Q9cGkJ#vhY6J}4|!P+V$5px6Of*HF1_0}gxiZWMI2W(7~d(MO}=vdiGC ztenW5KNg=MYpw64m(cmgKSJ&4F*|u!fE4rkvHJ9jvFW(eq1&k%L&9KJ2t8dPwiExp=EHSHU7f>PSbZ1rYl{^_vWCFB<TvTK>g+KpLZ9tS z#(yf0y12CRoLldSI}1;l(6Y8}y=1Awq8>U`Qyd`4<8-EScx}-r7QOUm?DyjTgDqk4 z($Z>E6qKHSKKoKUp8q-U(0B{q(cs%}qv@ub%&XisCqm1^Z{Gdo_LuG@6(M3{$w$d{1;&4>PyJq1 z`Dd?sEN=51WCc*BbC1=^u@^Qao2W;ZWdlVec2rL_n_{vCll-6p!s4AeGCorN>-sH-BQL5Hmd+LSQaMF2D zof(xRhJjEpj+TZXI$J}~scj9L1MULIHC7pZBD)r;55N7u7ux^+bBfN7O1jSmAmTsH z>I-h4R(8x+Uy8$)n|xR-EVJU1<*+jAs-nh$ifRXJwv6VL24IhbaO59;f$6K>80A3o z?YAHED3jGSR$%jEh?ShLL~*pPKm7@vk32Ferq)+89j|`%2K3g=8FfBQ2eqx~Y8^q- zwjpM%neF^6jXN{X$^?+4_l8#e<&2I$U%Qd>AW~yV{+$UR;s0?>|N8w4{Y%e(Lo;~% zE8FW?Jm8BRu&sqI|mK`W|q(`uY}&n-|A>5em7Ab{FiZUKSlqv#y`pg_%h zMSXqPb=`H$)VChGtl+u*K8>AU_!^wWg=uLZ;&ZxYptmc8otuXd4kogLD4U0x^@EVr z(7L~!-2Ld~R02(5jWmByHULq;)V===FP^5=9q|a)ODUdZwf%(J;(as_|In`Q zf8X>GS&k-8DSkZT6u1vrirS^K;qW+V;*(P+KHA?E#*U2xtXr+Yvl-L+F6NL%O3SLR z@4D;sC%Hp0EiRS~Ks5WFgMYU2nsDLV>vIk=(^sh~Ps+opo$g?*-_jaeQYLFcMNeZZ zS~fMpx1#|!G(L}_crfkKt{F2h<$K?Qch;n{^PEPzS?I_MXhe;x5pIe#nwQ8x)uUsY4wf&)(x8l^%B*xTXtmd7Uf9g~4E?UI$oyR*Uln`xd!q7YK zAh>$9nXf}8X-y3R#~la%k|h%&_j)3MNJ9e#-+Bw7_uo&pvT@Iyi^AiNhiB%@$${GP z;B@VtC!b`hOuQ%kIjlJG>$#uDdlgHP)vA&aM|q78)0WPFr@)&AqT&81HoVh~c$_jE zc%ueOt-W=u-0}VuTd!YqCII3cB!QCE8o2=ETXJ8=D{;lQFbzOFqHcD%FiVvKMGEFF z;=Ptp!q8@*(2VH;oz4y>~>#fiR2a|3P>H+Xr z-3fSh`Uz&SA%TH17^FC=X9O_q0Nyr_-;uU{s9)2K&eo`D))R<`c#o1m5}b+P*6rV$ zbG!vWw6c=90M?fv6-FkIfAM{tD-(*hChZMbwx-!lf->o-r{9!m4osOwDF7*jIN08U zt;@F}IGo%bhn+HIq#ZwBf4`|Wx${tVyHW9#ub|}2Gxtce(~9hV@=0_(^ia;cV_R`C zs;{~V1xFrfR$XK{D4)6g!3WXz*S~_UneA{+or)>fU1z!(a;PCvF}r{EGYr4=7VphC z`bzEv3ozr3J6MwcWDf#50dv>)zK7Uu%Jni2a^wDE%29Y^?%AxQS{wvjk4wj#Luyex zr6l!x!$2Exq4)biJyBW8Z?dbJ_?RhQY+9&k=e4tr;wns-@>v1If~W+b{qG;xph~vX z9r>guD=S!6Q4MqEyHQj@;RX{ERBh3?W(S%!cTs0$hG>>LU>I1^vl-v3|8v@yvB~|` zw-7k`=*f;H`+U5w4?Dl}9Yh-%MxQS{+~nQ<)TdB$_0?uIMwVlE?OL?{;unZEHs;I@ zBv%8ImS6sIPFKV5>ecLdh(qN%hzC_&brnj_I3sHZ{qY7O+S-cdAN>d;YuBcI^o@!G z@Y{u7K*C1N$a%sB8v_khwO-6VWCj#_Y610kjbPJ?9wZV{sy$6wqCN+Zk?7v}-P!}W z1ezFt7CkV$ONW$l$cRKR-qDNHmm;f}zrPpx#nf}zth>gudF zq4Wt-J5-XBbK+$gc}E<9sn=hhGtAQQ@WW`opG(;s7oz7WKJ7G2`R;dfO2n~7BG%by zO1Zb+PW#xqB@6MZ#g`yz_oTXZu;vy93W{XRJ9HYnffN@E_J^=xc^6`F3Ukn6H#>f0 zcK?^5#~Qy=Gn*R5TAYbFB@hu2-~4;~8zq}P-3AoN@cg??R+;scSHw+k35X7dvGtv8 z=nA$s{?@*=zJ{MJJ!k{9dx90(GLxhXB;_ z^Pe-$XFc94S%6Wg@g*pk6=WnGIkUo(XO zL?k-R8=d){ERB^^*n)ey*Tdm9Rai#If|Vtc1d>$Dp6^6)RcbJ4C!MDCjcD4^J1S-J zfcCYxb{jb~QwNb#vcU3>IAWsXm-e}6TN@g_`At@fmD%N0SdR3P{zDJNl|OxJlZ>wqb+Syfoa05+}YMO>xc({@YSn2h~L z)L7r{>!ux$A%R%*JU4)T-L?X*JX7AM0f_e%)Ko}@iYc`Urp!t2Y&y`{k8LYk5gLr} zePoFs>6p>K6F=VYVA@S(zaLdsT!G?~PTC^?k;G`d>n`-a^wQ{DVtz#`^C|!Gmr;85 z*~}8kcEq~6&~oRU7=GuSoYkaCem<(d`Arm_cw$a7oF;eIHP^6W!chkY5G|UzpZo;A z1!-*tCiMP{L^n2K*NrzKwu1uX=0Q{RQuX0@XyF&2Q3;T!u+??Ayb5ORUyPC|g{j0e zBGKErv3=tJbeLNxr1}n7EF$8t@sakX@69@#OQ5v;=$rtWe^=AX@D|M<1t5|>yz3Y> zj4CQJFmGuY6nm;Nf~3&y&FyHe?}D136<|6Zi-XLX=;4;U0-| z8qm<@&1nAdj}dEaHLXKYbxjX%v%!Dx!A#0IDk^fWc*?gSzYY0UVr^}vlg?7BMG}XI zc;%&+!d_gQbF#^C*!$db=y>3PG(N>tD(%|r^`h*OOHh9P`FkXN=sLse*Q4o%8(6lY zI8?}Za@wcyV6#CrWugGwy{n-smERDw2vL(v7|eElC!r)e&63o=se=sfkS- zsT-x;L5-)6AAJCV+%5NNfda0!RsUt=x6f;&V8F(z>~3 zlRykW3-7FdT+gpwHX1;w7AV?2}McwKSW1j*j?X=j)oqVZt0ANaB2OM zIC@}P5wv|`~hr3t*`9- z!9ka1sIam9Kns7e?zei$jEr(--gT9A7Nvc-sM8q5jY60<(~g=2Q<7-}rtFgt3&+s1 zwF_Gz`zrG%=Km94RZF~{( z7u)8kz>g0421dNathZ2kZ9b;WFM`XP5lm&f9<^c1>Mp=;cIYtoALHfHS*$RnZ|C24 z{rJdlOvDc)Jkh>+H@|wlRDIA@X#<~ZjF4)KZT9(5Vbg<{KCb{})5{o$tZ75EXJ~Kh zAlkNfpk(7pT(j+I6sRe4nOyJw0}jB{Yp-QI<&$Id2r%*-45I0=#}N6`pUjP3EgbEk zsh`4&vrmGzq8Js^0w|kO0J}4N6Jk>JwEgC`Rjr65+-4OjbuP=az%rzx+`jI&jdz}O zy9GdFhB64UzS-Y+;tboOlkZJ`6~5DrwRJCRTW59~bK6ERV^Il;YD!0$@EX-TjYQG^ z{tjI5)SZ|!m`X>W!J(>C@)T3>b5F(l#}#;tSE(wR*RRLmZMOkL@-6LK`s_aZkj(c3 z55<&uo*uz-i8_iHl_$LdSI17}4ElRBB+ z8B%>io&DJQZUaWbPPW&W+373;Jd|{wwg)Y3M679{Rga(7|Cei*Thcj_!NOemK%Syw zzd611^c(&rIlX0>YK(>M*bac^_LQt~rf8t4#$eM2Q8ObS)eCB&WC>onh6nNK-`s?X z?Uwvr-Z-J?lv7Z7`Q>I`pFJd+A2&(V-PMJTmtThZ@WVhbm`S&1U0mGwF_L1b>(ZEl{X4YtGU3}gC1 zb5K}a3dLbhN)^ZOAP#x*0nA?h5+w3!^E=R$$x};##=L_Mo=^hU$4%lemjoq_b$6p{ z>sA<#JOT)-ON2ut2~?f>+E>1h&N=%fgQq56(lFFHfSs#$pub;-;?6hIHN;Lo%;FNf zhwpX9t>#SiCxv@g4m^F`6+`P@XtpHP=gf(wd1Uc0S3yN!%GH0rRwrzLveCAYN3AE)BzNg`!Hqy>G0(HVRJbl=^Dx!w&2J|e~F^qJ4caj=aeZd zZKB}t!H#=MMyFAbnn))^BH(XdWX>6)QQ&mHbfKDPtnD8>k&N_%`}Y+$&=g|m|I(!1FUQD zSktPzI)8KeJsBo^vcW>OtOM4Wj7<5;b8bI0@90bKmu${baf@_|O#F-Qe{R{@iN%tV z8@g%G!_*dc7`7{FVPpl89vx;NGU8D{kHPI#kYDOUtxv|` zYyX9%8{dW_78#Wa;i{{{><1oTjTSyG2LVS_RRn`U3=a>p@2m^Qnl%6=ie*VFp(@m_ zX>;+`XU@l#;%Vq^?Z;5p2trh{EIZRIEYs2wQYU>sv(%R4qt%vme2sXheejQ$oiV)S zb>cr{fl-=eTE1^K040eu#l)ulaLql=@>#h%bR<(f7-W+P?IL2&;!9km>C}}qIs!d1 z$UD^&Uy8#96;<&*d~w^$*nfEUC;-{Z%Q5G{2lpt4_`?V|k~sbS{R}iCBO_3$gs7n#z-xw~G%>D_CSZGOe%Qy_BI?|!$( z(fa>3h=iIVi9^64o)e?SEP}n({s%;?zyU?0>B^$NGzhytTqjw+x0TeX?tf>n_r5L z&H4TB*}fJZm4iS-q?ou(G#X{nhCo9gV*Lzzdrj-=z4ri8T~z*)!%%rIuNF^LABBeU z`7nf4l@|OI9LJO{W?HJbmxk$o%d&%FuB2NIAR``%4Lx=B$wRB2+nOzeF}CtEHzX$s zM5H5c+D-4?@2p<%sqrQ+z4*|SMxsj@lTe}{k?-20M}yGA11ztIxYAh-FW{`!<*;Qm z19eTG&HyCmD9Oq7Tr7Is-;aTT0XQ5E6ciL7FE8%{^>C9)A~GC_L>O2|UC|dC4OVp! zh4l2(Fi2JP_XC;xX;@xHmu0Lt`6B$LEgvKKRRE=^3rmV^F-~9+D#?-%&#fy{@}wt+ z;s=1tv)y(>fc3l8E8r`ls>ft~F_Eg9gZd$RU_<=bhu>H~Dwr zPZ=B>L|a=Mn@Hsza^~hfD83aeEW?JA&d28CK8H8|UeDUHn9+0VmY0@V zE^bvNd;g3A4ttC2H_ZT#p=)|#{j=Zgc=QUYfFSjimH&)!MRq6FXo}4Fyd`JUmY@Ig zKgj;l$+yn1B&8=Ek7fax={KT!_V5r22Ac8Jjeo+DAxkbXrGZl0mkTaH*HR_2d&4TVlpdmB7y&c%O z6QFe49F4@qH3+H^UjbV7I|T24`b_lJ&4#Ynu<74B(cT>70J8XYI8ioR`(~;u%Z@$C zhnyII^l*Pm|KEOmX=ue`LVZ#0P1Zf-b4eh&WwOzNv$(FP>hj0GXRn@rQC6??c=MVK zL|g)y>9KK4Z(oORthpcMQQBdMABMs9^s4>g51B`LyhE7A1RjbZ3DvF05RwrNtu`Vgd=s0@fC3W0`r4Dp)gW}vUn415Xz(|OibewOPHaAJ~z zR&O!39e)Nojz1ak{6KO))RrwB*t)jI;uz#nbfKz*e=OTp&+(x9?GhS zhBm)^NAK@1`Qv!%Yb+He{wCCz*MHFI)5^~H*~7L##pq^yS@4vVABjO^COuhZl!&IG zx_>9WwE6+e>))Q9BP7c#0i8;p$7;MlV4?UC0f(#|X64Wq_0XaGnaps2RT>aj1z7lZ z#&^W;d6*{2X9z$9o-xR8+HhnA4$yD&pxGI~dj&I4Ur>j(ylQA}A1bC5Vea9xVWT}$ z`4I`l@!m^~&@>(pWdvGm{mlWAQ;JRwKw7Y8_rNnZT@qUNEJfc%Az@baH6DPhYK&Q9 z)t5iE`Tem7ZjG=?A=3qT@fM(WEE8^LGm|B6$#Jr9qyYX|92uNPBpx(NkG9hK8^ zBGZ^u7Fj-w?+9=ZcwTq`*s{fx7`X=6s$Ti~(2(ae5OS!YoI{au{T62dHhapkDQ_ya z1g2rgUk=q~20SInKv9Jc3zyA-!_pWk7D-_BtIbSPiZl!+jf8(E>&+(z9LD!_9pTL{ z{kHp|i>a|VSzp5YoOAQtu~e7<7%c_z0r@;r7nN6B@zjg<{0bHuo0LVyRhMbmo2_9e zjbm=ddR)5l9#n)W*qeS(Jy+drw~cDcA}k=Xeh54yeN2zHVM+!fuM~(R@2=R(G@F#n z=m2j`DY5>XJav-bA_;oy{V#8azF*K+rD=3n_o~#RF55PkyB*E^!?%$npa-yw)I-6UahdH^fY{cx^_&z;|_Cw z3~o-7gdzV5eUYjn2VI(pPD}LNcY)>0%~(>l3D5Z5gciXhfJ0@CZH_#2IP%bI_o2a8 zi6(D3+u}RqDMW(yD&;CDttSrKgvNj`-;PB`PJ^#7MKu#~6`S5|MPIjSCcrZu#GQrv z2tc};(4+OQ-qrokMGtcDi0s2;{&TkJo(O=fWZLgL@bqbAXWsrhTcDDP?I(6{e~>99 z%(^nF5j$nmlQ?PH^KfbDtxybV#B||>$UnhsmP8<-FY>bxh=}uyvW&>H``5pku_OXg zj!A#lTMgQOYrk+O^0ipfWw|G6i_RhIX@|Q6^_~*6+w$1<-MyX?4AGwO4lh)P8=BqC zHpn%18RX#xkxC&(knN$S!|RhV|F9`2C`&b2R}&iQSGA+7EtYP|kLoD42a<>1UmNae z>U;jSZ-iI=x!zL$;XV}b0005i87k%(zez+vR5dF0l{w)P0T6vgq(dd*P20&@+z3i-Ggc2Q1Ndt3xmC z(_}^yNq+7*V8G%6uzZMFXp(|g3TEPwDaS!oY*3;>2HXmdgaa1Wp-9o-3Jt?E)Cc#_ zARLhq$OKmM2YP*o_yY*%6(E`yK-8a)Uc-S^YZ}ot7(?9Qh3@phaC%{|LOmY+=B;#@ zkA_qfkB*zozO4!D=H4RYSarJ$%saFig%#-yE4HoaK>P0K7%FVf!9$M2`0fpV>G{|-6oGKsQq{!!|u;=(_Q7viPr zBM|bHB(KX%bVEf^xf^qioDWYR&z$ItBb`ti2oH>4>+((L>xe?9Xt*fGW5G>YNCX&a zZ9`r&9%^9Q4Dc3mNdO5!$rJK)11^`2c?XrFs4CUHj1uE_tm{N;V|WY+wC4cQMh5zZ z-uUg6!>{~mMHcBZX24;h0J3_fy>L47D=+!umA-{1Tw#jkj3WskUPs157o@u8cdW-{ zD}Rf!k&HGfbP1}BC&@E?db-D0kYlp4s4pOO<|3&>4#FJXT^0eCWHdSp@nqQn*if-Q z;_d+NS;SvkfLn{Bw912-hs|Xvx0%%-sSz|-|Dzr6)}v$h2rG|CRWQsf=$JJGT;l#)O^7M9rzDmfE5tj!-Ui->3j?!6mY;gFe(zpcY zJVcxoErA)`TX6BJ-=TJpw~@0r4C!=f!2%eE9tuznw52@>v#h8hflP7=35cr9Oi7WH zIx>QigIPWQuj(+gmCS}@r}}WwJV`tyt$f}vRE?o#W&qO3ygOR*4AY1Da!OKKx zlA$3`?8Mx|XTlfoXVuKqx_HN`UFh6B0)qiY6z6dOCG&20Y>Mt-+1SKf{pS9r^kY)J zGsh=Y_-deC9_AfXg2M824zzu32ilqg-}pcRQ;7de#c$AV3>B|i|H7! zd9ce}f>uuny4^+S^OYj%^qWcL$$S~XOL*c{(rh-HS`j}{6ba?kUR2L2gfEXue$!zb z4@VJ?#NaDR-}sKM5FLr5qj?x@jX}hs)P0N>=vgxWbJSZ|Cm<6*1%);&IJ6QjPpTDP zJet7zH<~aUq#&Z3qB% zr;OS;g($1`!Ra2I(n0%t>{z}Dp`kEl9X=m{%FLG825Xwt*A-@4ONN8QIZYyMN*_U{ za}BYn!$f}p(>tF)S+yIpmz2ZKcG@wISU8RqFEt>}x``2h1P2=3k~gKzvfo!vL=oTd z&ZC`w`r5BJaVE(l62>M(ocC4&(fL-ho)F`=O<7t|dEsw<>a3c3cxoy0hE$@X2kRw7 z`9BFFa4_Owo>*q>dPZf@)LFd_Qy0%nsy1p5)3LoB+g3C&qs(;5iIh%W1H}Q9OfVN> zJ*{&)>0LXfFDOQ!)H7;#AvR$|mfzis_NGDBfHKbyEI4LA_)80tCVnz{r5gwi$I!TS z2z{L~=z{YkEj}S6tP?5-kZ`b?;GrUe+Sz_gol~5a&JpYnW5vr&fI_7^yctx|j2{h@ zTumtqLpRj!UGMikdHuDqhPSBAy&%q_TxZhcbWbG^jk5@G5TRxu3Vixo2L_J2;!Y*6 zjPEmVNn;gNt2`f?2%`}jfgTUD5+6yljN$cV#pNz6Imz05+`v%R5H`NmfN(HwdV3{H zOEw{Gl8v&tYk|}fXT%PN%-X(HPRoPGM-IUB*C(rJsG|=Js~a%T6N4_38=|P4UyiAZXF#^4J5bFy9}pT*v3ti5I$Ofr0u!mGTxyD! z5y53k8F)##leOb@`CMs6#fs-QA{cU-=DVdKl(}JC>KLmX4i$8U);)Jy-!nHqgG87@ z0HnG^)9^6^repD-y#bI_m9b6gc*Ndce)$vM@yN?9->A}cwfn!qQv^> zFpNaR?6=4z6s^j+2b7_D!L%e%q)F;-?#0$s9c;y$tC0HK@SXXD7nmnV&`!|~r-HI6 zepFA-gUgdj(&Ff5kEX>nbT@XOVf8LvS1vxsHvr)yNcUIt5Nc+ZVCw$U;BXNa;TD(p z48_hgoptJOY6v1cLX{a}zYuQHlX=e;ODeeRa2S}epaA8yfwU65k-i|_d$AFc!_5I? z@c~&>b8fX|z(|A>k%o8w+H?N}cW~f{L@{A?OW?EHrAiL!`(rLBQzGl2 zy=JSYc~Lc^05*$cqvzYQd^(dIS(=zIFcU#cDT!rw!r zS%fS07(6LiKS897@i7tRNP@^#cVI=?8Mocyu3d7hL@^^Kg>){Agdv@>*2s}Ybz(+5 zVs?KuG$=@5_5tOnoL!T~2@6H4Oo$Esp9rC8(HHW2&7DwP<=X zs0Q9(&lwzuVW2mL$VegyB8N*xS+xhnWggfpjlfCT4fhP;{pD?l#d!{pusALJXDqd4 zsBuJFH@w^T*PCuucdw&e{)ELvbw#AMCT(?PVqkAonDu(3M@04_dBME4!l{MjmppX0 zbL##}BpRJGipKxJz15-#gV}r`40?GY*MTAl3zp3@w{5f>bT+MU^2bnyB1s*m+XkB> z`+i{JR9e;H;SqFfZ9~h}PN={V>N#yJ#{Wlyy+^;3Jx=N;|=wt{4QBvte&HO1S zs0c8EJzi@*0u(iYBD~Sw71^4Okn?co+9tF##{nYq!v7<>&@*>M_UU6~N1Cc9S~tDl z^T=gase>*38Q>9lJ`;8r<`N$J5t%kXcM|vScFy ziyuc2R2H=2(vy>avlkLC*(UXyh{EO6QC#Im`OFIVi}Dzd#{*KXNugb?J9czn*XBON zb>cqOPV)R2MM_APi|C1%p6J}Zdg!J9yDhx=#U}3O5pga84@sWA2G71!Vd6CiAl9fo zGw;3gPnc40;&*R$)+{>IbTnqjl8kg$;z6u-p81IqQ1mb(!Bh924yVV=6`l-$!od)F zcDAFdu?NFL8gz-$Fo;VCXQPOfiFPQIU@LePH^Vq@OBMtO>yky%$}x{+@jTo(}>*bWH)Glz9($Y=xx-rC)gHhpyViN6GH|^euBlUJk1U=9{@_s@af= zK%~VXsUeh4^`mz243=E!YO;Eec`7R7MeJv)8wFOQqf0YcHLa#2 z7LKBQTN_$;3?Km?+XTf_tKz%x|D=~-vjxRidpHr>z2UviKYZl|1be8eg2=LHjJekq zSGI+)FIAZJ`oc03AYvxItz=qJ@!3DU*gfr#^AvBPJDn1lnTx_lB(_q}VoIsdIs#>WFmZ-}RsB>g}Z5b?qQQ;m_|iaSi%?}d*m!Ll6{DFL_F27kZ}U%&&G-^KQFw^1=8 z1vL{YqQNMJ`+^wk2_Y1gV8|ZMdH80y%-_YqlLUAAbZX(jcx?CjzxO}$gL~DkEwqh~ z6+BkSGa2DMuJHVz03;-lAl9UJ2t*!R>GYz)Q*OGzJ@c@y+49P0*_#h(4Zt#$W|scw zWY$MVOzVugc#zjtjX{xh*c3CGZYI~zJjzhXK%z}#B^%qdCRsBsfRonIf)i)9v!cin z6yD=30-Eo>oNQNPX}E4aK)Tb^+$~VNO9C=9$g&PaHdx%2WpPjdLzkf&q^4-kF8bmv zT3AcoRA0kn=q8N;=um^bMs(+U4-LL_*Ar^jW&s{@5|Xv`fr00PQeoL7k+9APNX$YD zoPOJ<{rjK%ZCmk_5=pkFfXtjE+}!7}Gnxp~I*S1Y?|_lacH~_!tjSy~03p&TEP2Su z++|Capopm@>&nOAEM^uMZ&|}ld#lt*0){8a3!) zLRyKrp{a&C*gg>2_`lmgplL2DUAWCpF9{3j$(CWu7qJOU8|lC$pM^8DkjIMY4jkc(`2 zWz<0_367+x%rgg>N+S>@qLxsAWo)d`_hhdht0Xej6H7;BT*uOYiH$~%^_1)P+)-%u zJ~BC6QnHXb6aNsq^$wT1TD=+i71SogkkBOW*>CXs1kpL0-icD@~cl!!Y|KW+=1)u)9t+1w8 zk`zf)qni72n7a}RONW7o>gc&TNj(WqR~5dSWGRg$(VegMG%G6?K*j=9TJXZEnz%8L z1e8V93AM!{{KVZXI|C-Y-vk`dbxidA;f<~g%}@v02O``4{fGX)-tsimpz!Tj`Hle3 zJ~P$Fn(6<93bWo{fJl@}3zg;u>|Xz}uP*j1IQArG<=ka9UrB(GO6Kvgu5fNDlPV)g z#3qFXT9`|gNV`xC)^Zw=6D=08SWyLXL+=MYh*FTr??|Q)EeFO>Op)@ye4`Te*NUiXSn3 zmCQs)n;%PlxA}W3=+pNrHNu04_cb?$HotsNaK&HNV6c_^I|4Mq%CdTw|6}0!PjaAa z__lhjg}+FsHJ8m5@D+Ui_6uCo4m#5ws3|1RVOnT_$I!_0N@hKuuH=1EpB=}l%JyVA zRoWyul3D)2IURq(v94_ap`j-XZJ>QHvU|;Q{ZHQT2Sf+04nYDAIR=G^{(+O_TmtI9 zA%SwKG$D5nUUPCvm;?}QfZtcbH%V&#Hc^k8W(I24(aG-Gr9Mtig zT70KdpL!59Q1G_`BBVjMX+tnl6_fMn`26FAaC-W>NM&Zl*$dYcHA*RVchquZzW$`0 z=0AG6jh{s3)~rglp<9k#Shot_*3829Z0+OHJ7ax?W(6Zir~w|50EZ&V7-97toWuLB zQ__08pC1}_OG8i|Lh$hBX(%lbQLoOW)u~&V*z}$IVr=}b9FC6C<1cbJ+I&S+pTKd8 zyUdD2N#RCqPp|LhmaEyfOJ;5@XKt?M-Arl|9Kge&;o$VuT(xu~dkjXmr)vcgX(R#( z5}#=~ERs|4*zG4b#rc_qa3VRU#Ky*y=xD^P&6Y#TVfkO1f~+p*RrbR=JU3=H9HU}5 z)l$u=l*(3l=R^JHrxpGE^Hn>arC~U!NCE>5nkT4jNaOT0uJbP!sM}-uv40RGXeAe# z7LClGkm47$*o}Lc@VS}k(AcF(kw{;W)kI2(M3Zt@OUXjjyqPZix7{FZ;75g#KWnB` z{5KH!v#oh=tFBY|ZLLwS6&khgi#w{LUbEY z5lHyVrJtd6w;{EH@BPkMgr|E7VC z^5bCQcqo8tDGm?^jzfqVSzgP^`v>NPOI&zJ_$x41T(bZeK9T}>J-_fLb;4l)i1!6R zqIE$yI9%MP;_wAEP$vKp1l;s+h=SWVFat9bKMqX822iV*60HLy00i2GYaJxbxCRP- zA9reDy0GqV%R>Q7utSC&prCdFOpvY275b!A-*8`u0|B2XIBFV7Xj*mZakSVl92^W+`BEymL*xe?`r$Z|J=9C&dlzr*rwW(hpbn-JN4Fc&%LjLz1!Y9um|4( zWe>jgy_dfC4q&eW>=lAN`VQ<>fIa&0@4fiF3b0oO_UJpXR{{3u$G`XD_bR|gCj;5; z?O5))p5?T%{r#i6iqH9;$MP^f=eJ;&FVB1r<-ariO&(}0_uMS#cA=N=NBYh#U(}wm zgRwk}J>_1EU%pMwo@3 z{rl91HqY|q6deVZ>nKHa9v0P58j9%{P5_}eS@G`3 z_&rk=lXB5}hpNEu1OhGv0XKOKbgNo&Q6e5)7z*~i`O00VONF2_&*UNYNYAh*D?pmi zpYX>Gb$P{Q7dYIWuSb%o4kv&h34IbsBvTeW&4QbR*s%O23vO8WES@0kbF(MNAoN*1 zh}=IEsH%>DQ-$B9Bgd`6r6jj#@z`G?9W8%;`HDGPg%a%X%CILYfLZX>V}AW=QQ?%? zUvqkW=fpL~!Tk{(15pZVOa{?xh%J^`en);GWw}`xWPHpmcIvHErZN8B@C0K2=CDXA zpAkk$f5O8lxw#z*@;yL-M}bpKzL$uEp6pn=_UZS3b8H^~vm%(i!{^>B>?sOhvG%in ze5A~x_s2e5jAwAkO43uIK%47tR}`{AdT>rqkQ z0sJm)lNKKS{rgYc`ees{Z;S0Iitu?=085y2_-~)gnK0$JiyUtMslGvxs0MBD_Wrq0;p+t_YIjv(Ei$RV66P~ua_EA;Bg z$bBuVKK#Sd-<;6%c~ykZlLFASsPt$LyWnPD)t4@~0^Y#Yy%8<9F9KG4;+9t;i#FWd zBviW#zaPzVryXa69AHEbA`@V{r^=86jCDU$Zz%SuDDvq;h(&H*`@+5VG(34j1OWL? zdtKo(>k&Q=3SbC66;;o8s`aZ*U%{{YqnO-1OchsAV;bQRW%U4Q=uavo$eaVnMAr7H z*iX7A$y^7>8oOsGs%5Stq?CKa&&?34amp!AL3^P}bctUbh+3MH^3 zN1sV0_$(`cIoxra`tXL?fugEAhT`hsox_}pAd5}X@>`2fHb~KM3<9zop@KVF%x#YG zS&lGTCD;UIX39xM^CY944~JmW!>6ohm-HN_oW!Z>sK`-~<4C?8?rFU0KbP*m(&i0D zC=Gskb>p+B0G2ra#Jk@p&#&D7rkLXSX2-Cu4#$aHW-6fC>a1fzZ>%A`L_&G^WPyj| zJG~7%*aV$^X1u{? zNdZ`bOJV7Wx4cuGKXuO2eNkmDAv?sfOvCMW#`4?UgAu!r9Q&K?xMde(Mt^Jxc^Ws! zd?M0vXW|fzJ@!x80!hR(T!F!r6Ag(XCvfj5$Q=2Em~MRXHmBO5{&( z-+9^9z3)FskSQs^r@DytX;uIu=npdvslEhGoz}9VQLOXEo+Fr)K=wC`rLUog zas$7FTefHmZKs_h)bn}2s}P|G2Da>j_%2B))UDeYMby0P|w z2U(u=k3WFRXs@(;O0g+7!Sn{LZicqvv;nN$=$y z!#Zm%WDms@PFT?$nxtTPACCYkv~;=!KX<9jCh}t-LwADcVAE3eLjUe zX%@*AB3r&7ExYWv5*~o|LSen?P~ddv@cI;Zd@7tSmAxm0NF)@*!bwCT8lq7h$pj%x zDm0b|atA`f!NC!E>r56?{!J=@u!`9$!2Ao@bBHYH4=J??w`V72%2#C0!mif|xSTq? zeh2)11zxWTr;`L)K|HP@Mh!o~B*H`xj3kjvs;sS31(6|S0xa#4>_juw5$s@;4Sge& zsK^<8^xt1E{=QIxgsBI>b_xA$i?W zS`{SX8oS=EwkQVrfha2|N@bTh0{0Y($b~ZXM`Wra6nRyYd1KEky8lNP^}qQj)eEF2 z_{8f4p9Tdmg#VBWZ_k}{(#8Mm2&+d9#te)n-2>Rc7WP#k^d$Tm*)Dp3I#g9bPL6^J z6P-*L$l_blg6uH2aokueiT>_5dV6&QgBqfYT$fT#<{nHQ!SE1S`Ty3W#hj35 zW+XS<;xnd`*cG$#J_UINE)V72f2c4bT4WjE1LwJNRiyjz4Jykizdc{MyGWst-Q>+2){f;Bb5w zEI$iZxDq&=3JMAoOssJuH{Uq|3L=U@**H5!LJjoygwflhVPF6V59^{=n8F3h49p%t zgbB#X#mkclW*_#S=8#fHZHX47!EC}ZS$-qtH(7p7W3hg5sftOpZukNu z{5Dpo-SV5>r{x^B$3cd8s6UGS-XwZ@fZ$M)fsS+#W+`PIfhCRN0#phk>=$6q0Zd0W zi35W$MZ(K=B9uEfpF$iJ#U&1Qjj8>Qj|*j&HhC7ozA&~m45N3z#XW(_ua5z&qy&bK z9Q*MIA*#vI*Y$ny>gjLa{?+XO=!t(~wSrGd0Sw_k#n-Hkx5Yv(Qh2$q{9n-1C66~bV@g04+JI2av}x z7s2Rw{arz9UNeYL%nv9Ij-D~$7oK3GoX04pSv@ROj;jOO`i?gqIbrFaF7Ny#mEe<5 z07Lj|zjB5D3*Wr)wWgqQ0BcbcTUHBf!~2;8+G2h(CRCNIpW#Mng?B{DuO&4k5=rQZ z1XCK-;e_gPFohW*HAr*MltA`&4x_sZ= zVc7pj3263@11PC*p`=7XVTqfaXWEHoIC1Grl8Gb|(HP?K1XMajrwW(X3kO}Z>8i}P znf|?Fb2qkZ2ts$~1FFk_NK?6wRg6aSpmZpY1E|eOE?fT4WyiI?_IT)%Qi6|L0Sw_U z-FJcem|y+h(@i1uglNKS`BSumvElqAM3k(Ng`_YjiPCZ>>i6-(>2hSmt>cjxy0^EX zx2X;BaFpv2iK#<{)9pfGbp`TDi{LNFh3cfpUf}#0D8!P#PZl~n7)4K43>_T~3=bzc zTEfZ-{FPTy1-@?J?DBf){CUNeX{|(el1V1$fm{U@6I>`Xg*z!WXbR2YC=7QXbj(;N zj9~8odYU^B?h7IjOY;1IzUOel>2aXAx&meORdD;ghQ~2`f_Nl>wIA$2UvC16w-AcM z#p}e(!0pC10~1N_Juef!pJNH`jxb+A0*)RKODmfFhVon>;-VWRtN3`nx0O=+x2MBl8XsKeJ>~ zUMYX&*K3ab$>s}mhfplC`lU`(OmHBlz{L`K3D0Lkv$TYUVBa7*Hf={=TMuI4IHD2i zhvp#|kKqM90y;b%prpDOljqNXKR+iU>ek)bk2UXgK!X!%pcrtv$oU$`kd-@?rD88V zpQ#-p*R7z^8~ytW=M-NEApUVH!N;WlG6rw}aDMG?UBBsw`>q&@jT7xLp%l{6G0RU6 zRpYIyWFhyRlPkQlH5QJCBiQo6II^2RWV}l5_1hixVDm1uU8oYidN@^!y z!i*Yta&w?E%bsP94}Fr<5gH7mt0Ru~4i$+w|2ESgR+trdx}9$?Vtl3&T9Os5CQNjp zwA=-cSB2^jzcX?ZIZ~9Hd@9%tbo8KY-8S^L_8=BlkkA|y>#&NLwAiM$2o9%eF{pYR zRrMv9a_~&Jy&iFWDPmkAk-&<#wxh2*3f1N2O5kt`c+^%Ou=O@)s~}4cmhV+jt`6Px z;9_Dg{6LTt%(CC(9ybT3mTVgMLg*;!jqImrd^PQmIWYzacD_sS^|m|VFjkDeQQy* zZyh{2K0z0=*l)`#_~#+|mY3^+8uXEW}||K*8zIb+Y`@D~bCroli0emn)9C{i`u?cD*Gh zkQddmsS6vIbusFM>heGh6p3(%C;yZnk0p0X1H*JNi9+kY{^biM-fi;)COm96Nxf+| zABO^v;4|Sr{r}T4)QbqRhiAwvlIu1rhK&Fy-KVQCeTktg?LQX4a&X zPCcn%sBakC8et_`QXdDVfh3s3NobB8`^1(0#S zbHR0wOei?;f8O6dgt9c0K>oq*5=3~MiupzC&!v$o0fjyk|LZ!+Djb+GC*MN&lDpio zej7F~*@{GxLwUTiDHzx6HFXlk@p#2aRGFp4)S)M$(1!a^KBXKpzBnHqzjsv27a5MD zVa*Ww2Ax6)OtVATbyB=fwF^q%y>PiqXpspLHfbA%dk3-n`F9ZPkHZlthR$3Cx5n)M zMbbjdp7eeOc|H8zNhNo?k5-Rtpv6L0$ef;pXM)Y?@IKj^0fa3H(^%pXw;|H}} zR|cbPD4|S$;Wbdm^Rag@<};qxPKy+K;x42Xx`8bonm1Y7LXN7 z6|MMjga9!rg(=Y%=nOw^{w&yQWC)5jjM*m~jKZ37IGu!!Sb6wn+&_`fuw`v8dU~BK zFOb|J%3(}lW~ONp6z%Cq#*=7Svl;8(TnR^R8C17l#Ox}w{2X=Sp_buk z>?}}&USZ0iL@re`tq@ZVo(`wekyg8FT-A!LYx_CNjwzS|#qEXSFMz6W&y#AVN%_Xn z<`K!9jvmRBoW$EN{cy_3=%51pTBngdPh8P1oDyw_C-; z$sSbK`lRvZwoc|e!Zr{)HPknNjSH8fw`~Y&pwzIq?1(}j8Fa)pspKLyH@t)0vymqy zPmpA3-0N}&1**x$05>a3pM_5*@PD;<5e zzWnX#8wts^)d+U0ANZ&gK*s&9Qy<#=<>5f(-+QQ&O9(*PoF0aIn<}5F!lA*-mz))f znd$3lJO+mrhnl3;Y3nV(bv_FHScW3aFiYn$FG@iP{qtB z>r2aJqS6?Ad&#{auq!MSVoE@Xv5%HQa~d=W zb6q0EiL!E&c9dR7_(`W1QXCJq? zC5U{B<+sQDl5a0jaif`qhZ|}#32!ojobUkhBfZEA_ro6xAty2je=G=JEQlP$P+G_z zvZgwa@OTmP`VkA{peL-Mp(%(#haY{Od<=SWG3@jr;qvij5Lwoj9WX3sCjk*>Ey5lq zFk~f}b(XO+|2O+A5rGpS)E+nu6;rF>$@QBIE-+ZP4TnR{#8C*ujB!T(^Dp9%5 zq>O0LqUY8l8X?MwK$+-U%$Sf-al_%Kim~)UR)cLgd3z{edM_n>avr*Ny#C;!>mI(6 z@&Qr;bC2IASOsbmk- zQz=j(ZY_BQmnYsFg^~)uLsLdrqs8y^1e{t36Y9%RHLDJR;(Q*HkEK7r#yz6n47T^6 zd37T?oBE(RbGTqjK__mG%26mD?g*h5VF;s2$f6hXN2x}4$tLAA3#txtG)+$?*mX|X zuNaeNPZr^v?0Dn1W$7!M(ccy2R8)mRAr2_WIs-wZiZ_qv2_lIbQzx0t6Oc~T`4exy zeECP05A03_kU=%6zU#~yk3qt{!2q)N%GX%kgbZoN2- zK;rc&PavwdR5#F*P_+n(C*-1b&J^TTmWbln$Q4ra1tyb8^tN=OY1wA<^~M3!%dIL2 z8Vhq&A^fb(jo%r?ks{(Tk3fH=S7}q_mXybmL(rOM6ZGyI|8sHetsg}` zu)7t2ReBG(>XF*qd1o%^4l6kz)GS46G^+zYVSnS~FO zuuD#o1|=CrexU=k2TVs{bvYbPr|<;nF|dfk*yma@iLS;S*tWa@gZ&y5PeA0frksV6 zP?>-_cy1#bRwM^0<#A1wYqJy*>LV1I(MWVE`f)&^s@9MCd3A7lh;)~#2-Ep(`d}-X z8$&|yMJ_-rQRaD4BBeOpP&|1oN61vi;M}P6oF}Clw6v^_ihv#rz5V#XD@Ec`LLajJU;SI}-G?dm)WQ8+R z$-!#+2=0g_(6zM{jUR4CC`9NAuQ*e9!&FfqFA58PBnl%!5TOu)q+=x2X(T2Yh)X3F zs2ZVZx}IR?PLOQ-<1oV4hYC!nebD=uuyn}}s4 zGqJdG9(ug_Mn{wg`b`Bg&P8HM!UUfLNkWUXT6Po_wRxC&=zhFtmZcY&4j&mB#+DB@ zqIF9jRF~ICaCzN=&>ofN6fEZEkR!Letn!tra2s?BYh6MzJq-T8IB)Vgbl#K%Q=(s5 z;l}iX>fp^K1vJ%d05q-Ij)v6(VEk;#0f?g{&&$X;c&g8a5jZF($Ys@sr)}6KR>Vsa|ouSa~PylKDUAYGx6Ik%$XWwfHI_eF`!yfCIVt*zqqy*Gp zORvKNn7Hj-d~f|Tm>BEd?J@tTroxsZkm!V*UTn&*#Tye3!OGH^h!Yi94wB-qkkBPc zkOfF#VCwW)5_!crn7m&FimOYZIy0anCKE`4*t4S-4a-{)>LuBs|yH46sSR(5-_F7V)94BHzx*?4NGr3>9F9ccWLP+361guawoT4RRAgcJn{W= z+s~U5IoHIfea=8p()s;NC(NBrq5?^JpNs@i*7!EA-1rQtVgnsh3MfG)l^ZHVT?{a zHjIjfH*w9Tf1)H2`bcAbtAEdKNa&NW!;LjX)9~z+6VQ}bCAdUpWj1k>v;;f>4{B$Y zVZv0RNu<*Xj3;J>=0&(co}hJ82b$J(_+Ylr001BWNkl%snoj~uf8;mc{U?_DmC?TP|e65rFG>U^*%;y+ss z6E?FdCHiBr*vtc1JGmsMhsDs*AgVXKitDz$h`eMZvnQ~jJ(=dzSm}?ZJ8UB(WyI+- z?4MBbjzAe6oqjsj6;DNyVqk+KB*P$@NC8w&D@E<>iEuC!WbEq?%j+QC&(_tgjKh>j z%8IXu{RR7y37hORGxs1H_?2+R!U|c>gBu~%Pr(o^%2yIBY%8nqVCuX{@a9o5&uWRA zR&U4V)%`s2RYlJ=p+GFgH{~Lh`)8{3Qk}p<1w9)&WW+Kl2*i#a(I>UOp~qi6zw$DX z3z!RJX$5^*S+hR9F|E|&|c-v`a%K+@?!+~Yya7eL(aN6eRlh{KJI zt2)ryGz5(-w@8EyXjvA&7z`>BPxDhXheQVDTXbCt6o}miQy!pe2~<@3FlFu}_(T#+ zdzO~CVd++e0_i0DbbL{V;M^eDe~>NI=lO|3O*Niep4gA=t{dMgbR#mb@Q!&6kNupc zO6(F>XlE6`Z2jF|d1A+H+hTd&v08c=Yi8*2Wp^_*?SK`R1bnEGaF8{kSX58KJI};(g4(gf{C1(wm;;RBS>$mj6uEom*U=d$K}o0wHNzd)uV*7_ zLLKnN!|VoRobNxzIt7CfH4SmM7ft?htSX+4Z8=rw$|=Qww*V1`7cqw$35N>|IA+2m zP#^lOtar&tc1A^7^7;g;l@SYt7ZCToa*_{I4yc9Om#GfWfb9*Gn@ALhw?WkR^>U(EL8#18|!n3o$$v2Ucu;m zGaB*Sgil4k2L+PMK8WO zW$EmeKXW&S9QGvX#vDiIRZznNFa?OS#H@Pz5}d!~-|E^j_U?mQ$6=+wk^m7R5h z)oKvq^dxGg<)Lo>8s70{X6Nd_@>kZNr;Dadb21!@<(UGc3Ugd(Or3dlo5gZv{<%t? zfY2c$k+)lZYySw-XKJ2tb^ZRei|c;9D_&q{6u^l6>rT16wC?LSt?CXd`J-9Uj8i0 z&;+)>_pj9m4QV_FU`im#aFUb6#mZ2Ew9>bDeCGQ{=n!#-OlX#I+2((kZgh!P8)&%W zv{}8Y-W=FjFECyOupmFD{=dd+TTuE_O3s+0l_SX|AH+1ZICTxO2bHu~a%W7F&ol~s zC?QTTPhcoPB96lT?KpYeKXGi|MmTjoFDJz{%1n$K3(jvBUb|&Zdq2h;;iIfmq*YKy zs+a@Vo>R#U|koTW@wc(*~NK~|K=u_%>^9SHi6-?BcfAPlm_Mt|4 znF~nD^q4H-&L{xG{U?9n)V%u3o?O`#QOd`3B-5A7bOmV(nB|u>0SbKxPnD&4;R&>O z6#H&_8|O5>jM`Yg(GfQ4@ulUIUL&Qg8GmYn<&AL5=m@j3Qpd|u9jhfDqvapZ`BKdO zUd8^{T2OcxVjF)Y&B{pHgA6h@Jqc z1SFd#Dcb$G<&&t=y3s5pVu8}I^ zQ2=w**M0JR>n`u`PP!?P0{$%l!ANs+EU}@e2k`&`5Oexf#|4X5YF++#=j*He|IRr#OS7L-?;q1KiP7DF`=vD zRRBhJrqPbaJl^|KPuzXTNY>Yk9$3N*`;XG<0-R33k}@J`F^@o@QG%gD`!v0avo=4E z`Up+f9$~X7K^g%r+fQwY=ZLYtLyKo5tJjK_g^99le%L{AR}-+v$Bty_`k>to8nI6YZbZW$HTqS!%kpvzl` zwS`mAaOg3JFE|{o{Jb>2e#4T@*t#ib@&Kd=DZ(B@gIE|OW{jD`O;HC(H-@Rdn2jdo z1ZGc=t$)DpQgr>@`wm(6$kiK!7oad`OdO%HDu4`ss8z~+;(hC`X>(7aF?N<1(f;1- zgHmoTgAhn0LhQ|=1Z*-}k`;v5gtjD&EsSBo#usqr)>l!OpcQv)+c;zF5o2hv{s0r` zk`m+|cp!4;&xfn1h*@9wY1IWAcDSJZ{Og%(-| z4UNj}87bx$=RrbEVNCItzJ#3l^WpIMn834;M$RkzxTHSYaTtX}V@g0CBN>Sxv}zT4 zUU&hKZQGb9VQ5>X9AboeM#L9DPwh0kcg98NnzAo6Cg=*@d1fiX5fT1S0xX+!$O}-| zL&{(pI4n`RjN!1+&YBX#Oci8SC!nr4gY{}n{V0+uamChua8dCB;w+8_5*>>ISd!nP zu6c5Pc;;!Z(>#BBg>Y0^NHew8OoRpMkE{$Lk=@ZkfPJ-AKQ z_0#W4)>3q4k0F)DRbGz5*#s@ zc|>>Y0Il*jo*rsVQ4r0^$MTcU!MZP=!qB2OpIQnycz-e*lg=7-Qf-mE2icT>7&A|# z9}*#a2Fq_9NFyrM^fR`WfeWff3r`SRJK_dEh!CN3KX?EOI+npp2$5w6Bg>Ao^mHTDygcL{bP%eq zxPqZKpZATD=;oVmM*rf)&?v#qgeYanp~4Oiesa*|XwI#IMuTzX$aU5XBYKo&#=#IS za|R*qy`bl{6&ssA>30aff*eVyDoAkwk^G=@L-_;R2UnzCNP=;$nb}P zJ@;YPJT@;p%Y0AMa}b;5w{eH;zhwcG3>;{24mWkG$%-*oHMawBEqn5~ zH4j`uVGu#0=C;X+sgpSP+iUe*Ss;oQy`ivlDF(K8T^^ z%MojDH#)>dx>kZr|339(yjpiOhG^X|*#RcI!kAPECaUa97WEP<8fcY*5iP&zhs>`K zcZd>W8+WLDXu~hxyLSG)V-15wQvgf&bIjvi&veJU3oI2%VR?7T(%ZQ_0)1h@BJqJl z!#tnJCJSQ;I2}3)BR#nA-9KQTu8nY*E2j1eDdiNZ3P*lE{8Oi*_>41<_k}M&9S1J| zc@up5^-zuwT(t@vPdtI)jT>2Qf%R)-7&<0BEj0waIqP`*cc0_Y=PknUPz=eWxzJ~t zPA&0&gNiMJBQc|w9mmwcmNqxWGBHj}6QSlH<_T!^%2YdOi8A=gl1r<;Io2>}GzDNY zKFPb3RL{+ucH`TPp`@wj*9hzQ&9axNWeCVFPF%ck^R2+Nsjok^?iZ&82$m>xxSSdS z0Tm_s0FK=91`hh*1^5U1MwU;91MUeEP;}ynDEiWu;4U9Gb?Ngi|Hk#jy1UW;$}8x3 z@kK1jg()w3lK4D)B4-bz84H`)SEXA+Ge){aOp~=TRld;2T zw3t52=ggBBoR`!}Da`V-Wsvx|L7x}M%~z3^uVO+Viv5=wXN&30XwFCvz#Qv~ z8*E-Di;m;<0c8~~_ya16N?mZpBB)>T7WRMPDdcswj`(;9yDvWNG!!3yJiL=9?}RAq z**}T2w6OX?&%gi8s26EUV7fiQ!V)a~%K6xM_=!k(z0eandb&ag4kXak8p6;(lnHKX ziiHq6+w=%CS&koX1j}!KJSHV54{o~Uy=xAns<6E%miACbQUDqMyGa3#dbIQT{-pol zjA+X!9zYg2jF?hVAHLh1I+BjVW3 zoE(&2Z~=;rJr>S_f<0T3cj4+O7wCKA4YWV{DB>L*#;8Re?h2}f$ho?!>+$|s-$B#t zL-?q4w$`A6zP2HRLK>R43?dTb^#?XSg-M=R!A6EU@?4GFSgvU%Ex)KAn03S4u0~X9 zj~}J3$on6Br~E4bC=4QUoSYny#t~%D^eB_L$ysqqD$6 zqgZ`5N~G2UzaJRRY0kk|a^}V8o>ZS&eo71h@9PL+IF!VewSAzKZp<@; zIDyVI+bEOe7vWFF15K;?M}tGNDS$ct_Z<1tr;iz^J>eM(pLI!oLaz?x%D$EuUgMkv21tvp20 zLr>cTBJFF-h)Xi-Zdt$rFA|z9GGZ=Xj ziajSJlyKX;BeD2v-^D=HWRa_-^Ar)8-Xl*vkxAig$dt2??>2hT71vdIBTX12QRqng2sw9=<~!>0cU5I;t%E ze|smG`90%<%T*!e8d_ZVyf0;7YAfZo-38EK?2s{$8B;`)g3VV_R0PPr)(A zpz8bIXU!^mw~yxzP-5Klo8O@KB^uRloAMxxQ+wS$c;h?Qqi13rUyeC#U1%#6zOyBS zE$jL)Jd_koHRKU^t$p@(?6akzxOE88=hXF@rg9x`|% zZ@rzvXk0&t!CqcB;0pkzmVUAedD`Agp26u*l3T9ZcM2l?AyYjdEfLP907e2l@4NTT zi_Jg(ziIq`d*>IuXDkM}<`aoG?^dEZpg-4vx~XpD7x5dxg+EmZlxwqVi3Gv}Luh(s z5x%$R&zL&gm3cKtrxO#u|9uplZ~`2Jo9^8{);mD#(Y{w+LDO%4%X*d5<`e-~lUIW~ zsun<>IRn#$j8`vVH5)O#4V zZ@2t%4|F2op%A*awqW&(i!oCR;nxk%^6#4ocVhAC+Xy`rmqst%7Nvo0L#g{A$*DSeaXm#P9(2KjmN)RF%S?J96JePX~oVgBa?M@d~n7 zd4|s(%^s!y+h;d>VD!-CU3Sg-KAO5>{e>_++`?^spz;Xq38;_ru zn|0GXY$fz;IRxp-P-ic;e6SiFTRNZyig4lP7jQ!Ns*JHewI^15``aiz^US@)u1`$- zsY5l6{)G#%{l*)S93IYCANY6W!FX!gml1M!pbd7Rtfm-K51WPD(gMSxn_AKW5Oq%RQO}%x+bh8&r#uDh>-iFN!S711zKyi8DO2lx_^7~N~52oEzvNAab z9Du6tei#0kGe5z{zxU8|2WWOt^PP8M@Xa^HL^x}1x5HbEpC523x_z9IqQ@ifx}unI zj%w)$5`U2jT+(7hQzHV~_nv zWP>0+dZ4T+gr5*o%$(V?5!i3PT`RQdIwIS*W8j^4K!RiBKdOrSqmPEKwsx1UiTL}& zn>I1p0-GKu&fig7jDn+%;>FHEXh=1ol(u?;ghTx4WQ`CW#ej3@5(L6+FRuIP{O{VJ&=D2D zUIXyYySMRgQE%Ci8K*282`c5gVx3&E>R?f+1GDDlrB*C4l!m6gY1py05nGovARKc- zb-Dysh%3PLYyOP=2e+l)OkHOU>{XXu3ST{6iFJ2wLra&U@AcQw|K^)W^!M}W1C-l1 zo$$|?fr2BBK*7;Rv#Al|-AJ)|Uw)YhKk@Ao!^13bWcStKK;FRzv(T>ej5F9|x$$nX zt}gWc=RX*H_gyv@h~jhFKY3JQ#L|5$KH&s*obgPl8_Ri67}RptT^OJ+D8sy^MWwTF z&+Km@?x4jsQd1+?3P4I2b#o`7YGw@_Vj)4h60Lr(1D&mW#T~vXrCgBPY`@LF;%6O* zbiKXqC;Oix6d+@7fZYRFNN@oB^X_kbG3vZ{+C`t+arVaEp+hx4A7%gZKPKGCuK_4SzW-S48{$Ro#m?qm5P%^+%f^if8eChEFf3ATDm z@V|#%jj)R^x^46j)A?y}1iT4M{lXNKPM(nQaDzR=_~4B;MskybxC}(d;?b;1;EVOG z+VH=#Xl5uC!(?xOr4}%f0+96j54gW=QQVnZmv!m_Lw)3GgM5e;!Jcyt%7xp_R|ZdE zO$vtl2eI{o_1Lj}2#QJ;!PMZU1Kz|i9(n&ZHuIBRj)`?jcz@|xXE6k5ce-+d^S1o& z|0241bLMFmrpKZN0;o9eJQSaD3Y!o&mW`}_>;3m5x@F7A6WSC7&eBp;e&;(VKIx>f z?3%SLcK_f1VaI*<@fo2eq0cmz$vtTjrvLGeY?kTn+890J*=Nyu*Il4#6LzlFu)~Az z9Q8|tJbc!uy{BB&<7{R6sb81|e?bZ&mEuhNvG-qXL1>7^-^c~SC1oPDCb9i`+=;=B zo3GvHc$)&qQ7K#(Z9k&-MiGn@VE_BO-%mI^RYuP*7k9%Uu;EpL<0D6!Pne)$%FKL5 zaIo4FrqS*lFslUE1*0hO4K35rDZGJU#`O z`=`DY3Ds4S<^d$`FN$3(R%Ww1HPqERP&GLZs*`kyUoHJU5s9F2Sp%9k_VHB(|<4TJvZ|z~#KB001BWNklmN}eI6x96WCoB;kt?7m+#BKNAkI=exnJe11k>}X*S(FI!Wy_d!Q zndk!cgpax4f&xsq=pvMU^{b;^X2(-cq2=zo#Z(USY7*IhO0?$vUU*?l9%j@N5MLNv zvIH&n+=Ix54c0J6e15~+?_y=;0jc<(T{z*M0y@=o6qPwJ^}rhBmZXQ4?OVIBW(h6M zO)JvKFi0%QmtkEoI;;a)tnsSq872=fW(6RDCm1xa@BO`76sOyrRtFFsfY2XS0+)+j zC*znt(~0s)g{FD++z$-*1<|mifi+4?+AANKBI6w$`?ase&o=xuqX+O!nSzRoFGj&` zRDf_}BN~46BQ|b4Q(ln~{ZbT^oqaYczWGfyZ)l{A64M>`+=F;uUv>{*>#TC-=Nk$( zngCK_-S($Hr5THn=K9cQ`#$pwyw%mC?S9OklLGAc!ygb@yEgqg0o<_vMOZoU3!?c& z2*1%2&gVXrl)F$rcVcD*Xzaq85Bp35c?k_l708SeHC2d}L_D_bni{hL$i*`ZIz%=P zz#8Ex2iW(IUCR}hmxjcrZU!a3tWjMP*gsFK`JBll>LvqKQ%gD5-}G>!p$M8*HDUXP z9ud3AU8Yu3n9;cozg+u7+E6Cilc%P*4a&iE!4$+CD2&*HLxZWh@aM z4UMAFgX95dr58I_#-8KI_cXn(>XJ*?EThr3?w4Ld(+xN97SORy!&X(ydG*!ZN+KM1 z|9!OHeK*3JH)kBG^^*g?k9C#%^JP`di|C2DPb5o~bpcG7Q^`Ug`_{a^1shini||J* z&r0Etghb6h5K5rO!#&%tt3OD10NEff7tdh77*zrGyQl4a-IrU*Z?0jg#m$xrT}$Do za43O_vKXe!t%TFbQ6MuK(Z0DA8<(^o9;ad%@9>coWqLocXDjYl^*^J~238VtVop0QrnXtPcgj|Y=)z8QH39yn_6@$lx&XnWuR)=8Dt$!0^j z(4odw<((WF?MAzDKXU>rrpPZhTXOGzWFA0+Jz^OnYI;$JaKO@g5xlPTs;HP7j+tF8FtlQHo}KN?fPifRna zx7@-O#29s>MX3V;)c)#MV@j|`-JkdZ8Ii8%pGV7`cLGC0JO?mswCW7Opo4)jv6_tR z^b&&+prLGH4(j$TMowWmQ?f((XeIyXp-^l#gG{mf%_O#yD- z@;q|O=cL9P%BB~x-EvWDUIgQd+9jaJhcWliA{3Mrq^&VUC`WH|4>qr8MQDI8cx}uD zBJ4jF#l!FZ1_8}9tyqRp4hKrkI0KcJTrw*9GS-Jj-=p}GX6F-@P4o2A=H5sJrMPhN zWK>^u6>{dx8P)pBbD))4+Mjp={cpU%adKPCFD*Y6zsNe%^^LX3hlGv7j;0%LH zGv_dh+l`{rPQ#=tuN?EakL5llV==Tn`6N0Xcz{8@c2C#tDZr18{5c|C8l)?E{c^$^ z(d8YOGOGw>wM7{XU$AEgi(hJl;`XI`06~?`w7|9+>|DL|=lj!qAc8^19Rjf=c+Tw` zpGxFb9>M$a`NmdsXV9h?SWEAj2VhN}Y%Q;{f)HjNG0pgmDR@FMkwjZVC$_EYL?lRw zxmYw?aEWgE;4VxHr_Z3Fhe?g^l|T3aJiB?uCpCBwj!mHuVe0mnnuNzUZ5pf6<{fm< zxYh)t^B!Km9^L=^C!-J3az?UZDizS-^&2S z4fI@SCTiaN`Iva+m2l+bj76@;{GCWw7n*6*`oI2_@w`9Gn~d8Iy#i6EnC8qny~Id! zj{}pYx;b8ok1xHH3CZr>s3t(DO=SIgwmM_Hy`8NF>76hEj-nz)A@J|B&rVrS zRtM?o0wt|fETW`4-qpn_vXt9Uct@}&CBWkme#0nf8mndW{O3PN_Vyy$-p(q+v_CqA zcj{CW9disj&d%&V+r6gi8rrY{TYvXEB-gLc2=)G>@(}!WzjKgu6TZ(!-_R?K!Ks{* zhl%xt@c1$hF&pX{#FGDPfac<3*k%0BIY9yi$_RrhaP>93+;HO&-`g1&R4Raf_AM*_ zlq{?}$-aIQ58!zfn00=c7PjV!5J$eZAG42{1&1?(W-AI}!%=i>>&5nU?Fa`W+$3m8 z9N6+6u5Eli{hsSOdw{GN=Br;FQ;a-bC86t~gpy#(XiE!2N{IZ%&>n|a4s^UbHGQ8g zff|b;wqpnLH13KD#w()4nt7dF*x0$#{zD$9z6cco1(W7GRv5!Pr)T7mc5<9|tqNINdiFkIUf-O+IOS$B<> z3?~(Ul3@QnKX~kG>YOv~OdISXJHbfU>3EFjC024Fub}ZYz4SyBIj$h;=T1OjRjJ_# z?A#)VDXeMvR;7c^Quu?nS-2sf-|$9^FT*0HZE?!;E)r#(9qk@{0X% zx@ke}?Au^hKQ=Aih`xRmaFLe~f-bGUA^egKojLqrM|l1BUu}B+Hd-jdJREDx3cy+- z12YaOn{@dL%TbR>sDIQXTfaKMXCS#kh#k)>#FF7SzHS2CHzrWGp?e(EXc3m2xNb)s(a&$=VA z;k@s|GqD0LpPN;CM`CM`<=^z)CiM3yfICMNy?Cr|pg;6)GY5zvK=#p`WJu%5o_kN2 zKissg%hnT2R0yL|mq{6obpc2L2nwBk=k~Xqfuc$?wb>{ZiMjw^5rLE9SbczmUQe?6 z08nn{4aG{c{sfJdRN&=HZ$QV4*#_@xB+K8ky^{(5P{;{| zR)kklC=W+|2=XKwY-)rtypFlF$nSv`>}hNM`7BBjX^ki80n(OD&2&zrQpQ*m$^&NJ zy8db8Pn@5|Ws!6L1*?xt0TJT(s*a=(Vrf%WCrGfmK{6JGI7 z)w}iGrkf5u-&6#X>jv`{MzLj60JbuuEQZazVbQHh)!eVyiEWH>D+pce9|k*=_Xp_I zC!h<8gvbl%H+nLLL{|eQ)aIjR-b@4va(MHMeORcbqoBPR$KCT&6z&j+PzHJNO`ne1 zpZ^@5Nt5;}!AGD^tcaILps}$LL0To|?z@3)+p-?1Yf?Sly6_rw)X&P=bVDS#;lU7^ zS8PGk`gW+fq)<`)d0pc7l`(1j7B66 zFyqJn{GK}P*#FIDso6m#yH74!Ejz?ya?DU75`Ny1M&VJSvk`u898(V32Sqg%aMN;- zQmKV!P{YHRw)j;X@|QobMe;K7^XVt~M;w9b%P(hzpS|1ey90#wL?RJ1H8o+F+8zJ) zHz*%|nCfjd=0b7SZ_Mq*`)6H(wMUHd0(z(y z@xC^=-APP4bS83&3y@z)4b&nt)^${DT!kZ^ycFOeMTjE0c-t&RKqjR7QqkZ8tACDBj;n)6KGuL>Wy1jOlQM z1l8ZP{BQ2E`gv&;VRIbDgr6k?9282cc6Z z+iR}@OO|lNC(9NpD8{l=&c(W;PeXzhcoy4GXFrB|2hg-)EBd>FaO9Liae53?hlf3) zp(T^>n_&00`FttODX@LwnbvqIisK4r8SG#9?@q5hTN})Hc z5oYuYq^$^h!#=+(IfpAf@2mfR9w6@F*GVW)p-d z)Y2kDKq8FXB0q|%ijZGfiaMWy1K)TSQy0GqcQ`ZqkR4xD5!io!)Zc#lxTdUqHpF%J zoh2Eha5&7u9g6qq-#T@!QC|UBe)0ga$tA;K;~p=z9&iMfoOuy?N-ELQ+=0I4PW0~R z!(d+oy2A(6o102-<$O;QmBApk8M8mlqYowInO^M5)Vj*#=AA!Y*!;|ml&qK=VKOe3 zovk*JmI&t*)fZIVuxP33&GC$BtuWa%V^#uIDbcYZQYE1Br4lYr|k`~^8 zIT^p-%<7a0uLhZ0bBqbBtK#C}VDrDJNj#gzR~N;1LOLKxk2NxEFeCSa6JZ2t_PWBw}zo5~vy4 zfeW|1hN zO|Q;$^Af59>x*mgkC|V>*1QP_g>=N?DioKGt?|Hn4^liHp6JRIUife<^XGixv*_!U zv!ccr^P9t;MCayZt-n3uTp|2Y0Vw_-*FxEJ&2~zJf9jS0y2d%<=u0ggKu&$gv}PD7 zhs2553i13sV~>h@0UM|*i)pN4EOG+sEeQ4?8Ksp+_|PoIE@iZe1oT)Kmu-3$hYf6n zOO9tWRg{?rsHs8yop&<&^q#&&Y3zGX4-0c*)Q}?{6Y+S>0yG!ZL8rNCP7hQU&3ffRoRpn^uL9;g75UTgIYGc^HYFVOc7PJiz30e!akRxkMDHxhEP_CrHMAasY6fOU;YqcuDX{Oe!9`t>~AF`+!B>Q36Ndp*A%f1Y?0x=N=) zbrGV&Axnnn$Z&^^L6_kTFI*Ws9=Yb1na^)b2$V&xF~^uPZd%DQ#XdwNnUyJTYfn)v2=%o6JELpNBR=*e!%Lli*7< zM+kk|jDN%8XR*oVnMbAvX3qN(1MPhsw;XkJJk(F~ZOq}1Wf+$Ed5`1)GUfr3uX_HM z-swkuBR%Ff>y(|;BRoTD01h9RYfq4kh4%QO+SDY&8}5@x275$KAlf9gB)+%iDIC?a z4i2eAW_F08z&~pi8vwPdy|#2^Bus5>Z44%Kxm@u1d?+d^LT+yECknHYpm%h1utb(D zzL_p=FB0lQLr@=o92!}CQVdg=Gnx@dxIOsi6}RH?MeR`h#ZYNo2gB-%iOmw(5q-KT z=B7k-cvxed?Jw5>mMQ7@Ex%NP*v7a1(sut@H`*AWS>ykal3;m17_m)RhSSMLC{DhS?#Iq=k_gt`-p*oZmN43mM=P<=6g*N4uh)x$f&!)> z4u>PfUCJPA#>6~vdYRtS@n|FArcj2GSF-B#A7S@d?>Bi+`aGG0x_&)0!tGBXICLS6l0ZMRdqtQ!d1w9@{Rj>`0u6hD9hC9;QA?PR$pAV&HpUt>I zJG+sPkf3#TcA|^8Fk_v)X*wV&2mMFNK-WzFmap<_Qpjol^l$pl{%`X&7=%FaM;T7C z`$ZIGDq7L`(m2MoYeANug*~z$Xq(?}w!OwVsSZSQ^04gWZ(!Zgryw54Wxrqb#wN6H zPmL)zMisJnAroJp5@M0inn#&iij&z{ooUn?(~U4IL9Bag(~g@Crqy65^pWcUn5RKo z$JI%rGm3D?Jius?Bn7Cw>>pQo_dWiSZ0yi+|J|Te+qgtb2_%b@)2eCqC>6^N+V(ci z+wua+VuRL>B(6bhd`iuA*X?xUycB*4FD#ayJU}qWJrfO>5`w)m%8^N^$Lv96EKkBs z$t?*zjbLB8l%qMc#u<5l3~P2}eLjLK5b@`tvu+=(IPE;_n6@8uJ{+9g=f)MyXjsz? zC~{6eZ&vISa?ft)dV%X(< zq#QsRhO`EmvO~W$t>Ei7zpT0gN>+-raecN?C?>|9#l;E1P2&<-Sf#w#-kAvF^ba4w zDJ^fqO^~RK8BXtA6&0)qwlkwsNC+vWCO!=PXW6L#^_5pRFNTKK78HQOE$U7K&E6rU z@DMUG+O7Tn?R^Q9oOQM5ulB90x_a+)chXDtgaBCp;R&Jwqv#V61#y&7-{FZmIO2Pb z;`pB9%z)>ib7tH|#^tGq3T{Uhk>MdAs6>P$Bq2#>>rOA}^xj?FwbxfQ=l=iy`_;d` z{;IpWlaPSaIen_TYy0Z^?()0${_eet@Jn|727qATp)jY3Zk91{~L z(6VY}>2>k4Qv&O!({mC|F9y949Ncy}2CsQ9ax||zP5htWI=lX~U}Q z(wg!jmpLCNZ}~-lpGg#wKmYXGN1y-Y;W7;{3R28H=apO~TNu>&&wu{0zis2zN-UN> z*GJDDnJ`F;@VWBHtWvYr*qk?%L+8X1ym#jhaQ?)>Vx>~IbhLHx;)}78hBYlx5b5|R zS5A>f?r<`M@I(=ly;^6lkkv-AO)f0pqM}yMO6@@ zzsaMBk&=oM395e+bV=7B@9d4;}*fITt@E~Yy5O0HGJPOaq^*8lka z9Yc3r&fQ-X!ymTr3z;Gv28l*_frFXU13SwvXKNAFIy4=2p3>&(0RJ zj?Ggcwpuo|Em9WAi1r{o09~12atc>=KZJK2dT$XSXs+@rkgmRrzt(@hoS=PA87 zH8mxENC`@ABn`)pOU%D#kF@-Ygj8&S=NO4f&N@FwDVsIbYZv>JcmU1v8xAM-2b-`z z(1^Z3J@(YD!DJwcLZBKBC&9JiwW7^FYd1Gz`MI68N4fLKqZl8}mB1hA2^OF(&(p(O zX||^%1C}?Cc=5q+j{NWz+V;WJ2i^zF*55Y!qf2WgmqIU!aZMl?UUx}zgWts} z06m|xK_xYT<+uLgGog)FeXv;2;4T5JE>O}xP<-613#iA<{6BRLa66^q3N=m-;<^{_ z#orA487}e+>LM3qQPL+?+Wm=soG1=4A0@t=HB1fIO#w-QS+ z$KubUEal$Mv~nu0SPbURINdnpt;O!D7996hV<=FM;czqJfg0p|A<5k#o}-w{Nx>>< zQ*lQd2mS49!dSU=CEQ-hfS2A5@9anKA-S_x1i&tu`piWyyNu7N!~Euv5ba~%WA}`F z=ccb%tUu@eZOL!e?@~z?NLLnamNzTI)mz@$-f-jpdBhc|FZKyoEx(EFMK74S5oS(b zfoh|a_G!3($4P9dJ=rM%}oQOTjG(JShPE z=oY_3nJC@(&_lqBF9H)2GB~xv_!f9#b>SsKJRirV0?clHUt6pX91hfDN2mqI{ZWj# zt1um`#YCt9X>S-gj~`A!PbNX-MG}g*Cf-l8{6tO>9=d>*_8?Yo>x4H*3R8OPJ1~TU zyJr^g0H@FL)6wK&qv@HSeBvF`2cDpLIh^^S%mr_PT+sS6=&RJQF$^l{6r)&R#cdD$ z_u#tAud~ObQs@`F32P^-!o^u=loSnhf( z#si2`Y1b%IfBA>jRsZc5e(kBMcg?ZzHvIxJkW`+)6c*J;&$MUd0hUi3#chB17c|cd zmCgEyfce88hI8{~LC~^#2o`#xA(Z6x8Aegc@mIfs<1c>^#U#_^Szwm2sykwsT)6z6PQA;voTlL>fdrx2W(f^Q}!S~=aB6p%BB#|i|3 z$ONj8s;Wk^DuQ%%HPZet(p6Q62dlAXUq22X8AZzJM%EKV-d}}+%ZGw5hysrgR1GGk zMHlEsHJLC4L2PKJ^HXGLN~I&tl~h^*Lya67mU*z|f_4NWl>*pJ@6GrUbk6%4~;E8_C`dd?<6y=i(tMRz5a?TfUB>j{N3UVFTIxL+)$Q> z;7)}Z9s-v?YS@b3wp@gCWSPo-aBqWL65?~`(6zM-?OWH)op`6aN>kgkLI$5MIMDmT zJ{)Y_DhUfV9>9_#flnJ8m9U4~J>aQ87B zdhQU?SqW=orSMZ|MEXy8{6|}kDB?T zk3O#`^{LDIVm$yWjm{4;y}+`Y@BL(W^V@C~9FaioAL9;EjZM;5rtq> zeWp5vJS{_EX%Wch(g=qOSpB-SsB2#)n4oi*dp3@dhKP;z4&m_jgNV&ipRKo~#ZX)X zhf4{&L$N%I^aJ(`((!V(M?Nl-K6KC4zv#Jmsy7uJYib=>x2+YS+PNNJdSn`Zc}i0N zvm#89iaxm3bV+jyrHi%vblkZN;(H$b-q;Us{`x%FzhdjZR1ZL}vlhlgb=XtCth)8% z5B|p2vc74dJa}>1fU^8*UQjeXiudmP5iaX}0#3bs&sy}NgShlkKv^yjS)`?Rk@9YGn4Urp|$NTUyF1z>~wW5-j>c zxFW3P`sg0u6j^?{zUiU;ePiFc;reWBnC9h^MOpuoQ2^Zw=pz(mK>VpkzaCtD$z@zQofF)c6;s9a0Yt$2{=a-1 zmmGTrZks6&cHE(IEYq^wr1|7ut7K1kLfG%C$Ij|@JX^m3vym3S8x%u(xd)bn$_d8e zJS>cxdAO5JE(@uu_F?(jdMsPj47Z1{e0l1Wku}6nsUv%aF*BZ!Sst?Z%)3z&7tG?A z8Z9VfnF)Gkj6nWK?d3I_kW{Q9+wY2sAOxR=lgR``L$e!ewlpIUo|^?4>l?!^$^xOdw+#hqx-R8b`ajY%^b>df%Ove?j`!* zaG>8E#$kUQ4pg;ZN9`JnhFjnWL?rK>EGb($0Xv+s9ZE zF0X^zqfNaoq9C)`gp)%Q0F#yN>mJACNK!JT+4G20rkbmxvO8v8Unqev^`;l#gmc*p zmpQXlP4CXG1Uy@RF&Z3WC#J-PLbuOZ<3oFOicXp zvo|F6K6b2(^`A5IvqE2Awyw08*ITQ@f-c>3^F5!4ocorW#h@g+N+|CZQ;>niA-blH zqj`8AHV^Mcb8-w_$uV@K#vxerI;B@>m0dqUNk)gBXU-*3U&(S|D64Zg*nmNI7ze^_ z=#H$wbX7f^z6ju!oOg-28Ft7i2pmWX!BWQ~AcdlCI2{E<8$(#Wss^>K)o{8L7B9*6 zSa+lqeDf_rl+wYJ%PGh;-s{eIPiU*F#3H_412;D^w*#_+=`J< z12VoaoSvW%e3?F!R?QTC>+DKC64TYo)lhDC0rktO(7Gy$Xk8T?F2?He(lF-N9oL+A zT%{=EQwa?A#4yqyM><2ajM9l}Qu$;6%$C21`O7(Y?P6FJfIL7UkFK>rbez*76xVv| ze(osxdvcaK05`>`EYPWdKY0V`zMcDr{_l++LXuW#*ap58aem4v#mPV*D-qLsRHb8xCdSc_oIqWA z22mr4XeN&8d*12ps@$w9K6h?sOe=&nYnYy<#wR8BZ6g}5rm>L;I)N; z1t2y&f!XnCv~KL2gBXNzs8}JLNDBq%KN3SSnOAvyeR8ryqf!@aJdLWm7b|jn=`;^O z-^ZXCLV1wPuUa2M+xli(V?&McBQ&92UtG=0thpYbK4PhspI)9BIXpY_&>ioe+4G2= z^QVfhmG8f3?5_*&VmwB<=UTl0*9L-(*WK}!@at~8%N17hIfdnC-cKq2gt0TZVA>-_ z)1hfTDCK|YZ-PnATvl|2d2?xaj0`-vl=z(}2;1KI1`)Io_nnj=>-8b;@nK>-hTVTU zfQeWRhKGtuUO3!dmBLf`afRuHZFIUwoP;oyOguAl>K5mzOt~-QLf6JR)Hem;33x<0 zw4~8ZQ-<+lgE;W?USyIvtT?Y7T^F4*uV>3;4NQ-x(9=DI>G6zAjZy$b(#lk3Gta+} z<>$vW1rP>^S}?N+hCNudp$d)d^|sghPru%cM8{vHd5X z8GrbWf76y+knmdxZ%Y>bQvrdjUVwLtnH6I41T7!^>1RXh-*f|6TC>Mgiva}RU+oz- zC&E$sGM|N!oS~&sGCQHBO4IZ6M%LUjz#Hnfz4ctwby6dDX`VekIfL%)hcPxdqcRtg zHXwU*Nz`RPE7OB=!hxA*H&^qhQ7)DYhx|T-_k&ouu?|&HAKY%P4BPKPKGEHWgU`N% zWX6kv!wpX%ftJ;cSa;c$Qn;ke6Oc}mCOT&@*fWd4BQX>xI#L#2dI0skO7i?V=C{f- z(;w8t4H__zH&9dW!Rk#lh}1{sD1ebO@YK)u2`-YLL6~&_ZitaF(Ncuo^h=o}5(l3A z`RKoX=q>;x^maLa+hMnhwdSV+0_jSi57ZT#<^@+MP#0eDiJyJn-?43jD9fv{h?4ps zXW)puzaqeiMtGqVWRb`d%!)k$ba7<0QW3a>Bawd>V4&-q2CTk#;~Y<)O=r>b!cp|@ zrwI=-FD|RH8pfypo8$X5oXvKbbMAT$lZtIlC%QJ)Vp&HOyk572Pg-7I$Nbb!+qdf= zdUy6BmGjGHc|{v>8F4hOsKUB8Y=t*4H}j%_7UVKHOpa!8@WnBtQ+cVp!klRnmKl3V z5T+5SwRaNdTy5@A*q<7Y+B*DLbzU>vKGpB7N!7&QIClJj#-dOWj8eKTbzdyhamHy$ zNIY!$<%&n5*`V|XSB z55sVT0Sb&2M55y$rFR-h)YQ4L{>_^atfrBRmiN;FXiAml(s>*`FpcBAGg_sWjzhsW zQwhr0IiAqX6<``V-~+nWR-tuGqb(#m^xR<_>&dGk7#R>!db!Nb-0;FYE$)L}gjvpv z9GRVY@W0(OvsZ9^*!nZ>KdGF*-C>+c55P*mJR!Y6COkp&`@Z__(D`rwCugKlT6k&y zW<@BOCs!q55TFS=hbYR808Z2K~4UgX?vHm=H{Zt}_!_V%+;Nfw=8!lyW z($_N~c|i;T4u^8s_@?vael+c7Mq<+sq6*x#u@emf#$!z_fw(Oum6Cvs#f7dwOFqtDl;Npc z7DD@)YJ{qNaJdwmv$6af=hN1dBY)nH(cvUqzNoC|nt;TfwMxOh-<`m!3)i4&O`G@} z(;=x0vYtw%-?Ql)hWire@0mp^NyEbA;&F~^%s1fLh*DXT0GH3f;|1DRhp}Rv9Q0wm z%}&hX+20(7!xz*tK#B&T&Y_5(mSFjf#3YgjpZW3F4{rXd7W8tpS8sD%a?bzd@BsW7 zdgBbIMolGH_R;%37hH4cbz*mvX*;FKmV3lxXBcyZxKm8T475UtlpvEf3pe6~t5SYd zD2ofPy#y|IksmJw0idByNB0b4sCQasVM~TNkqc)V%U9nax(MAqH==cZEb9s*S{ndO zO)vv(o4meg!^~tba&$<<9`U3LPJcvAUXWP5$j0P1PywlIi4$&ZIeiohN?qw z*yQ9(JVXIVB@K)Y%wl*bg;c^oE=%)9<+GRrB)y|Hf3;wU@6_DpN7p%Z@cHLre>yYW zf7pkC0fqGiHG>L;%9EP?04iCq2qnlTrjR(g{eiLX{s-#tqAU-uHsuKqRPb6f)wvRL zeX1dl?pZiB%20^MsRZEh1eSg5m;Wo+dEx7wWKGP#lIsJG;@$u)R*;sHIQ@C#(^3hj zWO<3tFsiD(aC^w&m(T0d=NY*?5>vAn+;t=ez{E%rV<*x`%w~lmWYc*Vs`E=48QBZq z_c>70=tswz2!av&rlQnr65Ahs2{}ik4E~oD;Kh!jr)l4*!+-kS$oD_^d1Pr5iopH+ z!$B|4_dn^N*Qyk!O98~W&?ZT+5^!cH;10J$ns5Hz*Zdt@w$l2{B85TwjtY0PR}l8u zOoirQB&UIFMy5o`GZ0J0$f2e-hx7j3R=CA@LM;KJ4197njp1Wcm>NqXlU5LmLSl~- z9=8+zkVgnaq$U7g$SrCDML4A7#`3lThNLo>8W=_2-X4q(r{MHgt2CjaOmnK0Q>!A^ z%E5}PkC^??YH=WaUn4yWzc+*B8(Pt{vK4`7NCdi-+z9%m;#tHd(}+!Fk&5T!dS=y# z|;D)IH67$4E4XbpNiM zC%${*tw<(0)ycVj?(ouu{}rVGtOWXw0rCQ*1Og8Eo7dGey#H(8^0jYX54T^*0;M>0 z%T0gF-C}evdG>0%WPy|*Mv{j-k>^AenLI%p>n~c4)(xE{(puyW$rw75F_4<22@zCX zb-?3u!RvPm>(7vZ1;z9b+`*=K0OPO+wE+ zex+Jm`K1&uS$^K9EbjBjXEShQVrXcKVA+~h)V9^z@^ltb3M#KqMoMt2h)M2om(De_ zc?0@xyPr9NOo3pW(e?=5D7hW%P01SF_8KjTxJTUdMPk%f$+)W$ZN#J$MPZ=Lx zZF)*^zkZ07nUM;=RGFu7JivX#x)KC@o!gok-*ea9zV`FGc+RL~7jTw{l|jvx;3gSW zLm&cIJ{d!vn#@HZM{-$0-Z;*A<0eEJqoqB$I!M#)iAIkpm&do`?j)z<80#O#iGxF! z7)c1MUvPU(sG93Y{*blO(#Mq?hM4Eszq@zx*Pp*7-MfQG6;|Ot zZ9(tL34wG`sdx&L1EV-`WC-Jf)5tjjaQdo*@H!QH)Z`8c z%P-PMGR{_>V9rD~5#&o|FToB@5%Y!|UK}W>A?aB}qAoOaHlm@u3E_q)C_Q^BZ|Qgn zy}Nou+di-z(oGPWdE=9Xbasa!<8P~cRcp+|M+O)@E->)!mraDPD{{xCKQ07 zP&rRPHG+V@_1xOJ8}9msZ~3P6X2wKMe47?p1#dh(PE>+PU}#=`O!VP0smqa>L4C6q zom*Fu30*Z-bN_atjMTd9Tdi-tMvcZ@)D+vY%iNLmm=*7XH(g@v%Dx3m^4j z&s_QEb5Kl*KzTO$Va|~72L5H8k=pnC?=SgRoOg*cR975-Xi6i`;$$V@jxq@mM2a*S z6QntEv!bAunMJhLg%#&^puVF;Sjl3HQJ#;lV}6PQX2)X~>p70`fic9Vvq%|kIB7PI z%UhhXAUH(Db)b?KQRMFn%dgrKC3mOz`9zbD9sz1kuk-*^A!dQ+%#A1nYRxyaM%I;2 zArNvRQWru!L8GYiC?Zene26=ZF4wqN;MNkTq6@h3FT>V|x z`ctYiJ@De6$A5C$?YSu;WeMSD=tGdlN^@9rdt+tau=H6krvm7nK<5nUF@Ya}K(P6P zKe#>6x$SRV;l|?8Y8buPLdDCoWuPcL0j*F<6M>EfPtit6+AyC)z@0{IOSLF0RX5kc zLo+XJD!BZC1hwK*aZL6PV`5+wvl9uVGY;emUP+yH5s{5hfi`@qWI>#^Gt2DU;%$js zM<~wakzY^EM5TjJGzAm(aNmSDW=@l;v@vN78Gu|GjzSh5R~7+E;WkB4+fs{YOP%ON zp3g`Vzi8^j1V)YwV*L0FQU>){Q!j&B?!nY&Zi8F|_9td%s^|GfMt^wAmyi=H=-J}y zRbG9Y-P%Mmlzq8%^)?CLSkwLvy(BT<2mH= zF67}w-a!`Mt2jjT6;xZKHmk=>rwDSI3=T~W5l>`e-oABCkT^)b?yLxERH3r=CT&3| z11kat%3MPs$Cni(LT4cZmm>$aGY?&d-CpICI>PBuE_gYDDR(}9zweJg2b;kUw`Q=aZDC$KNK2ham! zy--HgV!dK4Ql$+axcjES>We?-t!sCfv*(K@x`a!_7+H)W5!+UXKsSRzHUm&Efshxn zlC+J5_70G>B-W|iF($16F+$yfSXR(79wk(1k-=1b0)iUN+tFob7G-tJIhpEjne5l+ z@{~F4dJCuIf5^!VChtjEdJX;*;{$eQY6=E(eN)>lXzdG;HV_uuwYi|~^YkTP(pQxAAwHsL>0 z9-y2Oa7<7FiK1`+FPDbTed`xIO{*hLy`O+dzN#)DieSPBl5iVp)QTZIflvYx1T}3! zr&$OKP0&fyq_`7|C#g}usw%`{5}O)ArC^Vo5}^)6q&n4{Ac6hW26fZY%UHl8UcbO=RyGb@QW+m_-YXVB=OlBE{Kaf>p2tCU&59nSj9Wqn%3 zKc9{xJJvgs-1Wd0r~d7$f1F47>7tx5;Xl&~U>6f`TV;t7csE_u)^O#g?({URTJMT9 zDQnFrKw3(ZN5r{vjpv?E&lZ({oA5Qkka-wA$Y5bthsbP7Z3LlYT{5pnpSG;#@QD1r zDt3{V(D(FOB}T9O>5u7!2YrNzK6}W7N6e089+f&L<}d+8IxHbkNKa&DaCED$&-&yn z4?`l=6l9nAAx3BB#GyTtkA3aGW)3|`X-*yXaHfYXKQlYkMLZc~y|mY!DF~z=t)3m? zIsw5UT?u>$H-wsQ`0_`*D>lE+SJ&Y%S(WC-87TmgRxK(@8kO2WJ}u!8;b;yx#rh@o z9NO}3$=NA(fSHU+QxQ0rB^{8EKxA(?>m#B~f<`=@!G=CjPUy`uV@braGO}Z<&;S4l z^GQTOR9K;rF!0)U)h)kn=}jk41bkXUf`Vu~dsGbsFsaoV0wU)85jPu!2?*~klstcN}wx(xAtwHdwuY{tN+>Cv^pX z30?L)fe;3z7?dTUegfJp#(8w+7I9yKvTSBVU|K|Vu{BI@_6~@LBwQd6CYn(=o>)l8 z7kYdSkx6}5r3CV;tH7DbZQ6G(d0anFR+b=7lv6FCk|oO<-enC#?>4hqehGPcW|F%e zB*^ou!uBlue6Qnu7XFtl+f&XzoDCj8R|2jWb61%@X@V3%`~=RqvaRN-f4IZjw0gZW z+^DF@QtV`I9KdvlzDnZSKzDrBi@#E@AK3ycQ^wP(-Sb4 zM#zB5q{t35nMI+LsA#ljOUkTF9e!|*pUcChz!Ya~N>-Z`pb~!nLPEaK5i_aTnWPD>PjcRn~qoBf8Zt6rKGu&;3JPdX8;ua6E z(qT_I?S5JRe>Qo5a!R2080)bCDTLR%{>qm6D?avVSAEyTp4wKxNu$J-C$P-Fmp#Uc zZJAC2hJ-^rHMxk1xnEbGrpyCoi-IEMSo^01KpZ?xAGu^+VK$isED){6Hta$&lu{xKkHD{v& z@OM!qR?%c{g+#hH5H*CFw|)N7!1~KS;i+5R;;N>BPTKr?h6AGiR%J=3CXh+V_DV@V z)wU(743FLuEG&I-=4FY+H&wSxO0&|(E3*7HO0b|pD34KA36&UV-aw!1Al{y+=dlrF z$NP^b_C5NQ*yG>$ixPZ>JbI98hxgA|re{9oIGYuKAsUnDxN^*K0T<6$89e@~=1|qO zU%1h~;=B*IYuo*<0Ie@(;Z}VGChU>$NWdY3lt7*)Mm)>~(mQHIo}W9^Z1VVeBDIL7 z7Ep?l7J5BfRH6w*_+nyy{c%f{sU!hV+CC}a&R{Avu;<6I2k!i7E>SC8vs?DeOR;EN!4tdHT&kXF5A!Pgam zDGhp-=Vh(zE*1*C5)_~uB(j4=tPFx}T7CIP&kJn0;=QigwoBdN2B#}bV@700Z3*8W zc^H`#97LHgJHGS{>PyJupDCeV0B$WfvV@>cd;&&nEN@H=Jd@b{+k0oW-+MrVJ0#?~ z@YCP?LxO)+K30`Rmpeun6i%1FV6oHk3?%nQ&wUO$pZhgDAW!-h|a8t9J zwmf=+8UUf4UMU)~Q>+A+WLX&a&l8Bg!Yn@xAC`hYI-EMb?^m&>?taJ^Jwkka7JL?d zT?ts|8Rx&EtiCl8szg!F#^fjDDRdGWr#8>pK=+lL1*$~xbS{xzK+_e08}hCHbGsW>Rz==;%bPvTYp-&J8#lQ_ zH4eGp&mnsW*aIvi)C)a+x`e($%#U1JV*B`H-k2HPl|9k@cq2i0aL>A;)=~uYl^6po z-5a(zt6R@^1tOcB{;CFtKO`nU2&hC&BEp26sXV_r4gC298X3s~KTFfJNAvOVUD@$t zyHW?9*^%7;*l|th+2ZR0t}6ilUl)2kz&)!|n~PC|SDONufPmXnlmaj`;;OP$=xvoi zRiK3uh0c4&3g5~Lx4NROo1LNB^IZOL)Zw8S|2{Z8KItjkG&jd?x>~tnl|_gxn7w!* zwbzT@UuyMCn_F`zX2~aKCXD#Ri^fFX&cxwA?nv$V?SSNjiyz(E+X=s}2prh*>)6sW z)Opni|EmcCl?_*I6v0jzbhu>wot1{YiKtA6&VOf@yLH1VcetU=6^yny{GnF3{cTQ< zze*~FU(V1G%_M4jirP3S{zp@Hd7qXT=%sCG6u%RLRjw=*aE3Bdxis?G%z+dZ@t&8yfvG{@1q(k#OZxi3BGPA7vnKrsn6A8 z1iPSF7jjkr-CJ0dphOvzk_qmTcloBMzvF@xu9_8X&TvhW)8&o8=?ytNG(5;1g3BFp zI9(w)-65yPQ!S>w<&0?*a&Z{Bcp-1ZkzE}mHExz@h1zi_-y=toq{wq4TtyEB7tq^EFkHDVKst7vghy`3%2CL`bzxj3i ze)_$xc>3q+$7iJw=@d@=cO8oG?>a8G;`MS$pbNYe*S{)Z&O$+BrONVZd!E%RSOr+O z^14Fkzq9b`_wy-&4m<6Pk5%|}fn+$s_qx@$LZ12l);)(+;9r$6=QRabSat1`LH7`L zu*oX?cF!;OzIG6_oaJXBwSzoXWhe({UK9EO|Y}>{FwFL z`Z_BFD(3+f;|1)zg8o0N2hrcVl5_E@f6i+PF#jdy^TU>07h-GijUUsM#rnE(aR491 zLSDd5py&Iap1t|2gEy6E?`sNB$yq8sM^^|do}W)y^ygZCw;q8msCp*G&NJ8v_-j^v z5!duJ1z5z%ssKh>6~s>1D>>&@CD1?GPQYKY?n}Imm0XM^zV%=I%gUYGO3ryXfqqTU cPqA42f7K8qyiDV{*8l(j07*qoM6N<$f|ioE-T(jq literal 0 HcmV?d00001 diff --git a/android/app/src/debug/res/values/colors.xml b/android/app/src/debug/res/values/colors.xml new file mode 100644 index 000000000..56779d1e1 --- /dev/null +++ b/android/app/src/debug/res/values/colors.xml @@ -0,0 +1 @@ + #660B0B0B \ No newline at end of file diff --git a/android/app/src/debug/res/values/strings.xml b/android/app/src/debug/res/values/strings.xml new file mode 100644 index 000000000..631d28ea8 --- /dev/null +++ b/android/app/src/debug/res/values/strings.xml @@ -0,0 +1,5 @@ + + [DEVELOP] RocketChatRN + + No Browser Found + diff --git a/android/app/src/debug/res/values/styles.xml b/android/app/src/debug/res/values/styles.xml new file mode 100644 index 000000000..654ec9502 --- /dev/null +++ b/android/app/src/debug/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0137ae666..c61f8cd8c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,7 +26,9 @@ android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:resizeableActivity="true" + android:largeHeap="true"> ; + +export default RCActivityIndicator; diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js index 3ab6c266b..6842218e2 100644 --- a/app/containers/Avatar.js +++ b/app/containers/Avatar.js @@ -36,7 +36,7 @@ export default class Avatar extends React.PureComponent { }; render() { const { - text = '', size = 25, baseUrl, borderRadius = 4, style, avatar, type = 'd' + text = '', size = 25, baseUrl, borderRadius = 2, style, avatar, type = 'd' } = this.props; const { initials, color } = avatarInitialsAndColor(`${ text }`); diff --git a/app/containers/Banner.js b/app/containers/Banner.js index 5cc765c39..ae7b75d1f 100644 --- a/app/containers/Banner.js +++ b/app/containers/Banner.js @@ -6,11 +6,7 @@ import { connect } from 'react-redux'; const styles = StyleSheet.create({ bannerContainer: { - backgroundColor: '#ddd', - position: 'absolute', - top: '0%', - zIndex: 10, - width: '100%' + backgroundColor: '#ddd' }, bannerText: { textAlign: 'center', @@ -21,7 +17,8 @@ const styles = StyleSheet.create({ @connect(state => ({ connecting: state.meteor.connecting, authenticating: state.login.isFetching, - offline: !state.meteor.connected + offline: !state.meteor.connected, + logged: !!state.login.token })) export default class Banner extends React.PureComponent { @@ -31,7 +28,9 @@ export default class Banner extends React.PureComponent { offline: PropTypes.bool } render() { - const { connecting, authenticating, offline } = this.props; + const { + connecting, authenticating, offline, logged + } = this.props; if (offline) { return ( @@ -40,6 +39,7 @@ export default class Banner extends React.PureComponent { ); } + if (connecting) { return ( @@ -56,6 +56,14 @@ export default class Banner extends React.PureComponent { ); } - return null; + if (logged) { + return this.props.children; + } + + return ( + + Not logged... + + ); } } diff --git a/app/containers/Button/index.js b/app/containers/Button/index.js new file mode 100644 index 000000000..7477060d9 --- /dev/null +++ b/app/containers/Button/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text, Platform } from 'react-native'; + +import { COLOR_BUTTON_PRIMARY, COLOR_TEXT } from '../../constants/colors'; +import Touch from '../../utils/touch'; + +const colors = { + backgroundPrimary: COLOR_BUTTON_PRIMARY, + backgroundSecondary: 'white', + + textColorPrimary: 'white', + textColorSecondary: COLOR_TEXT +}; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 15, + paddingVertical: 10, + borderRadius: 2 + }, + text: { + textAlign: 'center', + fontWeight: '700' + }, + background_primary: { + backgroundColor: colors.backgroundPrimary + }, + background_secondary: { + backgroundColor: colors.backgroundSecondary + }, + text_color_primary: { + color: colors.textColorPrimary + }, + text_color_secondary: { + color: colors.textColorSecondary + }, + margin: { + marginBottom: 10 + }, + disabled: { + opacity: 0.5 + } +}); + +export default class Button extends React.PureComponent { + static propTypes = { + title: PropTypes.string, + type: PropTypes.string, + onPress: PropTypes.func, + disabled: PropTypes.bool + } + + static defaultProps = { + title: 'Press me!', + type: 'primary', + onPress: () => alert('It works!'), + disabled: false + } + + render() { + const { + title, type, onPress, disabled + } = this.props; + return ( + + + {title} + + + ); + } +} diff --git a/app/containers/CloseModalButton.js b/app/containers/CloseModalButton.js new file mode 100644 index 000000000..5e60c0a12 --- /dev/null +++ b/app/containers/CloseModalButton.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TouchableOpacity, StyleSheet } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { NavigationActions } from 'react-navigation'; +import { COLOR_TEXT } from '../constants/colors'; + +const styles = StyleSheet.create({ + button: { + width: 25, + height: 25, + marginTop: 5 + }, + icon: { + color: COLOR_TEXT, + left: -5 + } +}); + +export default class CloseModalButton extends React.PureComponent { + static propTypes = { + navigation: PropTypes.object.isRequired + } + + render() { + return ( + this.props.navigation.dispatch(NavigationActions.back())} style={styles.button}> + + + ); + } +} diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 104b496a3..e3edbe1bf 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { ScrollView } from 'react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; -import _ from 'lodash'; +import map from 'lodash/map'; import { emojify } from 'react-emojione'; import TabBar from './TabBar'; import EmojiCategory from './EmojiCategory'; @@ -78,7 +78,7 @@ export default class EmojiPicker extends Component { return emojiRow.length ? emojiRow[0].count + 1 : 1; } updateFrequentlyUsed() { - const frequentlyUsed = _.map(this.frequentlyUsed.slice(), (item) => { + const frequentlyUsed = map(this.frequentlyUsed.slice(), (item) => { if (item.isCustom) { return item; } @@ -88,7 +88,7 @@ export default class EmojiPicker extends Component { } updateCustomEmojis() { - const customEmojis = _.map(this.customEmojis.slice(), item => + const customEmojis = map(this.customEmojis.slice(), item => ({ content: item.name, extension: item.extension, isCustom: true })); this.setState({ customEmojis }); } diff --git a/app/containers/Loading.js b/app/containers/Loading.js new file mode 100644 index 000000000..4e4f795c0 --- /dev/null +++ b/app/containers/Loading.js @@ -0,0 +1,103 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Modal, Animated } from 'react-native'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.25)' + }, + image: { + width: 100, + height: 100, + resizeMode: 'contain' + } +}); + +export default class Loading extends React.PureComponent { + static propTypes = { + visible: PropTypes.bool.isRequired + } + + state = { + scale: new Animated.Value(1), + opacity: new Animated.Value(0) + } + + componentDidMount() { + this.opacityAnimation = Animated.timing( + this.state.opacity, + { + toValue: 1, + duration: 1000, + useNativeDriver: true + } + ); + this.scaleAnimation = Animated.loop(Animated.sequence([ + Animated.timing( + this.state.scale, + { + toValue: 0, + duration: 1000, + useNativeDriver: true + } + ), + Animated.timing( + this.state.scale, + { + toValue: 1, + duration: 1000, + useNativeDriver: true + } + ) + ])); + + if (this.props.visible) { + this.startAnimations(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.visible && this.props.visible !== prevProps.visible) { + this.startAnimations(); + } + } + + componentWillUnmount() { + this.opacityAnimation.stop(); + this.scaleAnimation.stop(); + } + + startAnimations() { + this.opacityAnimation.start(); + this.scaleAnimation.start(); + } + + render() { + const scale = this.state.scale.interpolate({ + inputRange: [0, 0.5, 1], + outputRange: [1, 1.1, 1] + }); + return ( + {}} + > + + + + + ); + } +} diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index d88dcaa95..24ec221a6 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -80,6 +80,7 @@ export default class MessageBox extends React.PureComponent { onChangeText(text) { this.setState({ text }); + this.props.typing(text.length > 0); requestAnimationFrame(() => { const { start, end } = this.component._lastNativeSelection; @@ -174,11 +175,11 @@ export default class MessageBox extends React.PureComponent { }; ImagePicker.showImagePicker(options, (response) => { if (response.didCancel) { - console.log('User cancelled image picker'); + console.warn('User cancelled image picker'); } else if (response.error) { - console.log('ImagePicker Error: ', response.error); + console.warn('ImagePicker Error: ', response.error); } else if (response.customButton) { - console.log('User tapped custom button: ', response.customButton); + console.warn('User tapped custom button: ', response.customButton); } else { const fileInfo = { name: response.fileName, @@ -278,7 +279,7 @@ export default class MessageBox extends React.PureComponent { }); }); } catch (e) { - console.log('spotlight canceled'); + console.warn('spotlight canceled'); } finally { delete this.oldPromise; this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice(); @@ -321,7 +322,7 @@ export default class MessageBox extends React.PureComponent { this.roomsCache = [...this.roomsCache, ...results.rooms].filter(onlyUnique); this.setState({ mentions: [...rooms.slice(), ...results.rooms] }); } catch (e) { - console.log('spotlight canceled'); + console.warn('spotlight canceled'); } finally { delete this.oldPromise; } @@ -454,7 +455,6 @@ export default class MessageBox extends React.PureComponent { style={{ margin: 8 }} text={item.username || item.name} size={30} - baseUrl={this.props.baseUrl} />, { item.username || item.name } ] diff --git a/app/containers/MessageBox/styles.js b/app/containers/MessageBox/styles.js index 5c0388bc1..dd10d720e 100644 --- a/app/containers/MessageBox/styles.js +++ b/app/containers/MessageBox/styles.js @@ -22,8 +22,9 @@ export default StyleSheet.create({ maxHeight: 120, flexGrow: 1, width: 1, - paddingTop: 15, - paddingBottom: 15, + // paddingVertical: 12, needs to be paddingTop/paddingBottom because of iOS/Android's TextInput differences on rendering + paddingTop: 12, + paddingBottom: 12, paddingLeft: 0, paddingRight: 0 }, @@ -35,7 +36,7 @@ export default StyleSheet.create({ fontSize: 20, textAlign: 'center', padding: 15, - paddingHorizontal: 21, + paddingHorizontal: 12, flex: 0 }, mentionList: { diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js index 3abf75459..389f4b63e 100644 --- a/app/containers/TextInput.js +++ b/app/containers/TextInput.js @@ -1,26 +1,31 @@ import React from 'react'; -import { View, StyleSheet, Text, TextInput } from 'react-native'; +import { View, StyleSheet, Text, TextInput, ViewPropTypes, Platform } from 'react-native'; import PropTypes from 'prop-types'; -import Icon from 'react-native-vector-icons/FontAwesome'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import sharedStyles from '../views/Styles'; -import { COLOR_DANGER } from '../constants/colors'; +import { COLOR_DANGER, COLOR_TEXT } from '../constants/colors'; const styles = StyleSheet.create({ inputContainer: { - marginBottom: 20 + marginBottom: 15 }, label: { - marginBottom: 4, - fontSize: 16 + marginBottom: 10, + color: COLOR_TEXT, + fontSize: 14, + fontWeight: '700' }, input: { + fontSize: 14, paddingTop: 12, paddingBottom: 12, + // paddingTop: 5, + // paddingBottom: 5, paddingHorizontal: 10, borderWidth: 2, - borderRadius: 2, + borderRadius: 4, backgroundColor: 'white', borderColor: 'rgba(0,0,0,.15)', color: 'black' @@ -33,14 +38,23 @@ const styles = StyleSheet.create({ borderColor: COLOR_DANGER }, wrap: { - flex: 1, position: 'relative' }, icon: { position: 'absolute', - right: 0, - padding: 10, - color: 'rgba(0,0,0,.45)' + color: 'rgba(0,0,0,.45)', + height: 45, + textAlignVertical: 'center', + ...Platform.select({ + ios: { + padding: 12 + }, + android: { + paddingHorizontal: 12, + paddingTop: 18, + paddingBottom: 6 + } + }) } }); @@ -49,7 +63,10 @@ export default class RCTextInput extends React.PureComponent { static propTypes = { label: PropTypes.string, error: PropTypes.object, - secureTextEntry: PropTypes.bool + secureTextEntry: PropTypes.bool, + containerStyle: ViewPropTypes.style, + inputStyle: PropTypes.object, + inputRef: PropTypes.func } static defaultProps = { error: {} @@ -58,28 +75,40 @@ export default class RCTextInput extends React.PureComponent { showPassword: false } - get icon() { return ; } + icon = ({ name, onPress, style }) => - tooglePassword = () => this.setState({ showPassword: !this.state.showPassword }) + iconLeft = name => this.icon({ name, onPress: null, style: { left: 0 } }); + + iconPassword = name => this.icon({ name, onPress: () => this.tooglePassword(), style: { right: 0 } }); + + tooglePassword = () => this.setState({ showPassword: !this.state.showPassword }); render() { const { - label, error, secureTextEntry, ...inputProps + label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, ...inputProps } = this.props; const { showPassword } = this.state; return ( - + { label && {label} } - {secureTextEntry && this.icon} + {iconLeft && this.iconLeft(iconLeft)} + {secureTextEntry && this.iconPassword(showPassword ? 'eye-off' : 'eye')} {error.error && {error.reason}} diff --git a/app/containers/Typing.js b/app/containers/Typing.js index 0e2db6e1f..eb667093b 100644 --- a/app/containers/Typing.js +++ b/app/containers/Typing.js @@ -1,16 +1,17 @@ import React from 'react'; - import PropTypes from 'prop-types'; -import { StyleSheet, Text, Keyboard } from 'react-native'; +import { View, StyleSheet, Text, Keyboard, LayoutAnimation } from 'react-native'; import { connect } from 'react-redux'; const styles = StyleSheet.create({ typing: { - transform: [{ scaleY: -1 }], fontWeight: 'bold', paddingHorizontal: 15, height: 25 + }, + emptySpace: { + height: 5 } }); @@ -18,11 +19,13 @@ const styles = StyleSheet.create({ username: state.login.user && state.login.user.username, usersTyping: state.room.usersTyping })) - export default class Typing extends React.Component { shouldComponentUpdate(nextProps) { return this.props.usersTyping.join() !== nextProps.usersTyping.join(); } + componentWillUpdate() { + LayoutAnimation.easeInEaseOut(); + } onPress = () => { Keyboard.dismiss(); } @@ -31,7 +34,13 @@ export default class Typing extends React.Component { return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : ''; } render() { - return ( this.onPress()}>{this.usersTyping}); + const { usersTyping } = this; + + if (!usersTyping) { + return ; + } + + return ( this.onPress()}>{usersTyping}); } } diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js index d9db162a2..c809dee80 100644 --- a/app/containers/message/Audio.js +++ b/app/containers/message/Audio.js @@ -4,9 +4,9 @@ import { View, StyleSheet, TouchableOpacity, Text, Easing } from 'react-native'; import Video from 'react-native-video'; import Icon from 'react-native-vector-icons/MaterialIcons'; import Slider from 'react-native-slider'; +import { connect } from 'react-redux'; import Markdown from './Markdown'; - const styles = StyleSheet.create({ audioContainer: { flex: 1, @@ -61,6 +61,9 @@ const formatTime = (t = 0, duration = 0) => { return `${ formattedMinutes }:${ formattedSeconds }`; }; +@connect(state => ({ + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' +})) export default class Audio extends React.PureComponent { static propTypes = { file: PropTypes.object.isRequired, @@ -115,8 +118,8 @@ export default class Audio extends React.PureComponent { const { uri, paused } = this.state; const { description } = this.props.file; return ( - - + [ + - - - + , + + ] ); } } diff --git a/app/containers/message/Emoji.js b/app/containers/message/Emoji.js index edbcd470c..5f87f64e0 100644 --- a/app/containers/message/Emoji.js +++ b/app/containers/message/Emoji.js @@ -2,8 +2,12 @@ import React from 'react'; import { Text, ViewPropTypes } from 'react-native'; import PropTypes from 'prop-types'; import { emojify } from 'react-emojione'; +import { connect } from 'react-redux'; import CustomEmoji from '../EmojiPicker/CustomEmoji'; +@connect(state => ({ + customEmojis: state.customEmojis +})) export default class Emoji extends React.PureComponent { static propTypes = { content: PropTypes.string, diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js index 942676dcc..2bf78435c 100644 --- a/app/containers/message/Image.js +++ b/app/containers/message/Image.js @@ -1,41 +1,30 @@ import PropTypes from 'prop-types'; import React from 'react'; import { CachedImage } from 'react-native-img-cache'; -import { Text, TouchableOpacity, View, StyleSheet } from 'react-native'; +import { TouchableOpacity, StyleSheet } from 'react-native'; +import { connect } from 'react-redux'; import PhotoModal from './PhotoModal'; +import Markdown from './Markdown'; const styles = StyleSheet.create({ button: { flex: 1, - flexDirection: 'column', - height: 320, - borderColor: '#ccc', - borderWidth: 1, - borderRadius: 6 + flexDirection: 'column' }, image: { - flex: 1, - height: undefined, - width: undefined, - resizeMode: 'contain' + width: 320, + height: 200, + resizeMode: 'cover' }, labelContainer: { - height: 62, - alignItems: 'center', - justifyContent: 'center' - }, - imageName: { - fontSize: 12, - alignSelf: 'center', - fontStyle: 'italic' - }, - message: { - alignSelf: 'center', - fontWeight: 'bold' + alignItems: 'flex-start' } }); -export default class Image extends React.PureComponent { +@connect(state => ({ + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' +})) +export default class extends React.PureComponent { static propTypes = { file: PropTypes.object.isRequired, baseUrl: PropTypes.string.isRequired, @@ -45,8 +34,9 @@ export default class Image extends React.PureComponent { state = { modalVisible: false }; getDescription() { - if (this.props.file.description) { - return {this.props.file.description}; + const { file, customEmojis } = this.props; + if (file.description) { + return ; } } @@ -60,8 +50,9 @@ export default class Image extends React.PureComponent { const { baseUrl, file, user } = this.props; const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; return ( - + [ this._onPressButton()} style={styles.button} > @@ -69,18 +60,16 @@ export default class Image extends React.PureComponent { style={styles.image} source={{ uri: encodeURI(img) }} /> - - {this.props.file.title} - {this.getDescription()} - - + {this.getDescription()} + , this.setState({ modalVisible: false })} /> - + ] ); } } diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js index 1b50f901d..3c88b09d5 100644 --- a/app/containers/message/Markdown.js +++ b/app/containers/message/Markdown.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line import SimpleMarkdown from 'simple-markdown'; import { emojify } from 'react-emojione'; +import { connect } from 'react-redux'; import styles from './styles'; import CustomEmoji from '../EmojiPicker/CustomEmoji'; @@ -17,126 +18,139 @@ const BlockCode = ({ node, state }) => ( ); const mentionStyle = { color: '#13679a' }; -const Markdown = ({ - msg, customEmojis, style, markdownStyle, customRules, renderInline -}) => { - if (!msg) { - return null; - } - msg = emojify(msg, { output: 'unicode' }); - - const defaultRules = { - username: { - order: -1, - match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/), - parse: capture => ({ content: capture[0] }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - alert('Username')} - > - {node.content} - - ) - } - }) - }, - heading: { - order: -2, - match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/), - parse: capture => ({ content: capture[0] }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - alert('Room')} - > - {node.content} - - ) - } - }) - }, - fence: { - order: -3, - match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/), - parse: capture => ({ - lang: capture[2] || undefined, - content: capture[3] - }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - - ) - } - }) - }, - blockCode: { - order: -4, - match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/), - parse: capture => ({ content: capture[2] }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - - ) - } - }) - }, - customEmoji: { - order: -5, - match: SimpleMarkdown.inlineRegex(/^:([0-9a-zA-Z-_.]+):/), - parse: capture => ({ content: capture }), - react: (node, output, state) => { - const element = { - type: 'custom', - key: state.key, - props: { - children: {node.content[0]} - } - }; - const content = node.content[1]; - const emojiExtension = customEmojis[content]; - if (emojiExtension) { - const emoji = { extension: emojiExtension, content }; - element.props.children = ( - - ); - } - return element; +const defaultRules = { + username: { + order: -1, + match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/), + parse: capture => ({ content: capture[0] }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + alert('Username')} + > + {node.content} + + ) } - } - }; - - const codeStyle = StyleSheet.flatten(styles.codeStyle); - style = StyleSheet.flatten(style); - return ( - {msg} - - ); + }) + }, + heading: { + order: -2, + match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/), + parse: capture => ({ content: capture[0] }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + alert('Room')} + > + {node.content} + + ) + } + }) + }, + fence: { + order: -3, + match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/), + parse: capture => ({ + lang: capture[2] || undefined, + content: capture[3] + }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + + ) + } + }) + }, + blockCode: { + order: -4, + match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/), + parse: capture => ({ content: capture[2] }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + + ) + } + }) + } }; +const codeStyle = StyleSheet.flatten(styles.codeStyle); + +@connect(state => ({ + customEmojis: state.customEmojis +})) +export default class Markdown extends React.Component { + shouldComponentUpdate(nextProps) { + return nextProps.msg !== this.props.msg; + } + render() { + const { + msg, customEmojis = {}, style, markdownStyle, customRules, renderInline + } = this.props; + if (!msg) { + return null; + } + const m = emojify(msg, { output: 'unicode' }); + + const s = StyleSheet.flatten(style); + return ( + ({ content: capture }), + react: (node, output, state) => { + const element = { + type: 'custom', + key: state.key, + props: { + children: {node.content[0]} + } + }; + const content = node.content[1]; + const emojiExtension = customEmojis[content]; + if (emojiExtension) { + const emoji = { extension: emojiExtension, content }; + element.props.children = ( + + ); + } + return element; + } + }, + ...defaultRules, + ...customRules + }} + renderInline={renderInline} + >{m} + + ); + } +} + Markdown.propTypes = { - msg: PropTypes.string.isRequired, + msg: PropTypes.string, customEmojis: PropTypes.object, // eslint-disable-next-line react/no-typos style: ViewPropTypes.style, @@ -149,5 +163,3 @@ BlockCode.propTypes = { node: PropTypes.object, state: PropTypes.object }; - -export default Markdown; diff --git a/app/containers/message/ReactionsModal.js b/app/containers/message/ReactionsModal.js index 08491518f..8f3f5620f 100644 --- a/app/containers/message/ReactionsModal.js +++ b/app/containers/message/ReactionsModal.js @@ -3,6 +3,7 @@ import { View, Text, TouchableWithoutFeedback, FlatList, StyleSheet } from 'reac import PropTypes from 'prop-types'; import Modal from 'react-native-modal'; import Icon from 'react-native-vector-icons/MaterialIcons'; +import { connect } from 'react-redux'; import Emoji from './Emoji'; const styles = StyleSheet.create({ @@ -52,6 +53,10 @@ const styles = StyleSheet.create({ }); const standardEmojiStyle = { fontSize: 20 }; const customEmojiStyle = { width: 20, height: 20 }; + +@connect(state => ({ + customEmojis: state.customEmojis +})) export default class ReactionsModal extends React.PureComponent { static propTypes = { isVisible: PropTypes.bool.isRequired, diff --git a/app/containers/message/User.js b/app/containers/message/User.js index 4921b48e8..8480f9b7d 100644 --- a/app/containers/message/User.js +++ b/app/containers/message/User.js @@ -7,7 +7,9 @@ import Avatar from '../Avatar'; const styles = StyleSheet.create({ username: { - fontWeight: 'bold' + color: '#000', + fontWeight: '400', + fontSize: 14 }, usernameView: { flexDirection: 'row', @@ -22,7 +24,8 @@ const styles = StyleSheet.create({ time: { fontSize: 10, color: '#888', - paddingLeft: 5 + paddingLeft: 5, + fontWeight: '400' }, edited: { marginLeft: 5, @@ -35,11 +38,10 @@ export default class User extends React.PureComponent { static propTypes = { item: PropTypes.object.isRequired, Message_TimeFormat: PropTypes.string.isRequired, - onPress: PropTypes.func, - baseUrl: PropTypes.string + onPress: PropTypes.func } - renderEdited(item) { + renderEdited = (item) => { if (!item.editedBy) { return null; } @@ -50,7 +52,6 @@ export default class User extends React.PureComponent { style={{ marginLeft: 5 }} text={item.editedBy.username} size={20} - baseUrl={this.props.baseUrl} avatar={item.avatar} /> diff --git a/app/containers/message/Video.js b/app/containers/message/Video.js index 0b8a019ab..237f5e38b 100644 --- a/app/containers/message/Video.js +++ b/app/containers/message/Video.js @@ -1,8 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, StyleSheet, TouchableOpacity, Image, Platform } from 'react-native'; +import { StyleSheet, TouchableOpacity, Image, Platform } from 'react-native'; import Modal from 'react-native-modal'; import VideoPlayer from 'react-native-video-controls'; +import { connect } from 'react-redux'; import Markdown from './Markdown'; import openLink from '../../utils/openLink'; @@ -27,6 +28,9 @@ const styles = StyleSheet.create({ } }); +@connect(state => ({ + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' +})) export default class Video extends React.PureComponent { static propTypes = { file: PropTypes.object.isRequired, @@ -55,18 +59,20 @@ export default class Video extends React.PureComponent { const { baseUrl, user } = this.props; const uri = `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; return ( - + [ this.open()} > - + , - + ] ); } } diff --git a/app/containers/message/index.js b/app/containers/message/index.js index babf15cec..399c67214 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,13 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, TouchableHighlight, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native'; +import { View, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; import equal from 'deep-equal'; import { KeyboardUtils } from 'react-native-keyboard-input'; -import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages'; import Image from './Image'; import User from './User'; import Avatar from '../Avatar'; @@ -18,13 +17,54 @@ import Url from './Url'; import Reply from './Reply'; import ReactionsModal from './ReactionsModal'; import Emoji from './Emoji'; -import messageStatus from '../../constants/messagesStatus'; import styles from './styles'; +import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages'; +import messagesStatus from '../../constants/messagesStatus'; +import Touch from '../../utils/touch'; + +const getInfoMessage = ({ + t, role, msg, u +}) => { + if (t === 'rm') { + return 'Message removed'; + } else if (t === 'uj') { + return 'Has joined the channel.'; + } else if (t === 'r') { + return `Room name changed to: ${ msg } by ${ u.username }`; + } else if (t === 'message_pinned') { + return 'Message pinned'; + } else if (t === 'ul') { + return 'Has left the channel.'; + } else if (t === 'ru') { + return `User ${ msg } removed by ${ u.username }`; + } else if (t === 'au') { + return `User ${ msg } added by ${ u.username }`; + } else if (t === 'user-muted') { + return `User ${ msg } muted by ${ u.username }`; + } else if (t === 'user-unmuted') { + return `User ${ msg } unmuted by ${ u.username }`; + } else if (t === 'subscription-role-added') { + return `${ msg } was set ${ role } by ${ u.username }`; + } else if (t === 'subscription-role-removed') { + return `${ msg } is no longer ${ role } by ${ u.username }`; + } else if (t === 'room_changed_description') { + return `Room description changed to: ${ msg } by ${ u.username }`; + } else if (t === 'room_changed_announcement') { + return `Room announcement changed to: ${ msg } by ${ u.username }`; + } else if (t === 'room_changed_topic') { + return `Room topic changed to: ${ msg } by ${ u.username }`; + } else if (t === 'room_changed_privacy') { + return `Room type changed to: ${ msg } by ${ u.username }`; + } + return ''; +}; @connect(state => ({ message: state.messages.message, editing: state.messages.editing, - customEmojis: state.customEmojis + customEmojis: state.customEmojis, + Message_TimeFormat: state.settings.Message_TimeFormat, + Message_GroupingPeriod: state.settings.Message_GroupingPeriod }), dispatch => ({ actionsShow: actionMessage => dispatch(actionsShow(actionMessage)), errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)), @@ -35,13 +75,13 @@ export default class Message extends React.Component { status: PropTypes.any, item: PropTypes.object.isRequired, reactions: PropTypes.any.isRequired, - baseUrl: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired, + Message_GroupingPeriod: PropTypes.number.isRequired, + customTimeFormat: PropTypes.string, message: PropTypes.object.isRequired, user: PropTypes.object.isRequired, editing: PropTypes.bool, errorActionsShow: PropTypes.func, - customEmojis: PropTypes.object, toggleReactionPicker: PropTypes.func, onReactionPress: PropTypes.func, style: ViewPropTypes.style, @@ -63,28 +103,35 @@ export default class Message extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - if (!equal(this.props.reactions, nextProps.reactions)) { - return true; - } if (this.state.reactionsModal !== nextState.reactionsModal) { return true; } - return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString() || this.props.status !== nextProps.status; + if (this.props.status !== nextProps.status) { + return true; + } + // eslint-disable-next-line + if (!!this.props._updatedAt ^ !!nextProps._updatedAt) { + return true; + } + if (!equal(this.props.reactions, nextProps.reactions)) { + return true; + } + return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString(); } onPress = () => { KeyboardUtils.dismiss(); } - onLongPress() { + onLongPress = () => { this.props.onLongPress(this.parseMessage()); } - onErrorPress() { + onErrorPress = () => { this.props.errorActionsShow(this.parseMessage()); } - onReactionPress(emoji) { + onReactionPress = (emoji) => { this.props.onReactionPress(emoji, this.props.item._id); } onClose() { @@ -95,45 +142,9 @@ export default class Message extends React.Component { Vibration.vibrate(50); } - getInfoMessage() { - let message = ''; - const { - t, role, msg, u - } = this.props.item; - - if (t === 'rm') { - message = 'Message removed'; - } else if (t === 'uj') { - message = 'Has joined the channel.'; - } else if (t === 'r') { - message = `Room name changed to: ${ msg } by ${ u.username }`; - } else if (t === 'message_pinned') { - message = 'Message pinned'; - } else if (t === 'ul') { - message = 'Has left the channel.'; - } else if (t === 'ru') { - message = `User ${ msg } removed by ${ u.username }`; - } else if (t === 'au') { - message = `User ${ msg } added by ${ u.username }`; - } else if (t === 'user-muted') { - message = `User ${ msg } muted by ${ u.username }`; - } else if (t === 'user-unmuted') { - message = `User ${ msg } unmuted by ${ u.username }`; - } else if (t === 'subscription-role-added') { - message = `${ msg } was set ${ role } by ${ u.username }`; - } else if (t === 'subscription-role-removed') { - message = `${ msg } is no longer ${ role } by ${ u.username }`; - } else if (t === 'room_changed_description') { - message = `Room description changed to: ${ msg } by ${ u.username }`; - } else if (t === 'room_changed_announcement') { - message = `Room announcement changed to: ${ msg } by ${ u.username }`; - } else if (t === 'room_changed_topic') { - message = `Room topic changed to: ${ msg } by ${ u.username }`; - } else if (t === 'room_changed_privacy') { - message = `Room type changed to: ${ msg } by ${ u.username }`; - } - - return message; + get timeFormat() { + const { customTimeFormat, Message_TimeFormat } = this.props; + return customTimeFormat || Message_TimeFormat; } parseMessage = () => JSON.parse(JSON.stringify(this.props.item)); @@ -163,64 +174,97 @@ export default class Message extends React.Component { } isTemp() { - return this.props.item.status === messageStatus.TEMP || this.props.item.status === messageStatus.ERROR; + return this.props.item.status === messagesStatus.TEMP || this.props.item.status === messagesStatus.ERROR; } hasError() { - return this.props.item.status === messageStatus.ERROR; + return this.props.item.status === messagesStatus.ERROR; } - attachments() { + renderHeader = (username) => { + const { item, previousItem } = this.props; + + if (previousItem && ( + (previousItem.ts.toDateString() === item.ts.toDateString()) && + (previousItem.u.username === item.u.username) && + !(previousItem.groupable === false || item.groupable === false) && + (previousItem.status === item.status) && + (item.ts - previousItem.ts < this.props.Message_GroupingPeriod * 1000) + )) { + return null; + } + + return ( + + + + + ); + } + + renderContent() { + if (this.isInfoMessage()) { + return {getInfoMessage(this.props.item)}; + } + const { item } = this.props; + return ; + } + + renderAttachment() { if (this.props.item.attachments.length === 0) { return null; } const file = this.props.item.attachments[0]; - const { baseUrl, user } = this.props; + const { user } = this.props; if (file.image_type) { - return ; - } else if (file.audio_type) { - return @@ -246,7 +289,7 @@ export default class Message extends React.Component { } return ( - {this.props.item.reactions.map(reaction => this.renderReaction(reaction))} + {this.props.item.reactions.map(this.renderReaction)} this.props.toggleReactionPicker(this.parseMessage())} key='add-reaction' @@ -260,57 +303,42 @@ export default class Message extends React.Component { render() { const { - item, message, editing, baseUrl, customEmojis, style, archived + item, message, editing, style, archived } = this.props; const username = item.alias || item.u.username; const isEditing = message._id === item._id && editing; - const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`; + const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.timeFormat) }, ${ this.props.item.msg }`; return ( - this.onPress()} - onLongPress={() => this.onLongPress()} - disabled={this.isDeleted() || this.hasError() || archived} + - - {this.renderError()} - - - - - {this.renderMessageContent()} - {this.attachments()} + + {this.renderHeader(username)} + + {this.renderError()} + + {this.renderContent()} + {this.renderAttachment()} {this.renderUrl()} {this.renderReactions()} - {this.state.reactionsModal ? + {this.state.reactionsModal && - : null } - + ); } } diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 23911cc48..45a7a8f45 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -1,20 +1,20 @@ import { StyleSheet, Platform } from 'react-native'; export default StyleSheet.create({ - content: { - flexGrow: 1, - flexShrink: 1 + messageContent: { + flex: 1, + marginLeft: 30 }, flex: { flexDirection: 'row', flex: 1 }, message: { - padding: 12, - paddingTop: 6, - paddingBottom: 6, - flexDirection: 'row', - transform: [{ scaleY: -1 }] + paddingHorizontal: 12, + paddingVertical: 3, + flexDirection: 'column', + transform: [{ scaleY: -1 }], + flex: 1 }, textInfo: { fontStyle: 'italic', @@ -27,6 +27,7 @@ export default StyleSheet.create({ width: 16, height: 16 }, + temp: { opacity: 0.3 }, codeStyle: { ...Platform.select({ ios: { fontFamily: 'Courier New' }, @@ -40,7 +41,8 @@ export default StyleSheet.create({ }, reactionsContainer: { flexDirection: 'row', - flexWrap: 'wrap' + flexWrap: 'wrap', + marginTop: 6 }, reactionContainer: { flexDirection: 'row', @@ -70,5 +72,17 @@ export default StyleSheet.create({ }, avatar: { marginRight: 10 + }, + reactedContainer: { + borderColor: '#bde1fe', + backgroundColor: '#f3f9ff' + }, + reactedCountText: { + color: '#4fb0fc' + }, + errorIcon: { + padding: 10, + paddingRight: 12, + paddingLeft: 0 } }); diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js index e0d8c0d2a..ee753d424 100644 --- a/app/containers/routes/AuthRoutes.js +++ b/app/containers/routes/AuthRoutes.js @@ -6,12 +6,13 @@ import RoomsListView from '../../views/RoomsListView'; import RoomView from '../../views/RoomView'; import RoomActionsView from '../../views/RoomActionsView'; import CreateChannelView from '../../views/CreateChannelView'; -import SelectUsersView from '../../views/SelectUsersView'; +import SelectedUsersView from '../../views/SelectedUsersView'; import NewServerView from '../../views/NewServerView'; import StarredMessagesView from '../../views/StarredMessagesView'; import PinnedMessagesView from '../../views/PinnedMessagesView'; import MentionedMessagesView from '../../views/MentionedMessagesView'; import SnippetedMessagesView from '../../views/SnippetedMessagesView'; +import SearchMessagesView from '../../views/SearchMessagesView'; import RoomFilesView from '../../views/RoomFilesView'; import RoomMembersView from '../../views/RoomMembersView'; import RoomInfoView from '../../views/RoomInfoView'; @@ -28,19 +29,22 @@ const AuthRoutes = StackNavigator( CreateChannel: { screen: CreateChannelView, navigationOptions: { - title: 'Create Channel' + title: 'Create Channel', + headerTintColor: '#292E35' } }, - SelectUsers: { - screen: SelectUsersView, + SelectedUsers: { + screen: SelectedUsersView, navigationOptions: { - title: 'Select Users' + title: 'Select Users', + headerTintColor: '#292E35' } }, AddServer: { screen: NewServerView, navigationOptions: { - title: 'New server' + title: 'New server', + headerTintColor: '#292E35' } }, RoomActions: { @@ -78,6 +82,13 @@ const AuthRoutes = StackNavigator( headerTintColor: '#292E35' } }, + SearchMessages: { + screen: SearchMessagesView, + navigationOptions: { + title: 'Search Messages', + headerTintColor: '#292E35' + } + }, RoomFiles: { screen: RoomFilesView, navigationOptions: { diff --git a/app/containers/routes/NavigationService.js b/app/containers/routes/NavigationService.js index 45de3a2c4..11de166e9 100644 --- a/app/containers/routes/NavigationService.js +++ b/app/containers/routes/NavigationService.js @@ -48,6 +48,11 @@ export function goRoom({ rid, name }, counter = 0) { NavigationActions.navigate({ key: `Room-${ rid }`, routeName: 'Room', params: { room: { rid, name }, rid, name } }) ] }); - config.navigator.dispatch(action); } + +export function dispatch(action) { + if (config.navigator) { + config.navigator.dispatch(action); + } +} diff --git a/app/containers/routes/PublicRoutes.js b/app/containers/routes/PublicRoutes.js index 8fec4d74e..2fdc690e4 100644 --- a/app/containers/routes/PublicRoutes.js +++ b/app/containers/routes/PublicRoutes.js @@ -5,74 +5,114 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import ListServerView from '../../views/ListServerView'; import NewServerView from '../../views/NewServerView'; +import LoginSignupView from '../../views/LoginSignupView'; import LoginView from '../../views/LoginView'; import RegisterView from '../../views/RegisterView'; import TermsServiceView from '../../views/TermsServiceView'; import PrivacyPolicyView from '../../views/PrivacyPolicyView'; import ForgotPasswordView from '../../views/ForgotPasswordView'; +import database from '../../lib/realm'; + +const hasServers = () => { + const db = database.databases.serversDB.objects('servers'); + return db.length > 0; +}; + +const ServerStack = StackNavigator({ + ListServer: { + screen: ListServerView, + navigationOptions({ navigation }) { + return { + title: 'Servers', + headerRight: ( + navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })} + style={{ width: 50, alignItems: 'center' }} + accessibilityLabel='Add server' + accessibilityTraits='button' + > + + + ) + }; + } + }, + AddServer: { + screen: NewServerView, + navigationOptions: { + header: null + } + }, + LoginSignup: { + screen: LoginSignupView, + navigationOptions: { + header: null + } + } +}, { + headerMode: 'screen', + initialRouteName: hasServers() ? 'ListServer' : 'AddServer' +}); + +const LoginStack = StackNavigator({ + Login: { + screen: LoginView, + navigationOptions: { + header: null + } + }, + ForgotPassword: { + screen: ForgotPasswordView, + navigationOptions: { + title: 'Forgot my password', + headerTintColor: '#292E35' + } + } +}, { + headerMode: 'screen' +}); + +const RegisterStack = StackNavigator({ + Register: { + screen: RegisterView, + navigationOptions: { + header: null + } + }, + TermsService: { + screen: TermsServiceView, + navigationOptions: { + title: 'Terms of service', + headerTintColor: '#292E35' + } + }, + PrivacyPolicy: { + screen: PrivacyPolicyView, + navigationOptions: { + title: 'Privacy policy', + headerTintColor: '#292E35' + } + } +}, { + headerMode: 'screen' +}); const PublicRoutes = StackNavigator( { - ListServer: { - screen: ListServerView, - navigationOptions({ navigation }) { - return { - title: 'Servers', - headerRight: ( - navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })} - style={{ width: 50, alignItems: 'center' }} - accessibilityLabel='Add server' - accessibilityTraits='button' - > - - - ) - }; - } - }, - AddServer: { - screen: NewServerView, - navigationOptions: { - title: 'New server' - } + Server: { + screen: ServerStack }, Login: { - screen: LoginView, - navigationOptions: { - title: 'Login' - } + screen: LoginStack }, Register: { - screen: RegisterView, - navigationOptions: { - title: 'Register' - } - }, - TermsService: { - screen: TermsServiceView, - navigationOptions: { - title: 'Terms of service' - } - }, - PrivacyPolicy: { - screen: PrivacyPolicyView, - navigationOptions: { - title: 'Privacy policy' - } - }, - ForgotPassword: { - screen: ForgotPasswordView, - navigationOptions: { - title: 'Forgot my password' - } + screen: RegisterStack } }, { - navigationOptions: { - headerTitleAllowFontScaling: false - } + mode: 'modal', + headerMode: 'none' } ); diff --git a/app/lib/createStore.js b/app/lib/createStore.js index 288b64a31..c9188f082 100644 --- a/app/lib/createStore.js +++ b/app/lib/createStore.js @@ -1,8 +1,8 @@ import { createStore as reduxCreateStore, applyMiddleware, compose } from 'redux'; +import Reactotron from 'reactotron-react-native' ; // eslint-disable-line import createSagaMiddleware from 'redux-saga'; -import logger from 'redux-logger'; import applyAppStateListener from 'redux-enhancer-react-native-appstate'; -import Reactotron from 'reactotron-react-native'; // eslint-disable-line + import reducers from '../reducers'; import sagas from '../sagas'; @@ -20,8 +20,7 @@ if (__DEV__) { enhancers = compose( applyAppStateListener(), applyMiddleware(reduxImmutableStateInvariant), - applyMiddleware(sagaMiddleware), - applyMiddleware(logger) + applyMiddleware(sagaMiddleware) ); } else { sagaMiddleware = createSagaMiddleware(); diff --git a/app/lib/ddp.js b/app/lib/ddp.js index 5d6ef612c..a6fd91c34 100644 --- a/app/lib/ddp.js +++ b/app/lib/ddp.js @@ -1,4 +1,23 @@ import EJSON from 'ejson'; +import { Answers } from 'react-native-fabric'; +import { AppState } from 'react-native'; +import debounce from '../utils/debounce'; +// import { AppState, NativeModules } from 'react-native'; +// const { WebSocketModule, BlobManager } = NativeModules; + +// class WS extends WebSocket { +// _close(code?: number, reason?: string): void { +// if (Platform.OS === 'android') { +// WebSocketModule.close(code, reason, this._socketId); +// } else { +// WebSocketModule.close(this._socketId); +// } +// +// if (BlobManager.isAvailable && this._binaryType === 'blob') { +// BlobManager.removeWebSocketHandler(this._socketId); +// } +// } +// } class EventEmitter { constructor() { @@ -9,6 +28,7 @@ class EventEmitter { this.events[event] = []; } this.events[event].push(listener); + return listener; } removeListener(event, listener) { if (typeof this.events[event] === 'object') { @@ -24,7 +44,8 @@ class EventEmitter { try { listener.apply(this, args); } catch (e) { - console.log(e); + Answers.logCustom(e); + console.warn(e); } }); } @@ -34,72 +55,195 @@ class EventEmitter { this.removeListener(event, g); listener.apply(this, args); }); + return listener; } } + export default class Socket extends EventEmitter { - constructor(url) { + constructor(url, login) { super(); - this.url = url.replace(/^http/, 'ws'); + this.state = 'active'; + this.lastping = new Date(); + this._login = login; + this.url = url;// .replace(/^http/, 'ws'); this.id = 0; this.subscriptions = {}; - this._connect(); this.ddp = new EventEmitter(); - this.on('ping', () => this.send({ msg: 'pong' })); + this._logged = false; + const waitTimeout = () => setTimeout(async() => { + // this.connection.ping(); + this.send({ msg: 'ping' }); + this.timeout = setTimeout(() => this.reconnect(), 1000); + }, 40000); + const handlePing = () => { + this.lastping = new Date(); + this.send({ msg: 'pong' }, true); + if (this.timeout) { + clearTimeout(this.timeout); + } + this.timeout = waitTimeout(); + }; + const handlePong = () => { + this.lastping = new Date(); + if (this.timeout) { + clearTimeout(this.timeout); + } + this.timeout = waitTimeout(); + }; + + + AppState.addEventListener('change', (nextAppState) => { + if (this.state && this.state.match(/inactive/) && nextAppState === 'active') { + try { + this.send({ msg: 'ping' }, true); + // this.connection.ping(); + } catch (e) { + this.reconnect(); + } + } + if (this.state && this.state.match(/background/) && nextAppState === 'active') { + this.emit('background'); + } + this.state = nextAppState; + }); + + this.on('pong', handlePong); + this.on('ping', handlePing); + this.on('result', data => this.ddp.emit(data.id, { id: data.id, result: data.result, error: data.error })); this.on('ready', data => this.ddp.emit(data.subs[0], data)); + // this.on('error', () => this.reconnect()); + this.on('disconnected', debounce(() => this.reconnect(), 300)); + this.on('logged', () => this._logged = true); + + this.on('logged', () => { + Object.keys(this.subscriptions || {}).forEach((key) => { + const { name, params } = this.subscriptions[key]; + this.subscriptions[key].unsubscribe(); + this.subscribe(name, ...params); + }); + }); + this.on('open', async() => { + this._logged = false; + this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] }); + }); + + this._connect(); } - send(obj) { + check() { + if (!this.lastping) { + return false; + } + if ((Math.abs(this.lastping.getTime() - new Date().getTime()) / 1000) > 50) { + return false; + } + return true; + } + async login(params) { + try { + this.emit('login', params); + const result = await this.call('login', params); + this._login = { resume: result.token, ...result }; + this._logged = true; + this.emit('logged', result); + return result; + } catch (err) { + const error = { ...err }; + if (/user not found/i.test(error.reason)) { + error.error = 1; + error.reason = 'User or Password incorrect'; + error.message = 'User or Password incorrect'; + } + this.emit('logginError', error); + return Promise.reject(error); + } + } + async send(obj, ignore) { + console.log('send'); return new Promise((resolve, reject) => { this.id += 1; - const id = obj.id || `${ this.id }`; + const id = obj.id || `ddp-react-native-${ this.id }`; + // console.log('send', { ...obj, id }); this.connection.send(EJSON.stringify({ ...obj, id })); - this.ddp.once(id, data => (data.error ? reject(data.error) : resolve({ id, ...data }))); + if (ignore) { + return; + } + const cancel = this.ddp.once('disconnected', reject); + this.ddp.once(id, (data) => { + // console.log(data); + this.ddp.removeListener(id, cancel); + return (data.error ? reject(data.error) : resolve({ id, ...data })); + }); }); } + get status() { + return this.connection && this.connection.readyState === 1 && this.check() && !!this._logged; + } + _close() { + try { + // this.connection && this.connection.readyState > 1 && this.connection.close && this.connection.close(300, 'disconnect'); + if (this.connection && this.connection.close) { + this.connection.close(300, 'disconnect'); + delete this.connection; + } + } catch (e) { + // console.log(e); + } + } _connect() { - const connection = new WebSocket(`${ this.url }/websocket`); - connection.onopen = () => { - this.emit('open'); - this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] }); - }; - connection.onclose = e => this.emit('disconnected', e); - // connection.onerror = () => { - // // alert(error.type); - // // console.log(error); - // // console.log(`WebSocket Error ${ JSON.stringify({...error}) }`); - // }; + return new Promise((resolve) => { + this.lastping = new Date(); + this._close(); + clearInterval(this.reconnect_timeout); + this.reconnect_timeout = setInterval(() => (!this.connection || this.connection.readyState > 1 || !this.check()) && this.reconnect(), 5000); + this.connection = new WebSocket(`${ this.url }/websocket`, null); - connection.onmessage = (e) => { - const data = EJSON.parse(e.data); - this.emit(data.msg, data); - return data.collection && this.emit(data.collection, data); - }; - // this.on('disconnected', e => alert(JSON.stringify(e))); - this.connection = connection; + this.connection.onopen = () => { + this.emit('open'); + resolve(); + this.ddp.emit('open'); + return this._login && this.login(this._login); + }; + this.connection.onclose = debounce((e) => { console.log('aer'); this.emit('disconnected', e); }, 300); + this.connection.onmessage = (e) => { + try { + // console.log('received', e.data, e.target.readyState); + const data = EJSON.parse(e.data); + this.emit(data.msg, data); + return data.collection && this.emit(data.collection, data); + } catch (err) { + Answers.logCustom('EJSON parse', err); + } + }; + }); } logout() { + this._login = null; return this.call('logout').then(() => this.subscriptions = {}); } disconnect() { - this.emit('disconnected_by_user'); - this.connection.close(); + this._close(); } - reconnect() { - this.disconnect(); - this.once('connected', () => { - Object.keys(this.subscriptions).forEach((key) => { - const { name, params } = this.subscriptions[key]; - this.subscriptions[key].unsubscribe(); - this.subscribe(name, params); - }); - }); - this._connect(); + async reconnect() { + if (this._timer) { + return; + } + delete this.connection; + this._logged = false; + + this._timer = setTimeout(() => { + delete this._timer; + this._connect(); + }, 1000); } call(method, ...params) { return this.send({ msg: 'method', method, params - }).then(data => data.result || data.subs); + }).then(data => data.result || data.subs).catch((err) => { + Answers.logCustom('DDP call Error', err); + return Promise.reject(err); + }); } unsubscribe(id) { if (!this.subscriptions[id]) { @@ -109,19 +253,31 @@ export default class Socket extends EventEmitter { return this.send({ msg: 'unsub', id - }).then(data => data.result || data.subs); + }).then(data => data.result || data.subs).catch((err) => { + console.warn('unsubscribe', err); + Answers.logCustom('DDP unsubscribe Error', err); + return Promise.reject(err); + }); } subscribe(name, ...params) { + console.log(name, params); return this.send({ msg: 'sub', name, params }).then(({ id }) => { const args = { + id, name, params, unsubscribe: () => this.unsubscribe(id) }; + this.subscriptions[id] = args; + // console.log(args); return args; + }).catch((err) => { + console.warn('subscribe', err); + Answers.logCustom('DDP subscribe Error', err); + return Promise.reject(err); }); } } diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js new file mode 100644 index 000000000..391b2ed4f --- /dev/null +++ b/app/lib/methods/getCustomEmojis.js @@ -0,0 +1,23 @@ +import { InteractionManager } from 'react-native'; +import reduxStore from '../createStore'; +// import { get } from './helpers/rest'; + +import database from '../realm'; +import * as actions from '../../actions'; + +const getLastMessage = () => { + const setting = database.objects('customEmojis').sorted('_updatedAt', true)[0]; + return setting && setting._updatedAt; +}; + + +export default async function() { + const lastMessage = getLastMessage(); + let emojis = await this.ddp.call('listEmojiCustom'); + emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage); + emojis = this._prepareEmojis(emojis); + InteractionManager.runAfterInteractions(() => database.write(() => { + emojis.forEach(emoji => database.create('customEmojis', emoji, true)); + })); + reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(emojis))); +} diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js new file mode 100644 index 000000000..542276a27 --- /dev/null +++ b/app/lib/methods/getPermissions.js @@ -0,0 +1,22 @@ +import { InteractionManager } from 'react-native'; +import reduxStore from '../createStore'; +// import { get } from './helpers/rest'; + +import database from '../realm'; +import * as actions from '../../actions'; + +const getLastMessage = () => { + const setting = database.objects('permissions').sorted('_updatedAt', true)[0]; + return setting && setting._updatedAt; +}; + + +export default async function() { + const lastMessage = getLastMessage(); + const result = await (!lastMessage ? this.ddp.call('permissions/get') : this.ddp.call('permissions/get', new Date(lastMessage))); + const permissions = this._preparePermissions(result.update || result); + console.log('getPermissions', permissions); + InteractionManager.runAfterInteractions(() => database.write(() => + permissions.forEach(permission => database.create('permissions', permission, true)))); + reduxStore.dispatch(actions.setAllPermissions(this.parsePermissions(permissions))); +} diff --git a/app/lib/methods/getRooms.js b/app/lib/methods/getRooms.js new file mode 100644 index 000000000..ef2732e00 --- /dev/null +++ b/app/lib/methods/getRooms.js @@ -0,0 +1,51 @@ +import { InteractionManager } from 'react-native'; +// import { showToast } from '../../utils/info'; +import { get } from './helpers/rest'; +import mergeSubscriptionsRooms, { merge } from './helpers/mergeSubscriptionsRooms'; +import database from '../realm'; + +const lastMessage = () => { + const message = database + .objects('subscriptions') + .sorted('roomUpdatedAt', true)[0]; + return message && new Date(message.roomUpdatedAt); +}; + +const getRoomRest = async function() { + const { ddp } = this; + const updatedSince = lastMessage(); + const { token, id } = ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const [subscriptions, rooms] = await Promise.all([get({ token, id, server }, 'subscriptions.get', { updatedSince }), get({ token, id, server }, 'rooms.get', { updatedSince })]); + return mergeSubscriptionsRooms(subscriptions, rooms); +}; + +const getRoomDpp = async function() { + try { + const { ddp } = this; + const updatedSince = lastMessage(); + const [subscriptions, rooms] = await Promise.all([ddp.call('subscriptions/get', updatedSince), ddp.call('rooms/get', updatedSince)]); + return mergeSubscriptionsRooms(subscriptions, rooms); + } catch (e) { + return getRoomRest.apply(this); + } +}; + +export default async function() { + const { database: db } = database; + + return new Promise(async(resolve) => { + // eslint-disable-next-line + const { subscriptions, rooms } = await (false && this.ddp.status ? getRoomDpp.apply(this) : getRoomRest.apply(this)); + + const data = rooms.map(room => ({ room, sub: database.objects('subscriptions').filtered('rid == $0', room._id) })); + + InteractionManager.runAfterInteractions(() => { + db.write(() => { + subscriptions.forEach(subscription => db.create('subscriptions', subscription, true)); + data.forEach(({ sub, room }) => sub[0] && merge(sub[0], room)); + }); + resolve(data); + }); + }); +} diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js new file mode 100644 index 000000000..d2c94acb9 --- /dev/null +++ b/app/lib/methods/getSettings.js @@ -0,0 +1,24 @@ +import { InteractionManager } from 'react-native'; +import reduxStore from '../createStore'; +// import { get } from './helpers/rest'; + +import database from '../realm'; +import * as actions from '../../actions'; + +const getLastMessage = () => { + const [setting] = database.objects('settings').sorted('_updatedAt', true); + return setting && setting._updatedAt; +}; + +export default async function() { + const lastMessage = getLastMessage(); + const result = await (!lastMessage ? this.ddp.call('public-settings/get') : this.ddp.call('public-settings/get', new Date(lastMessage))); + console.log('getSettings', lastMessage, result); + + const filteredSettings = this._prepareSettings(this._filterSettings(result.update || result)); + + InteractionManager.runAfterInteractions(() => + database.write(() => + filteredSettings.forEach(setting => database.create('settings', setting, true)))); + reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings))); +} diff --git a/app/lib/methods/helpers/buildMessage.js b/app/lib/methods/helpers/buildMessage.js new file mode 100644 index 000000000..a565046fa --- /dev/null +++ b/app/lib/methods/helpers/buildMessage.js @@ -0,0 +1,7 @@ +import normalizeMessage from './normalizeMessage'; +import messagesStatus from '../../../constants/messagesStatus'; + +export default (message) => { + message.status = messagesStatus.SENT; + return normalizeMessage(message); +}; diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js new file mode 100644 index 000000000..15cc768f1 --- /dev/null +++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js @@ -0,0 +1,51 @@ +import normalizeMessage from './normalizeMessage'; +// TODO: delete and update + +export const merge = (subscription, room) => { + subscription.muted = []; + if (room) { + subscription.roomUpdatedAt = room._updatedAt; + subscription.lastMessage = normalizeMessage(room.lastMessage); + subscription.ro = room.ro; + subscription.description = room.description; + subscription.topic = room.topic; + subscription.announcement = room.announcement; + subscription.reactWhenReadOnly = room.reactWhenReadOnly; + subscription.archived = room.archived; + subscription.joinCodeRequired = room.joinCodeRequired; + + if (room.muted && room.muted.length) { + subscription.muted = room.muted.filter(role => role).map(role => ({ value: role })); + } + } + if (subscription.roles && subscription.roles.length) { + subscription.roles = subscription.roles.map(role => (role.value ? role : { value: role })); + } + + if (subscription.mobilePushNotifications === 'nothing') { + subscription.notifications = true; + } else { + subscription.notifications = false; + } + + subscription.blocked = !!subscription.blocker; + return subscription; +}; + +export default (subscriptions = [], rooms = []) => { + if (subscriptions.update) { + subscriptions = subscriptions.update; + rooms = rooms.update; + } + return { + subscriptions: subscriptions.map((s) => { + const index = rooms.findIndex(({ _id }) => _id === s.rid); + if (index < 0) { + return merge(s); + } + const [room] = rooms.splice(index, 1); + return merge(s, room); + }), + rooms + }; +}; diff --git a/app/lib/methods/helpers/normalizeMessage.js b/app/lib/methods/helpers/normalizeMessage.js new file mode 100644 index 000000000..e391a7ce2 --- /dev/null +++ b/app/lib/methods/helpers/normalizeMessage.js @@ -0,0 +1,31 @@ +import parseUrls from './parseUrls'; + +function normalizeAttachments(msg) { + if (typeof msg.attachments !== typeof [] || !msg.attachments || !msg.attachments.length) { + msg.attachments = []; + } + msg.attachments = msg.attachments.map((att) => { + att.fields = att.fields || []; + att = normalizeAttachments(att); + return att; + }); + return msg; +} + +export default (msg) => { + if (!msg) { return; } + msg = normalizeAttachments(msg); + msg.reactions = msg.reactions || []; + // TODO: api problems + if (Array.isArray(msg.reactions)) { + msg.reactions = msg.reactions.map((value, key) => ({ teste: 1, emoji: key, usernames: value.usernames.map(username => ({ value: username })) })); + } else { + msg.reactions = Object.keys(msg.reactions).map(key => ({ teste: 1, emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) })); + } + msg.urls = msg.urls ? parseUrls(msg.urls) : []; + msg._updatedAt = new Date(); + // loadHistory returns msg.starred as object + // stream-room-msgs returns msg.starred as an array + msg.starred = msg.starred && (Array.isArray(msg.starred) ? msg.starred.length > 0 : !!msg.starred); + return msg; +}; diff --git a/app/lib/methods/helpers/parseUrls.js b/app/lib/methods/helpers/parseUrls.js new file mode 100644 index 000000000..e0896e3a7 --- /dev/null +++ b/app/lib/methods/helpers/parseUrls.js @@ -0,0 +1,14 @@ +export default urls => urls.filter(url => url.meta && !url.ignoreParse).map((url, index) => { + const tmp = {}; + const { meta } = url; + tmp._id = index; + tmp.title = meta.ogTitle || meta.twitterTitle || meta.title || meta.pageTitle || meta.oembedTitle; + tmp.description = meta.ogDescription || meta.twitterDescription || meta.description || meta.oembedAuthorName; + let decodedOgImage; + if (meta.ogImage) { + decodedOgImage = meta.ogImage.replace(/&/g, '&'); + } + tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl; + tmp.url = url.url; + return tmp; +}); diff --git a/app/lib/methods/helpers/protectedFunction.js b/app/lib/methods/helpers/protectedFunction.js new file mode 100644 index 000000000..f44fe6f51 --- /dev/null +++ b/app/lib/methods/helpers/protectedFunction.js @@ -0,0 +1,12 @@ +import { Answers } from 'react-native-fabric'; + +export default fn => (params) => { + try { + fn(params); + } catch (e) { + Answers.logCustom('erro', e); + if (__DEV__) { + alert(e); + } + } +}; diff --git a/app/lib/methods/helpers/rest.js b/app/lib/methods/helpers/rest.js new file mode 100644 index 000000000..1efe08f54 --- /dev/null +++ b/app/lib/methods/helpers/rest.js @@ -0,0 +1,40 @@ +import toQuery from './toQuery'; + + +const handleSuccess = (msg) => { + if (msg.success !== undefined && !msg.success) { + return Promise.reject(msg); + } + return msg; +}; + +export const get = function({ + token, id, server +}, method, params = {}) { + return fetch(`${ server }/api/v1/${ method }/?${ toQuery(params) }`, { + method: 'get', + headers: { + // 'Accept-Encoding': 'gzip', + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': id + } + }).then(response => response.json()).then(handleSuccess); +}; + + +export const post = function({ + token, id, server +}, method, params = {}) { + return fetch(`${ server }/api/v1/${ method }`, { + method: 'post', + body: JSON.stringify(params), + headers: { + // 'Accept-Encoding': 'gzip', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': id + } + }).then(response => response.json()).then(handleSuccess); +}; diff --git a/app/lib/methods/helpers/toQuery.js b/app/lib/methods/helpers/toQuery.js new file mode 100644 index 000000000..4fb9b7776 --- /dev/null +++ b/app/lib/methods/helpers/toQuery.js @@ -0,0 +1,3 @@ +export default function(obj) { + return Object.keys(obj).filter(p => obj[p] !== undefined && obj[p] !== null).map(p => `${ encodeURIComponent(p) }=${ encodeURIComponent(obj[p]) }`).join('&'); +} diff --git a/app/lib/methods/loadMessagesForRoom.js b/app/lib/methods/loadMessagesForRoom.js new file mode 100644 index 000000000..9f12a93ea --- /dev/null +++ b/app/lib/methods/loadMessagesForRoom.js @@ -0,0 +1,68 @@ +import { InteractionManager } from 'react-native'; + +import { get } from './helpers/rest'; +import buildMessage from './helpers/buildMessage'; +import database from '../realm'; + + +// TODO: api fix +const types = { + c: 'channels', d: 'im', p: 'groups' +}; + +async function loadMessagesForRoomRest({ rid: roomId, latest, t }) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const data = await get({ token, id, server }, `${ types[t] }.history`, { roomId, latest }); + return data.messages; +} + +async function loadMessagesForRoomDDP(...args) { + const [{ rid: roomId, latest }] = args; + try { + const data = await this.ddp.call('loadHistory', roomId, latest, 50); + if (!data || !data.messages.length) { + return []; + } + return data.messages; + } catch (e) { + console.warn('loadMessagesForRoomDDP', e); + return loadMessagesForRoomRest.call(this, ...args); + } + + // } + // if (cb) { + // cb({ end: data && data.messages.length < 20 }); + // } + // return data.message; + // }, (err) => { + // if (err) { + // if (cb) { + // cb({ end: true }); + // } + // return Promise.reject(err); + // } + // }); +} + +export default async function loadMessagesForRoom(...args) { + console.log('aqui'); + const { database: db } = database; + console.log('database', db); + + return new Promise(async(resolve) => { + // eslint-disable-next-line + const data = (await (false && this.ddp.status ? loadMessagesForRoomDDP.call(this, ...args) : loadMessagesForRoomRest.call(this, ...args))).map(buildMessage); + if (data) { + InteractionManager.runAfterInteractions(() => { + try { + db.write(() => data.forEach(message => db.create('messages', message, true))); + resolve(data); + } catch (e) { + console.warn('loadMessagesForRoom', e); + } + }); + } + return resolve([]); + }); +} diff --git a/app/lib/methods/loadMissedMessages.js b/app/lib/methods/loadMissedMessages.js new file mode 100644 index 000000000..5bd0074c1 --- /dev/null +++ b/app/lib/methods/loadMissedMessages.js @@ -0,0 +1,60 @@ +import { InteractionManager } from 'react-native'; + +import { get } from './helpers/rest'; +import buildMessage from './helpers/buildMessage'; +import database from '../realm'; + + +async function loadMissedMessagesRest({ rid: roomId, lastOpen: lastUpdate }) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const { result } = await get({ token, id, server }, 'chat.syncMessages', { roomId, lastUpdate }); + // TODO: api fix + return result.updated || result.messages; +} + +async function loadMissedMessagesDDP(...args) { + const [{ rid, lastOpen: lastUpdate }] = args; + + try { + const data = await this.ddp.call('messages/get', rid, { lastUpdate: new Date(lastUpdate) }); + return data.updated || data.messages; + } catch (e) { + return loadMissedMessagesRest.call(this, ...args); + } + + // } + // if (cb) { + // cb({ end: data && data.messages.length < 20 }); + // } + // return data.message; + // }, (err) => { + // if (err) { + // if (cb) { + // cb({ end: true }); + // } + // return Promise.reject(err); + // } + // }); +} + +export default async function(...args) { + const { database: db } = database; + return new Promise(async(resolve) => { + // eslint-disable-next-line + const data = (await (false && this.ddp.status ? loadMissedMessagesDDP.call(this, ...args) : loadMissedMessagesRest.call(this, ...args))); + + if (data) { + data.forEach(buildMessage); + return InteractionManager.runAfterInteractions(() => { + try { + db.write(() => data.forEach(message => db.create('messages', message, true))); + resolve(data); + } catch (e) { + console.warn('loadMessagesForRoom', e); + } + }); + } + resolve([]); + }); +} diff --git a/app/lib/methods/readMessages.js b/app/lib/methods/readMessages.js new file mode 100644 index 000000000..10ac99410 --- /dev/null +++ b/app/lib/methods/readMessages.js @@ -0,0 +1,33 @@ +import { post } from './helpers/rest'; +import database from '../realm'; + +const readMessagesREST = function readMessagesREST(rid) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + return post({ token, id, server }, 'subscriptions.read', { rid }); +}; + +const readMessagesDDP = function readMessagesDDP(rid) { + try { + return this.ddp.call('readMessages', rid); + } catch (e) { + return readMessagesREST.call(this, rid); + } +}; + +export default async function readMessages(rid) { + const { database: db } = database; + // eslint-disable-next-line + const data = await (false && this.ddp.status ? readMessagesDDP.call(this, rid) : readMessagesREST.call(this, rid)); + const [subscription] = db.objects('subscriptions').filtered('rid = $0', rid); + db.write(() => { + subscription.open = true; + subscription.alert = false; + subscription.unread = 0; + subscription.userMentions = 0; + subscription.groupMentions = 0; + subscription.ls = new Date(); + subscription.lastOpen = new Date(); + }); + return data; +} diff --git a/app/lib/methods/sendMessage.js b/app/lib/methods/sendMessage.js new file mode 100644 index 000000000..eba1f5eac --- /dev/null +++ b/app/lib/methods/sendMessage.js @@ -0,0 +1,72 @@ +import Random from 'react-native-meteor/lib/Random'; +import messagesStatus from '../../constants/messagesStatus'; + +import buildMessage from '../methods/helpers/buildMessage'; +import { post } from './helpers/rest'; +import database from '../realm'; +import reduxStore from '../createStore'; + +export const getMessage = (rid, msg = {}) => { + const _id = Random.id(); + const message = { + _id, + rid, + msg, + ts: new Date(), + _updatedAt: new Date(), + status: messagesStatus.TEMP, + u: { + _id: reduxStore.getState().login.user.id || '1', + username: reduxStore.getState().login.user.username + } + }; + database.write(() => { + database.create('messages', message, true); + }); + return message; +}; + +function sendMessageByRest(message) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const { _id, rid, msg } = message; + return post({ token, id, server }, 'chat.sendMessage', { message: { _id, rid, msg } }); +} + +function sendMessageByDDP(message) { + const { _id, rid, msg } = message; + return this.ddp.call('sendMessage', { _id, rid, msg }); +} + +export async function _sendMessageCall(message) { + try { + // eslint-disable-next-line + const data = await (false && this.ddp.status ? sendMessageByDDP.call(this, message) : sendMessageByRest.call(this, message)); + return data; + } catch (e) { + database.write(() => { + message.status = messagesStatus.ERROR; + database.create('messages', message, true); + }); + } +} + +export default async function(rid, msg) { + const { database: db } = database; + try { + const message = getMessage(rid, msg); + const room = db.objects('subscriptions').filtered('rid == $0', rid); + + db.write(() => { + room.lastMessage = message; + }); + + const ret = await _sendMessageCall.call(this, message); + // TODO: maybe I have created a bug in the future here <3 + db.write(() => { + db.create('messages', buildMessage({ ...message, ...ret }), true); + }); + } catch (e) { + console.warn('sendMessage', e); + } +} diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js new file mode 100644 index 000000000..6c045e6b8 --- /dev/null +++ b/app/lib/methods/subscriptions/room.js @@ -0,0 +1,71 @@ +// import database from '../../realm'; +// import reduxStore from '../../createStore'; +// import normalizeMessage from '../helpers/normalizeMessage'; +// import _buildMessage from '../helpers/buildMessage'; +// import protectedFunction from '../helpers/protectedFunction'; + +const subscribe = (ddp, rid) => Promise.all([ + ddp.subscribe('stream-room-messages', rid, false), + ddp.subscribe('stream-notify-room', `${ rid }/typing`, false) +]); +const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(e => console.warn(e))); + +let timer = null; +let promises; +let logged; +let disconnected; + +const stop = (ddp) => { + if (promises) { + promises.then(unsubscribe); + promises = false; + } + + ddp.removeListener('logged', logged); + ddp.removeListener('disconnected', disconnected); + + logged = false; + disconnected = false; + + clearTimeout(timer); +}; + +export default async function subscribeRoom({ rid, t }) { + if (promises) { + promises.then(unsubscribe); + promises = false; + } + const loop = (time = new Date()) => { + if (timer) { + return; + } + timer = setTimeout(async() => { + try { + await this.loadMissedMessages({ rid, t, lastOpen: timer }); + timer = false; + loop(); + } catch (e) { + loop(time); + } + }, 5000); + }; + + + logged = this.ddp.on('logged', () => { + clearTimeout(timer); + timer = false; + promises = subscribe(this.ddp, rid); + }); + + disconnected = this.ddp.on('disconnected', () => { loop(); }); + + if (!this.ddp.status) { + loop(); + } else { + promises = subscribe(this.ddp, rid); + } + + return { + stop: () => stop(this.ddp) + }; +} diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js new file mode 100644 index 000000000..4302568b2 --- /dev/null +++ b/app/lib/methods/subscriptions/rooms.js @@ -0,0 +1,58 @@ +import database from '../../realm'; +import { merge } from '../helpers/mergeSubscriptionsRooms'; + +export default async function subscribeRooms(id) { + const subscriptions = Promise.all([ + this.ddp.subscribe('stream-notify-user', `${ id }/subscriptions-changed`, false), + this.ddp.subscribe('stream-notify-user', `${ id }/rooms-changed`, false), + this.ddp.subscribe('stream-notify-user', `${ id }/message`, false) + ]); + + let timer = null; + const loop = (time = new Date()) => { + if (timer) { + return; + } + timer = setTimeout(async() => { + try { + await this.getRooms(time); + timer = false; + loop(); + } catch (e) { + loop(time); + } + }, 5000); + }; + + this.ddp.on('logged', () => { + clearTimeout(timer); + timer = false; + }); + + this.ddp.on('logout', () => { + clearTimeout(timer); + timer = true; + }); + + this.ddp.on('disconnected', () => { loop(); }); + + this.ddp.on('stream-notify-user', (ddpMessage) => { + const [type, data] = ddpMessage.fields.args; + const [, ev] = ddpMessage.fields.eventName.split('/'); + if (/subscriptions/.test(ev)) { + const tpm = merge(data); + return database.write(() => { + database.create('subscriptions', tpm, true); + }); + } + if (/rooms/.test(ev) && type === 'updated') { + const [sub] = database.objects('subscriptions').filtered('rid == $0', data._id); + database.write(() => { + merge(sub, data); + }); + } + }); + + await subscriptions; + console.log(this.ddp.subscriptions); +} diff --git a/app/lib/realm.js b/app/lib/realm.js index 8ad8d5db7..bfe84ed59 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -51,6 +51,7 @@ const roomsSchema = { _id: 'string', t: 'string', lastMessage: 'messages', + description: { type: 'string', optional: true }, _updatedAt: { type: 'date', optional: true } } }; @@ -63,6 +64,14 @@ const subscriptionRolesSchema = { } }; +const userMutedInRoomSchema = { + name: 'usersMuted', + primaryKey: 'value', + properties: { + value: 'string' + } +}; + const subscriptionSchema = { name: 'subscriptions', primaryKey: '_id', @@ -90,7 +99,9 @@ const subscriptionSchema = { blocked: { type: 'bool', optional: true }, reactWhenReadOnly: { type: 'bool', optional: true }, archived: { type: 'bool', optional: true }, - joinCodeRequired: { type: 'bool', optional: true } + joinCodeRequired: { type: 'bool', optional: true }, + notifications: { type: 'bool', optional: true }, + muted: { type: 'list', objectType: 'usersMuted' } } }; @@ -137,7 +148,9 @@ const attachment = { color: { type: 'string', optional: true }, ts: { type: 'date', optional: true }, attachments: { type: 'list', objectType: 'attachment' }, - fields: { type: 'list', objectType: 'attachmentFields' } + fields: { + type: 'list', objectType: 'attachmentFields', default: [] + } } }; @@ -265,8 +278,48 @@ const schema = [ customEmojisSchema, messagesReactionsSchema, messagesReactionsUsernamesSchema, - rolesSchema + rolesSchema, + userMutedInRoomSchema ]; + +// class DebouncedDb { +// constructor(db) { +// this.database = db; +// } +// deleteAll(...args) { +// return this.database.write(() => this.database.deleteAll(...args)); +// } +// delete(...args) { +// return this.database.delete(...args); +// } +// write(fn) { +// return fn(); +// } +// create(...args) { +// this.queue = this.queue || []; +// if (this.timer) { +// clearTimeout(this.timer); +// this.timer = null; +// } +// this.timer = setTimeout(() => { +// alert(this.queue.length); +// this.database.write(() => { +// this.queue.forEach(({ db, args }) => this.database.create(...args)); +// }); +// +// this.timer = null; +// return this.roles = []; +// }, 1000); +// +// this.queue.push({ +// db: this.database, +// args +// }); +// } +// objects(...args) { +// return this.database.objects(...args); +// } +// } class DB { databases = { serversDB: new Realm({ @@ -296,7 +349,7 @@ class DB { return this.databases.activeDB; } - setActiveDB(database) { + setActiveDB(database = '') { const path = database.replace(/(^\w+:|^)\/\//, ''); return this.databases.activeDB = new Realm({ path: `${ path }.realm`, diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 97b00d3b9..834c9dd98 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,47 +1,56 @@ -import Random from 'react-native-meteor/lib/Random'; import { AsyncStorage, Platform } from 'react-native'; import { hashPassword } from 'react-native-meteor/lib/utils'; -import _ from 'lodash'; +import foreach from 'lodash/forEach'; +import Random from 'react-native-meteor/lib/Random'; +import { Answers } from 'react-native-fabric'; import RNFetchBlob from 'react-native-fetch-blob'; import reduxStore from './createStore'; import settingsType from '../constants/settings'; import messagesStatus from '../constants/messagesStatus'; import database from './realm'; -import * as actions from '../actions'; -import { someoneTyping, roomMessageReceived } from '../actions/room'; -import { setUser, setLoginServices, removeLoginServices } from '../actions/login'; -import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect'; +// import * as actions from '../actions'; + +import { setUser, setLoginServices, removeLoginServices, loginRequest, loginSuccess, loginFailure } from '../actions/login'; +import { disconnect, connectSuccess, connectFailure } from '../actions/connect'; import { setActiveUser } from '../actions/activeUsers'; import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages'; import { pinnedMessagesReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages'; import { mentionedMessagesReceived } from '../actions/mentionedMessages'; import { snippetedMessagesReceived } from '../actions/snippetedMessages'; import { roomFilesReceived } from '../actions/roomFiles'; +import { someoneTyping, roomMessageReceived } from '../actions/room'; import { setRoles } from '../actions/roles'; import Ddp from './ddp'; -export { Accounts } from 'react-native-meteor'; +import normalizeMessage from './methods/helpers/normalizeMessage'; + +import subscribeRooms from './methods/subscriptions/rooms'; +import subscribeRoom from './methods/subscriptions/room'; + +import protectedFunction from './methods/helpers/protectedFunction'; +import readMessages from './methods/readMessages'; +import getSettings from './methods/getSettings'; + +import getRooms from './methods/getRooms'; +import getPermissions from './methods/getPermissions'; +import getCustomEmoji from './methods/getCustomEmojis'; + + +import _buildMessage from './methods/helpers/buildMessage'; +import loadMessagesForRoom from './methods/loadMessagesForRoom'; +import loadMissedMessages from './methods/loadMissedMessages'; + +import sendMessage, { getMessage, _sendMessageCall } from './methods/sendMessage'; -const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line const TOKEN_KEY = 'reactnativemeteor_usertoken'; -const SERVER_TIMEOUT = 30000; - +const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line const returnAnArray = obj => obj || []; -const normalizeMessage = (lastMessage) => { - if (lastMessage) { - lastMessage.attachments = lastMessage.attachments || []; - lastMessage.reactions = _.map(lastMessage.reactions, (value, key) => - ({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) })); - } - return lastMessage; -}; - - const RocketChat = { TOKEN_KEY, - + subscribeRooms, + subscribeRoom, createChannel({ name, users, type }) { return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type); }, @@ -97,59 +106,78 @@ const RocketChat = { reduxStore.dispatch(setActiveUser(this.activeUsers)); this._setUserTimer = null; return this.activeUsers = {}; - }, 3000); + }, 1000); + this.activeUsers[ddpMessage.id] = ddpMessage.fields; }, - reconnect() { - if (this.ddp) { - this.ddp.reconnect(); + async loginSuccess(user) { + if (!user) { + const { user: u } = reduxStore.getState().login; + user = Object.assign({}, u); } + + // TODO: one api call + // call /me only one time + if (!user.username) { + const me = await this.me({ token: user.token, userId: user.id }); + // eslint-disable-next-line + user.username = me.username; + } + if (user.username) { + const userInfo = await this.userInfo({ token: user.token, userId: user.id }); + user.username = userInfo.user.username; + if (userInfo.user.roles) { + user.roles = userInfo.user.roles; + } + } + return reduxStore.dispatch(loginSuccess(user)); }, - connect(url) { - if (this.ddp) { - this.ddp.disconnect(); - } - this.ddp = new Ddp(url); + connect(url, login) { return new Promise((resolve) => { - this.ddp.on('disconnected_by_user', () => { - reduxStore.dispatch(disconnect_by_user()); - }); - this.ddp.on('disconnected', () => { + if (this.ddp) { + this.ddp.disconnect(); + delete this.ddp; + } + + this.ddp = new Ddp(url, login); + if (login) { + protectedFunction(() => RocketChat.getRooms()); + } + + this.ddp.on('login', protectedFunction(() => reduxStore.dispatch(loginRequest()))); + + this.ddp.on('logginError', protectedFunction(err => reduxStore.dispatch(loginFailure(err)))); + + this.ddp.on('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage))); + + this.ddp.on('background', () => this.getRooms().catch(e => console.warn('background getRooms', e))); + + this.ddp.on('disconnected', () => console.log('disconnected')); + + this.ddp.on('logged', protectedFunction((user) => { + this.getRooms().catch(e => console.warn('logged getRooms', e)); + this.loginSuccess(user); + })); + this.ddp.once('logged', protectedFunction(({ id }) => { this.subscribeRooms(id); })); + + this.ddp.on('disconnected', protectedFunction(() => { reduxStore.dispatch(disconnect()); - }); - // this.ddp.on('open', async() => { - // resolve(reduxStore.dispatch(connectSuccess())); - // }); - this.ddp.on('connected', () => { - resolve(reduxStore.dispatch(connectSuccess())); - RocketChat.getSettings(); - RocketChat.getPermissions(); - RocketChat.getCustomEmoji(); - this.ddp.subscribe('activeUsers'); - this.ddp.subscribe('roles'); - }); - - this.ddp.on('error', (err) => { - alert(JSON.stringify(err)); - reduxStore.dispatch(connectFailure()); - }); - - this.ddp.on('users', ddpMessage => RocketChat._setUser(ddpMessage)); + })); this.ddp.on('stream-room-messages', (ddpMessage) => { - const message = this._buildMessage(ddpMessage.fields.args[0]); - return reduxStore.dispatch(roomMessageReceived(message)); + const message = _buildMessage(ddpMessage.fields.args[0]); + requestAnimationFrame(() => reduxStore.dispatch(roomMessageReceived(message))); }); - this.ddp.on('stream-notify-room', (ddpMessage) => { + this.ddp.on('stream-notify-room', protectedFunction((ddpMessage) => { const [_rid, ev] = ddpMessage.fields.eventName.split('/'); if (ev !== 'typing') { return; } return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] })); - }); + })); - this.ddp.on('stream-notify-user', (ddpMessage) => { + this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => { const [type, data] = ddpMessage.fields.args; const [, ev] = ddpMessage.fields.eventName.split('/'); if (/subscriptions/.test(ev)) { @@ -161,6 +189,11 @@ const RocketChat = { } else { data.blocked = false; } + if (data.mobilePushNotifications === 'nothing') { + data.notifications = true; + } else { + data.notifications = false; + } database.write(() => { database.create('subscriptions', data, true); }); @@ -178,11 +211,33 @@ const RocketChat = { sub.reactWhenReadOnly = data.reactWhenReadOnly; sub.archived = data.archived; sub.joinCodeRequired = data.joinCodeRequired; + if (data.muted) { + sub.muted = data.muted.map(m => ({ value: m })); + } }); } - }); + if (/message/.test(ev)) { + const [args] = ddpMessage.fields.args; + const _id = Random.id(); + const message = { + _id, + rid: args.rid, + msg: args.msg, + ts: new Date(), + _updatedAt: new Date(), + status: messagesStatus.SENT, + u: { + _id, + username: 'rocket.cat' + } + }; + requestAnimationFrame(() => database.write(() => { + database.create('messages', message, true); + })); + } + })); - this.ddp.on('rocketchat_starred_message', (ddpMessage) => { + this.ddp.on('rocketchat_starred_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.starredMessages = this.starredMessages || []; @@ -191,14 +246,14 @@ const RocketChat = { this.starredMessagesTimer = null; } - this.starredMessagesTimer = setTimeout(() => { + this.starredMessagesTimer = setTimeout(protectedFunction(() => { reduxStore.dispatch(starredMessagesReceived(this.starredMessages)); this.starredMessagesTimer = null; return this.starredMessages = []; - }, 1000); + }), 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const starredMessage = this._buildMessage(message); + const starredMessage = _buildMessage(message); this.starredMessages = [...this.starredMessages, starredMessage]; } if (ddpMessage.msg === 'removed') { @@ -206,9 +261,9 @@ const RocketChat = { return reduxStore.dispatch(starredMessageUnstarred(ddpMessage.id)); } } - }); + })); - this.ddp.on('rocketchat_pinned_message', (ddpMessage) => { + this.ddp.on('rocketchat_pinned_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.pinnedMessages = this.pinnedMessages || []; @@ -224,7 +279,7 @@ const RocketChat = { }, 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const pinnedMessage = this._buildMessage(message); + const pinnedMessage = _buildMessage(message); this.pinnedMessages = [...this.pinnedMessages, pinnedMessage]; } if (ddpMessage.msg === 'removed') { @@ -232,9 +287,9 @@ const RocketChat = { return reduxStore.dispatch(pinnedMessageUnpinned(ddpMessage.id)); } } - }); + })); - this.ddp.on('rocketchat_mentioned_message', (ddpMessage) => { + this.ddp.on('rocketchat_mentioned_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.mentionedMessages = this.mentionedMessages || []; @@ -250,12 +305,12 @@ const RocketChat = { }, 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const mentionedMessage = this._buildMessage(message); + const mentionedMessage = _buildMessage(message); this.mentionedMessages = [...this.mentionedMessages, mentionedMessage]; } - }); + })); - this.ddp.on('rocketchat_snippeted_message', (ddpMessage) => { + this.ddp.on('rocketchat_snippeted_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.snippetedMessages = this.snippetedMessages || []; @@ -271,12 +326,12 @@ const RocketChat = { }, 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const snippetedMessage = this._buildMessage(message); + const snippetedMessage = _buildMessage(message); this.snippetedMessages = [...this.snippetedMessages, snippetedMessage]; } - }); + })); - this.ddp.on('room_files', (ddpMessage) => { + this.ddp.on('room_files', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.roomFiles = this.roomFiles || []; @@ -318,9 +373,9 @@ const RocketChat = { } this.roomFiles = [...this.roomFiles, message]; } - }); + })); - this.ddp.on('meteor_accounts_loginServiceConfiguration', (ddpMessage) => { + this.ddp.on('meteor_accounts_loginServiceConfiguration', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.loginServices = this.loginServices || {}; if (this.loginServiceTimer) { @@ -340,9 +395,9 @@ const RocketChat = { } this.loginServiceTimer = setTimeout(() => reduxStore.dispatch(removeLoginServices()), 1000); } - }); + })); - this.ddp.on('rocketchat_roles', (ddpMessage) => { + this.ddp.on('rocketchat_roles', protectedFunction((ddpMessage) => { this.roles = this.roles || {}; if (this.roleTimer) { @@ -353,39 +408,38 @@ const RocketChat = { reduxStore.dispatch(setRoles(this.roles)); database.write(() => { - _.forEach(this.roles, (description, _id) => { + foreach(this.roles, (description, _id) => { database.create('roles', { _id, description }, true); }); }); this.roleTimer = null; return this.roles = {}; - }, 5000); - this.roles[ddpMessage.id] = ddpMessage.fields.description; - }); - }).catch(console.log); - }, + }, 1000); + this.roles[ddpMessage.id] = (ddpMessage.fields && ddpMessage.fields.description) || undefined; + })); - me({ server, token, userId }) { - return fetch(`${ server }/api/v1/me`, { - method: 'get', - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': token, - 'X-User-Id': userId - } - }).then(response => response.json()); - }, + this.ddp.on('error', protectedFunction((err) => { + console.warn('onError', JSON.stringify(err)); + Answers.logCustom('disconnect', err); + reduxStore.dispatch(connectFailure()); + })); - userInfo({ server, token, userId }) { - return fetch(`${ server }/api/v1/users.info?userId=${ userId }`, { - method: 'get', - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': token, - 'X-User-Id': userId - } - }).then(response => response.json()); + // TODO: fix api (get emojis by date/version....) + + this.ddp.on('open', protectedFunction(() => { + RocketChat.getSettings(); + RocketChat.getPermissions(); + reduxStore.dispatch(connectSuccess()); + resolve(); + })); + + this.ddp.once('open', protectedFunction(() => { + this.ddp.subscribe('activeUsers'); + this.ddp.subscribe('roles'); + RocketChat.getCustomEmoji(); + })); + }).catch(err => console.warn(`asd ${ err }`)); }, register({ credentials }) { @@ -442,19 +496,18 @@ const RocketChat = { return this.login(params, callback); }, - loadSubscriptions(cb) { - this.ddp.call('subscriptions/get').then((data) => { - if (data.length) { - database.write(() => { - data.forEach((subscription) => { - database.create('subscriptions', subscription, true); - }); - }); - } - - return cb && cb(); - }); + login(params) { + return this.ddp.login(params); }, + logout({ server }) { + if (this.ddp) { + this.ddp.logout(); + } + database.deleteAll(); + AsyncStorage.removeItem(TOKEN_KEY); + AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`); + }, + registerPushToken(id, token) { const key = Platform.OS === 'ios' ? 'apn' : 'gcm'; const data = { @@ -470,92 +523,32 @@ const RocketChat = { updatePushToken(pushId) { return call('raix:push-setuser', pushId); }, - - _parseUrls(urls) { - return urls.filter(url => url.meta && !url.ignoreParse).map((url, index) => { - const tmp = {}; - const { meta } = url; - tmp._id = index; - tmp.title = meta.ogTitle || meta.twitterTitle || meta.title || meta.pageTitle || meta.oembedTitle; - tmp.description = meta.ogDescription || meta.twitterDescription || meta.description || meta.oembedAuthorName; - let decodedOgImage; - if (meta.ogImage) { - decodedOgImage = meta.ogImage.replace(/&/g, '&'); + loadMissedMessages, + loadMessagesForRoom, + getMessage, + sendMessage, + getRooms, + readMessages, + me({ server = reduxStore.getState().server.server, token, userId }) { + return fetch(`${ server }/api/v1/me`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': userId } - tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl; - tmp.url = url.url; - return tmp; - }); - }, - _buildMessage(message) { - message.status = messagesStatus.SENT; - normalizeMessage(message); - message.urls = message.urls ? RocketChat._parseUrls(message.urls) : []; - message._updatedAt = new Date(); - // loadHistory returns message.starred as object - // stream-room-messages returns message.starred as an array - message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred); - return message; - }, - loadMessagesForRoom(rid, end, cb) { - return this.ddp.call('loadHistory', rid, end, 20).then((data) => { - if (data && data.messages.length) { - const messages = data.messages.map(message => this._buildMessage(message)); - database.write(() => { - messages.forEach((message) => { - database.create('messages', message, true); - }); - }); - } - if (cb) { - cb({ end: data && data.messages.length < 20 }); - } - return data.message; - }, (err) => { - if (err) { - if (cb) { - cb({ end: true }); - } - return Promise.reject(err); - } - }); + }).then(response => response.json()); }, - getMessage(rid, msg = {}) { - const _id = Random.id(); - const message = { - _id, - rid, - msg, - ts: new Date(), - _updatedAt: new Date(), - status: messagesStatus.TEMP, - u: { - _id: reduxStore.getState().login.user.id || '1', - username: reduxStore.getState().login.user.username + userInfo({ server = reduxStore.getState().server.server, token, userId }) { + return fetch(`${ server }/api/v1/users.info?userId=${ userId }`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': userId } - }; - - database.write(() => { - database.create('messages', message, true); - }); - return message; - }, - async _sendMessageCall(message) { - const { _id, rid, msg } = message; - const sendMessageCall = call('sendMessage', { _id, rid, msg }); - const timeoutCall = new Promise(resolve => setTimeout(resolve, SERVER_TIMEOUT, 'timeout')); - const result = await Promise.race([sendMessageCall, timeoutCall]); - if (result === 'timeout') { - database.write(() => { - message.status = messagesStatus.ERROR; - database.create('messages', message, true); - }); - } - }, - async sendMessage(rid, msg) { - const tempMessage = this.getMessage(rid, msg); - return RocketChat._sendMessageCall(tempMessage); + }).then(response => response.json()); }, async resendMessage(messageId) { const message = await database.objects('messages').filtered('_id = $0', messageId)[0]; @@ -563,7 +556,7 @@ const RocketChat = { message.status = messagesStatus.TEMP; database.create('messages', message, true); }); - return RocketChat._sendMessageCall(message); + return _sendMessageCall(JSON.parse(JSON.stringify(message))); }, spotlight(search, usernames, type) { @@ -573,16 +566,6 @@ const RocketChat = { createDirectMessage(username) { return call('createDirectMessage', username); }, - async readMessages(rid) { - const ret = await call('readMessages', rid); - - const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid); - database.write(() => { - subscription.lastOpen = new Date(); - }); - - return ret; - }, joinRoom(rid) { return call('joinRoom', rid); }, @@ -646,6 +629,7 @@ const RocketChat = { } catch (e) { return e; } finally { + // TODO: fix that try { database.write(() => { const msg = database.objects('messages').filtered('_id = $0', placeholder._id); @@ -656,93 +640,9 @@ const RocketChat = { } } }, - async getRooms() { - const { login } = reduxStore.getState(); - let lastMessage = database - .objects('subscriptions') - .sorted('roomUpdatedAt', true)[0]; - lastMessage = lastMessage && new Date(lastMessage.roomUpdatedAt); - let [subscriptions, rooms] = await Promise.all([call('subscriptions/get', lastMessage), call('rooms/get', lastMessage)]); - - if (lastMessage) { - subscriptions = subscriptions.update; - rooms = rooms.update; - } - - const data = subscriptions.map((subscription) => { - const room = rooms.find(({ _id }) => _id === subscription.rid); - if (room) { - subscription.roomUpdatedAt = room._updatedAt; - subscription.lastMessage = normalizeMessage(room.lastMessage); - subscription.ro = room.ro; - subscription.description = room.description; - subscription.topic = room.topic; - subscription.announcement = room.announcement; - subscription.reactWhenReadOnly = room.reactWhenReadOnly; - subscription.archived = room.archived; - subscription.joinCodeRequired = room.joinCodeRequired; - } - if (subscription.roles) { - subscription.roles = subscription.roles.map(role => ({ value: role })); - } - return subscription; - }); - - - database.write(() => { - data.forEach(subscription => database.create('subscriptions', subscription, true)); - // rooms.forEach(room => database.create('rooms', room, true)); - }); - - - this.ddp.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false); - this.ddp.subscribe('stream-notify-user', `${ login.user.id }/rooms-changed`, false); - return data; - }, - disconnect() { - if (!this.ddp) { - return; - } - reduxStore.dispatch(disconnect_by_user()); - delete this.ddp; - return this.ddp.disconnect(); - }, - login(params, callback) { - return this.ddp.call('login', params).then((result) => { - if (typeof callback === 'function') { - callback(null, result); - } - return result; - }, (err) => { - if (/user not found/i.test(err.reason)) { - err.error = 1; - err.reason = 'User or Password incorrect'; - err.message = 'User or Password incorrect'; - } - if (typeof callback === 'function') { - callback(err, null); - } - return Promise.reject(err); - }); - }, - logout({ server }) { - if (this.ddp) { - this.ddp.logout(); - } - database.deleteAll(); - AsyncStorage.removeItem(TOKEN_KEY); - AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`); - }, - async getSettings() { - const temp = database.objects('settings').sorted('_updatedAt', true)[0]; - const result = await (!temp ? call('public-settings/get') : call('public-settings/get', new Date(temp._updatedAt))); - const settings = temp ? result.update : result; - const filteredSettings = RocketChat._prepareSettings(RocketChat._filterSettings(settings)); - database.write(() => { - filteredSettings.forEach(setting => database.create('settings', setting, true)); - }); - reduxStore.dispatch(actions.addSettings(RocketChat.parseSettings(filteredSettings))); - }, + getSettings, + getPermissions, + getCustomEmoji, parseSettings: settings => settings.reduce((ret, item) => { ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.valueAsNumber || item.valueAsBoolean || item.value; @@ -755,16 +655,6 @@ const RocketChat = { }); }, _filterSettings: settings => settings.filter(setting => settingsType[setting.type] && setting.value), - async getPermissions() { - const temp = database.objects('permissions').sorted('_updatedAt', true)[0]; - const result = await (!temp ? call('permissions/get') : call('permissions/get', new Date(temp._updatedAt))); - let permissions = temp ? result.update : result; - permissions = RocketChat._preparePermissions(permissions); - database.write(() => { - permissions.forEach(permission => database.create('permissions', permission, true)); - }); - reduxStore.dispatch(actions.setAllPermissions(RocketChat.parsePermissions(permissions))); - }, parsePermissions: permissions => permissions.reduce((ret, item) => { ret[item._id] = item.roles.reduce((roleRet, role) => [...roleRet, role.value], []); return ret; @@ -775,16 +665,6 @@ const RocketChat = { }); return permissions; }, - async getCustomEmoji() { - const temp = database.objects('customEmojis').sorted('_updatedAt', true)[0]; - let emojis = await call('listEmojiCustom'); - emojis = emojis.filter(emoji => !temp || emoji._updatedAt > temp._updatedAt); - emojis = RocketChat._prepareEmojis(emojis); - database.write(() => { - emojis.forEach(emoji => database.create('customEmojis', emoji, true)); - }); - reduxStore.dispatch(actions.setCustomEmojis(RocketChat.parseEmojis(emojis))); - }, parseEmojis: emojis => emojis.reduce((ret, item) => { ret[item.name] = item.extension; item.aliases.forEach((alias) => { @@ -815,20 +695,21 @@ const RocketChat = { return call('pinMessage', message); }, getRoom(rid) { - const result = database.objects('subscriptions').filtered('rid = $0', rid); - if (result.length === 0) { + const [result] = database.objects('subscriptions').filtered('rid = $0', rid); + if (!result) { return Promise.reject(new Error('Room not found')); } - return Promise.resolve(result[0]); + return Promise.resolve(result); }, async getPermalink(message) { const room = await RocketChat.getRoom(message.rid); + const { server } = reduxStore.getState().server; const roomType = { p: 'group', c: 'channel', d: 'direct' }[room.t]; - return `${ room._server.id }/${ roomType }/${ room.name }?msg=${ message._id }`; + return `${ server }/${ roomType }/${ room.name }?msg=${ message._id }`; }, subscribe(...args) { return this.ddp.subscribe(...args); @@ -878,6 +759,12 @@ const RocketChat = { eraseRoom(rid) { return call('eraseRoom', rid); }, + toggleMuteUserInRoom(rid, username, mute) { + if (mute) { + return call('muteUserInRoom', { rid, username }); + } + return call('unmuteUserInRoom', { rid, username }); + }, toggleArchiveRoom(rid, archive) { if (archive) { return call('archiveRoom', rid); @@ -887,6 +774,17 @@ const RocketChat = { saveRoomSettings(rid, params) { return call('saveRoomSettings', rid, params); }, + saveNotificationSettings(rid, param, value) { + return call('saveNotificationSettings', rid, param, value); + }, + messageSearch(text, rid, limit) { + return call('messageSearch', text, rid, limit); + }, + addUsersToRoom(rid) { + let { users } = reduxStore.getState().selectedUsers; + users = users.map(u => u.name); + return call('addUsersToRoom', { rid, users }); + }, hasPermission(permissions, rid) { // get the room from realm const room = database.objects('subscriptions').filtered('rid = $0', rid)[0]; diff --git a/app/presentation/RoomItem.js b/app/presentation/RoomItem.js index 16677bb1d..5529dd15b 100644 --- a/app/presentation/RoomItem.js +++ b/app/presentation/RoomItem.js @@ -1,11 +1,13 @@ import React from 'react'; import moment from 'moment'; import PropTypes from 'prop-types'; -import { View, Text, StyleSheet } from 'react-native'; - +import { View, Text, StyleSheet, ViewPropTypes } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; import { connect } from 'react-redux'; import SimpleMarkdown from 'simple-markdown'; +import messagesStatus from '../constants/messagesStatus'; + import Avatar from '../containers/Avatar'; import Status from '../containers/status'; import Touch from '../utils/touch/index'; //eslint-disable-line @@ -44,7 +46,6 @@ const styles = StyleSheet.create({ flex: 1, fontSize: 18, color: '#444', - marginRight: 8 }, lastMessage: { @@ -64,8 +65,8 @@ const styles = StyleSheet.create({ // backgroundColor: '#eee' }, row: { - width: '100%', - flex: 1, + // width: '100%', + // flex: 1, flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'flex-end' @@ -145,41 +146,57 @@ const renderNumber = (unread, userMentions) => { ); }; +const attrs = ['name', 'unread', 'userMentions', 'alert', 'showLastMessage', 'type', '_updatedAt']; @connect(state => ({ user: state.login && state.login.user, - StoreLastMessage: state.settings.Store_Last_Message, - customEmojis: state.customEmojis + StoreLastMessage: state.settings.Store_Last_Message })) -export default class RoomItem extends React.PureComponent { +export default class RoomItem extends React.Component { static propTypes = { type: PropTypes.string.isRequired, name: PropTypes.string.isRequired, StoreLastMessage: PropTypes.bool, _updatedAt: PropTypes.instanceOf(Date), lastMessage: PropTypes.object, + showLastMessage: PropTypes.bool, favorite: PropTypes.bool, alert: PropTypes.bool, unread: PropTypes.number, userMentions: PropTypes.number, id: PropTypes.string, onPress: PropTypes.func, - customEmojis: PropTypes.object, - user: PropTypes.object + onLongPress: PropTypes.func, + user: PropTypes.object, + avatarSize: PropTypes.number, + statusStyle: ViewPropTypes.style } + static defaultProps = { + showLastMessage: true, + avatarSize: 46 + } + shouldComponentUpdate(nextProps) { + const oldlastMessage = this.props.lastMessage; + const newLastmessage = nextProps.lastMessage; + + if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) { + return true; + } + return attrs.some(key => nextProps[key] !== this.props[key]); + } get icon() { const { - type, name, id + type, name, id, avatarSize, statusStyle } = this.props; - return ({type === 'd' ? : null }); + return ({type === 'd' ? : null }); } get lastMessage() { const { - lastMessage, type + lastMessage, type, showLastMessage } = this.props; - if (!this.props.StoreLastMessage) { + if (!this.props.StoreLastMessage || !showLastMessage) { return ''; } if (!lastMessage) { @@ -208,7 +225,7 @@ export default class RoomItem extends React.PureComponent { render() { const { - favorite, unread, userMentions, name, _updatedAt, customEmojis, alert + favorite, unread, userMentions, name, _updatedAt, alert, status } = this.props; const date = this.formatDate(_updatedAt); @@ -224,10 +241,19 @@ export default class RoomItem extends React.PureComponent { accessibilityLabel += ', you were mentioned'; } - accessibilityLabel += `, last message ${ date }`; + if (date) { + accessibilityLabel += `, last message ${ date }`; + } return ( - + {this.icon} @@ -236,9 +262,9 @@ export default class RoomItem extends React.PureComponent { {_updatedAt ? { date } : null} + {status === messagesStatus.ERROR ? : null } item.name !== action.user.name) - }; - case CREATE_CHANNEL.RESET: - return initialState; default: return state; } diff --git a/app/reducers/index.js b/app/reducers/index.js index 3a4c0dac1..6f66738e6 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -7,6 +7,7 @@ import room from './room'; import rooms from './rooms'; import server from './server'; import navigator from './navigator'; +import selectedUsers from './selectedUsers'; import createChannel from './createChannel'; import app from './app'; import permissions from './permissions'; @@ -26,6 +27,7 @@ export default combineReducers({ messages, server, navigator, + selectedUsers, createChannel, app, room, diff --git a/app/reducers/mentionedMessages.js b/app/reducers/mentionedMessages.js index ee5883fba..f8c445dd9 100644 --- a/app/reducers/mentionedMessages.js +++ b/app/reducers/mentionedMessages.js @@ -1,11 +1,22 @@ import { MENTIONED_MESSAGES } from '../actions/actionsTypes'; const initialState = { - messages: [] + messages: [], + ready: false }; export default function server(state = initialState, action) { switch (action.type) { + case MENTIONED_MESSAGES.OPEN: + return { + ...state, + ready: false + }; + case MENTIONED_MESSAGES.READY: + return { + ...state, + ready: true + }; case MENTIONED_MESSAGES.MESSAGES_RECEIVED: return { ...state, diff --git a/app/reducers/pinnedMessages.js b/app/reducers/pinnedMessages.js index 85cc62683..d912e5b7a 100644 --- a/app/reducers/pinnedMessages.js +++ b/app/reducers/pinnedMessages.js @@ -2,7 +2,8 @@ import { PINNED_MESSAGES } from '../actions/actionsTypes'; const initialState = { messages: [], - isOpen: false + isOpen: false, + ready: false }; export default function server(state = initialState, action) { @@ -10,7 +11,13 @@ export default function server(state = initialState, action) { case PINNED_MESSAGES.OPEN: return { ...state, - isOpen: true + isOpen: true, + ready: false + }; + case PINNED_MESSAGES.READY: + return { + ...state, + ready: true }; case PINNED_MESSAGES.MESSAGES_RECEIVED: return { diff --git a/app/reducers/roomFiles.js b/app/reducers/roomFiles.js index c27382d4d..911f295a7 100644 --- a/app/reducers/roomFiles.js +++ b/app/reducers/roomFiles.js @@ -1,11 +1,22 @@ import { ROOM_FILES } from '../actions/actionsTypes'; const initialState = { - messages: [] + messages: [], + ready: false }; export default function server(state = initialState, action) { switch (action.type) { + case ROOM_FILES.OPEN: + return { + ...state, + ready: false + }; + case ROOM_FILES.READY: + return { + ...state, + ready: true + }; case ROOM_FILES.MESSAGES_RECEIVED: return { ...state, diff --git a/app/reducers/selectedUsers.js b/app/reducers/selectedUsers.js new file mode 100644 index 000000000..5f455aea4 --- /dev/null +++ b/app/reducers/selectedUsers.js @@ -0,0 +1,30 @@ +import { SELECTED_USERS } from '../actions/actionsTypes'; + +const initialState = { + users: [], + loading: false +}; + +export default function messages(state = initialState, action) { + switch (action.type) { + case SELECTED_USERS.ADD_USER: + return { + ...state, + users: state.users.concat(action.user) + }; + case SELECTED_USERS.REMOVE_USER: + return { + ...state, + users: state.users.filter(item => item.name !== action.user.name) + }; + case SELECTED_USERS.SET_LOADING: + return { + ...state, + loading: action.loading + }; + case SELECTED_USERS.RESET: + return initialState; + default: + return state; + } +} diff --git a/app/reducers/server.js b/app/reducers/server.js index 337c393e8..c68e95dbc 100644 --- a/app/reducers/server.js +++ b/app/reducers/server.js @@ -5,7 +5,8 @@ const initialState = { connected: false, errorMessage: '', failure: false, - server: '' + server: '', + adding: false }; @@ -32,8 +33,17 @@ export default function server(state = initialState, action) { failure: true, errorMessage: action.err }; + case SERVER.ADD: + return { + ...state, + adding: true + }; case SERVER.SELECT: - return { ...state, server: action.server }; + return { + ...state, + server: action.server, + adding: false + }; default: return state; } diff --git a/app/reducers/snippetedMessages.js b/app/reducers/snippetedMessages.js index 839ff7d1b..727ed41ae 100644 --- a/app/reducers/snippetedMessages.js +++ b/app/reducers/snippetedMessages.js @@ -1,11 +1,22 @@ import { SNIPPETED_MESSAGES } from '../actions/actionsTypes'; const initialState = { - messages: [] + messages: [], + ready: false }; export default function server(state = initialState, action) { switch (action.type) { + case SNIPPETED_MESSAGES.OPEN: + return { + ...state, + ready: false + }; + case SNIPPETED_MESSAGES.READY: + return { + ...state, + ready: true + }; case SNIPPETED_MESSAGES.MESSAGES_RECEIVED: return { ...state, diff --git a/app/reducers/starredMessages.js b/app/reducers/starredMessages.js index 704fbcfab..b96fbe87c 100644 --- a/app/reducers/starredMessages.js +++ b/app/reducers/starredMessages.js @@ -2,7 +2,8 @@ import { STARRED_MESSAGES } from '../actions/actionsTypes'; const initialState = { messages: [], - isOpen: false + isOpen: false, + ready: false }; export default function server(state = initialState, action) { @@ -10,7 +11,13 @@ export default function server(state = initialState, action) { case STARRED_MESSAGES.OPEN: return { ...state, - isOpen: true + isOpen: true, + ready: false + }; + case STARRED_MESSAGES.READY: + return { + ...state, + ready: true }; case STARRED_MESSAGES.MESSAGES_RECEIVED: return { diff --git a/app/sagas/connect.js b/app/sagas/connect.js index 8291073c7..7c494275d 100644 --- a/app/sagas/connect.js +++ b/app/sagas/connect.js @@ -1,44 +1,44 @@ -import { call, takeLatest, select, take, race } from 'redux-saga/effects'; -import { delay } from 'redux-saga'; +import { call, takeLatest, select, put, all } from 'redux-saga/effects'; +import { AsyncStorage } from 'react-native'; import { METEOR } from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { setToken } from '../actions/login'; const getServer = ({ server }) => server.server; - - -const connect = url => RocketChat.connect(url); -const watchConnect = function* watchConnect() { - const { disconnect } = yield race({ - disconnect: take(METEOR.DISCONNECT), - disconnected_by_user: take(METEOR.DISCONNECT_BY_USER) - }); - if (disconnect) { - while (true) { - const { connected } = yield race({ - connected: take(METEOR.SUCCESS), - timeout: call(delay, 1000) - }); - if (connected) { - return; - } - yield RocketChat.reconnect(); +const getToken = function* getToken() { + const currentServer = yield select(getServer); + const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); + if (user) { + yield put(setToken(JSON.parse(user))); + try { + yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); + } catch (error) { + console.warn('getToken', error); } + return JSON.parse(user); } + return yield put(setToken()); }; + + +const connect = (...args) => RocketChat.connect(...args); + const test = function* test() { - // try { - const server = yield select(getServer); - // const response = - yield call(connect, server); + try { + const server = yield select(getServer); + const user = yield call(getToken); + // const response = + yield all([call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined)]);// , put(loginRequest({ resume: user.token }))]); // yield put(connectSuccess(response)); - // } catch (err) { + } catch (err) { + console.warn('test', err); // yield put(connectFailure(err.status)); - // } + } }; const root = function* root() { yield takeLatest(METEOR.REQUEST, test); // yield take(METEOR.SUCCESS, watchConnect); - yield takeLatest(METEOR.SUCCESS, watchConnect); + // yield takeLatest(METEOR.SUCCESS, watchConnect); }; export default root; diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index f4161201d..b538c3fb9 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -28,4 +28,5 @@ const handleRequest = function* handleRequest({ data }) { const root = function* root() { yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest); }; + export default root; diff --git a/app/sagas/hello.js b/app/sagas/hello.js deleted file mode 100644 index 83a72731e..000000000 --- a/app/sagas/hello.js +++ /dev/null @@ -1,14 +0,0 @@ -import { take, fork } from 'redux-saga/effects'; - -const foreverAlone = function* foreverAlone() { - yield take('FOI'); - console.log('FOIIIIIII'); - yield take('voa'); - console.log('o'); -}; - -const root = function* root() { - yield fork(foreverAlone); -}; - -export default root; diff --git a/app/sagas/index.js b/app/sagas/index.js index ab82fdefc..6489d7a32 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -1,5 +1,4 @@ import { all } from 'redux-saga/effects'; -import hello from './hello'; import login from './login'; import connect from './connect'; import rooms from './rooms'; @@ -18,7 +17,6 @@ const root = function* root() { yield all([ init(), createChannel(), - hello(), rooms(), login(), connect(), diff --git a/app/sagas/init.js b/app/sagas/init.js index 423e91a96..0360d82b3 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -2,15 +2,13 @@ import { AsyncStorage } from 'react-native'; import { call, put, takeLatest } from 'redux-saga/effects'; import * as actions from '../actions'; import { setServer } from '../actions/server'; -import { restoreToken } from '../actions/login'; +import { restoreToken, setUser } from '../actions/login'; import { APP } from '../actions/actionsTypes'; -import { setRoles } from '../actions/roles'; -import database from '../lib/realm'; import RocketChat from '../lib/rocketchat'; const restore = function* restore() { try { - const token = yield call([AsyncStorage, 'getItem'], 'reactnativemeteor_usertoken'); + const token = yield call([AsyncStorage, 'getItem'], RocketChat.TOKEN_KEY); if (token) { yield put(restoreToken(token)); } @@ -18,21 +16,16 @@ const restore = function* restore() { const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); if (currentServer) { yield put(setServer(currentServer)); - const settings = database.objects('settings'); - yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); - const permissions = database.objects('permissions'); - yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); - const emojis = database.objects('customEmojis'); - yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length)))); - const roles = database.objects('roles'); - yield put(setRoles(roles.reduce((result, role) => { - result[role._id] = role.description; - return result; - }, {}))); + + const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); + if (login && login.user) { + yield put(setUser(login.user)); + } } + yield put(actions.appReady({})); } catch (e) { - console.log(e); + console.warn('restore', e); } }; diff --git a/app/sagas/login.js b/app/sagas/login.js index f78febf50..41de7368d 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -1,16 +1,16 @@ import { AsyncStorage } from 'react-native'; -import { put, call, takeLatest, select, all, take } from 'redux-saga/effects'; -import { Answers } from 'react-native-fabric'; +import { put, call, take, takeLatest, select, all } from 'redux-saga/effects'; + import * as types from '../actions/actionsTypes'; import { - loginRequest, - loginSubmit, + // loginRequest, + // loginSubmit, registerRequest, registerIncomplete, - loginSuccess, + // loginSuccess, loginFailure, - logout, - setToken, + // logout, + // setToken, registerSuccess, setUsernameRequest, setUsernameSuccess, @@ -23,40 +23,41 @@ import * as NavigationService from '../containers/routes/NavigationService'; const getUser = state => state.login; const getServer = state => state.server.server; const getIsConnected = state => state.meteor.connected; -const loginCall = args => ((args.resume || args.oauth) ? RocketChat.login(args) : RocketChat.loginWithPassword(args)); + +// const loginCall = args => ((args.resume || args.oauth) ? RocketChat.login(args) : RocketChat.loginWithPassword(args)); +const loginCall = args => RocketChat.loginWithPassword(args); const registerCall = args => RocketChat.register(args); const setUsernameCall = args => RocketChat.setUsername(args); +const loginSuccessCall = () => RocketChat.loginSuccess(); const logoutCall = args => RocketChat.logout(args); -const meCall = args => RocketChat.me(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args); -const userInfoCall = args => RocketChat.userInfo(args); -const getToken = function* getToken() { - const currentServer = yield select(getServer); - const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); - if (user) { - try { - yield put(setToken(JSON.parse(user))); - yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); - return JSON.parse(user); - } catch (e) { - console.log('getTokenerr', e); - } - } else { - return yield put(setToken()); - } -}; +// const getToken = function* getToken() { +// const currentServer = yield select(getServer); +// const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); +// if (user) { +// try { +// yield put(setToken(JSON.parse(user))); +// yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); +// return JSON.parse(user); +// } catch (e) { +// console.log('getTokenerr', e); +// } +// } else { +// return yield put(setToken()); +// } +// }; -const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() { - try { - const user = yield call(getToken); - if (user.token) { - yield put(loginRequest({ resume: user.token })); - } - } catch (e) { - console.log(e); - } -}; +// const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() { +// try { +// const user = yield call(getToken); +// if (user.token) { +// yield put(loginRequest({ resume: user.token })); +// } +// } catch (e) { +// console.log(e); +// } +// }; const saveToken = function* saveToken() { const [server, user] = yield all([select(getServer), select(getUser)]); @@ -64,41 +65,29 @@ const saveToken = function* saveToken() { yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); const token = yield AsyncStorage.getItem('pushId'); if (token) { - RocketChat.registerPushToken(user.user.id, token); + yield RocketChat.registerPushToken(user.user.id, token); } - Answers.logLogin('Email', true, { server }); -}; - -const handleLoginRequest = function* handleLoginRequest({ credentials }) { - try { - const server = yield select(getServer); - const user = yield call(loginCall, credentials); - - // GET /me from REST API - const me = yield call(meCall, { server, token: user.token, userId: user.id }); - - // if user has username - if (me.username) { - const userInfo = yield call(userInfoCall, { server, token: user.token, userId: user.id }); - user.username = userInfo.user.username; - if (userInfo.user.roles) { - user.roles = userInfo.user.roles; - } - } else { - yield put(registerIncomplete()); - } - yield put(loginSuccess(user)); - } catch (err) { - if (err.error === 403) { - return yield put(logout()); - } - yield put(loginFailure(err)); + if (!user.user.username && !user.isRegistering) { + yield put(registerIncomplete()); } }; -const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { - yield put(loginRequest(credentials)); -}; +// const handleLoginRequest = function* handleLoginRequest({ credentials }) { +// try { +// // const server = yield select(getServer); +// const user = yield call(loginCall, credentials); +// yield put(loginSuccess(user)); +// } catch (err) { +// if (err.error === 403) { +// return yield put(logout()); +// } +// yield put(loginFailure(err)); +// } +// }; + +// const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { +// yield put(loginRequest(credentials)); +// }; const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) { yield put(registerRequest(credentials)); @@ -114,10 +103,14 @@ const handleRegisterRequest = function* handleRegisterRequest({ credentials }) { }; const handleRegisterSuccess = function* handleRegisterSuccess({ credentials }) { - yield put(loginSubmit({ - username: credentials.email, - password: credentials.pass - })); + try { + yield call(loginCall, { + username: credentials.email, + password: credentials.pass + }); + } catch (err) { + yield put(loginFailure(err)); + } }; const handleSetUsernameSubmit = function* handleSetUsernameSubmit({ credentials }) { @@ -128,6 +121,7 @@ const handleSetUsernameRequest = function* handleSetUsernameRequest({ credential try { yield call(setUsernameCall, { credentials }); yield put(setUsernameSuccess()); + yield call(loginSuccessCall); } catch (err) { yield put(loginFailure(err)); } @@ -154,20 +148,24 @@ const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ emai }; const watchLoginOpen = function* watchLoginOpen() { - const isConnected = yield select(getIsConnected); - if (!isConnected) { - yield take(types.METEOR.SUCCESS); + try { + const isConnected = yield select(getIsConnected); + if (!isConnected) { + yield take(types.METEOR.SUCCESS); + } + const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration'); + yield take(types.LOGIN.CLOSE); + sub.unsubscribe().catch(e => console.warn('watchLoginOpen unsubscribe', e)); + } catch (error) { + console.warn('watchLoginOpen', error); } - const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration'); - yield take(types.LOGIN.CLOSE); - sub.unsubscribe().catch(e => alert(e)); }; const root = function* root() { - yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); - yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); + // yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); + // yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); yield takeLatest(types.LOGIN.SUCCESS, saveToken); - yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit); + // yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit); yield takeLatest(types.LOGIN.REGISTER_REQUEST, handleRegisterRequest); yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit); yield takeLatest(types.LOGIN.REGISTER_SUCCESS, handleRegisterSuccess); diff --git a/app/sagas/mentionedMessages.js b/app/sagas/mentionedMessages.js index e367a2ae9..0a1d0b464 100644 --- a/app/sagas/mentionedMessages.js +++ b/app/sagas/mentionedMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyMentionedMessages } from '../actions/mentionedMessages'; -const watchMentionedMessagesRoom = function* watchMentionedMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('mentionedMessages', rid, 50); - yield take(types.MENTIONED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openMentionedMessagesRoom = function* openMentionedMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('mentionedMessages', rid, limit); + yield put(readyMentionedMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openMentionedMessagesRoom', e)); + } + sub = newSub; +}; + +const closeMentionedMessagesRoom = function* closeMentionedMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeMentionedMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeMentionedMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.MENTIONED_MESSAGES.OPEN, watchMentionedMessagesRoom); + yield takeLatest(types.MENTIONED_MESSAGES.OPEN, openMentionedMessagesRoom); + yield takeLatest(types.MENTIONED_MESSAGES.CLOSE, closeMentionedMessagesRoom); }; export default root; diff --git a/app/sagas/messages.js b/app/sagas/messages.js index 4f806aaec..7db85f934 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -1,5 +1,5 @@ -import { takeLatest, select, take, put, call } from 'redux-saga/effects'; -import { MESSAGES, LOGIN } from '../actions/actionsTypes'; +import { takeLatest, put, call } from 'redux-saga/effects'; +import { MESSAGES } from '../actions/actionsTypes'; import { messagesSuccess, messagesFailure, @@ -22,16 +22,16 @@ const toggleStarMessage = message => RocketChat.toggleStarMessage(message); const getPermalink = message => RocketChat.getPermalink(message); const togglePinMessage = message => RocketChat.togglePinMessage(message); -const get = function* get({ rid }) { - const auth = yield select(state => state.login.isAuthenticated); - if (!auth) { - yield take(LOGIN.SUCCESS); - } +const get = function* get({ room }) { try { - yield RocketChat.loadMessagesForRoom(rid, null); + if (room.lastOpen) { + yield RocketChat.loadMissedMessages(room); + } else { + yield RocketChat.loadMessagesForRoom(room); + } yield put(messagesSuccess()); } catch (err) { - console.log(err); + console.warn('messagesFailure', err); yield put(messagesFailure(err.status)); } }; diff --git a/app/sagas/pinnedMessages.js b/app/sagas/pinnedMessages.js index 95020a1a3..a451e322e 100644 --- a/app/sagas/pinnedMessages.js +++ b/app/sagas/pinnedMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyPinnedMessages } from '../actions/pinnedMessages'; -const watchPinnedMessagesRoom = function* watchPinnedMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('pinnedMessages', rid, 50); - yield take(types.PINNED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openPinnedMessagesRoom = function* openPinnedMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('pinnedMessages', rid, limit); + yield put(readyPinnedMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openPinnedMessagesRoom', e)); + } + sub = newSub; +}; + +const closePinnedMessagesRoom = function* closePinnedMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closePinnedMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closePinnedMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.PINNED_MESSAGES.OPEN, watchPinnedMessagesRoom); + yield takeLatest(types.PINNED_MESSAGES.OPEN, openPinnedMessagesRoom); + yield takeLatest(types.PINNED_MESSAGES.CLOSE, closePinnedMessagesRoom); }; export default root; diff --git a/app/sagas/roomFiles.js b/app/sagas/roomFiles.js index a2be1c530..28e919dc4 100644 --- a/app/sagas/roomFiles.js +++ b/app/sagas/roomFiles.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyRoomFiles } from '../actions/roomFiles'; -const watchRoomFiles = function* watchRoomFiles({ rid }) { - const sub = yield RocketChat.subscribe('roomFiles', rid, 50); - yield take(types.ROOM_FILES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openRoomFiles = function* openRoomFiles({ rid, limit }) { + newSub = yield RocketChat.subscribe('roomFiles', rid, limit); + yield put(readyRoomFiles()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openRoomFiles', e)); + } + sub = newSub; +}; + +const closeRoomFiles = function* closeRoomFiles() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeRoomFiles sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeRoomFiles newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.ROOM_FILES.OPEN, watchRoomFiles); + yield takeLatest(types.ROOM_FILES.OPEN, openRoomFiles); + yield takeLatest(types.ROOM_FILES.CLOSE, closeRoomFiles); }; export default root; diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index ded0d4e63..63c1b335c 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -1,9 +1,9 @@ import { Alert } from 'react-native'; import { put, call, takeLatest, take, select, race, fork, cancel, takeEvery } from 'redux-saga/effects'; import { delay } from 'redux-saga'; -import { FOREGROUND, BACKGROUND } from 'redux-enhancer-react-native-appstate'; +import { BACKGROUND } from 'redux-enhancer-react-native-appstate'; import * as types from '../actions/actionsTypes'; -import { roomsSuccess, roomsFailure } from '../actions/rooms'; +// import { roomsSuccess, roomsFailure } from '../actions/rooms'; import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room'; import { messagesRequest } from '../actions/messages'; import RocketChat from '../lib/rocketchat'; @@ -13,18 +13,18 @@ import * as NavigationService from '../containers/routes/NavigationService'; const leaveRoom = rid => RocketChat.leaveRoom(rid); const eraseRoom = rid => RocketChat.eraseRoom(rid); -const getRooms = function* getRooms() { - return yield RocketChat.getRooms(); -}; +// const getRooms = function* getRooms() { +// return yield RocketChat.getRooms(); +// }; -const watchRoomsRequest = function* watchRoomsRequest() { - try { - yield call(getRooms); - yield put(roomsSuccess()); - } catch (err) { - yield put(roomsFailure(err.status)); - } -}; +// const watchRoomsRequest = function* watchRoomsRequest() { +// try { +// yield call(getRooms); +// yield put(roomsSuccess()); +// } catch (err) { +// yield put(roomsFailure(err.status)); +// } +// }; const cancelTyping = function* cancelTyping(username) { while (true) { @@ -50,45 +50,46 @@ const usersTyping = function* usersTyping({ rid }) { } }; const handleMessageReceived = function* handleMessageReceived({ message }) { - const room = yield select(state => state.room); + try { + const room = yield select(state => state.room); - if (message.rid === room.rid) { - database.write(() => { - database.create('messages', message, true); - }); + if (message.rid === room.rid) { + database.write(() => { + database.create('messages', message, true); + }); - RocketChat.readMessages(room.rid); + RocketChat.readMessages(room.rid); + } + } catch (e) { + console.warn('handleMessageReceived', e); } }; const watchRoomOpen = function* watchRoomOpen({ room }) { - const auth = yield select(state => state.login.isAuthenticated); - if (!auth) { - yield take(types.LOGIN.SUCCESS); - } + yield put(messagesRequest({ ...room })); + // const { open } = yield race({ + // messages: take(types.MESSAGES.SUCCESS), + // open: take(types.ROOM.OPEN) + // }); + // + // if (open) { + // return; + // } - - yield put(messagesRequest({ rid: room.rid })); - - const { open } = yield race({ - messages: take(types.MESSAGES.SUCCESS), - open: take(types.ROOM.OPEN) - }); - - if (open) { - return; - } RocketChat.readMessages(room.rid); - const subscriptions = yield Promise.all([RocketChat.subscribe('stream-room-messages', room.rid, false), RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false)]); + const sub = yield RocketChat.subscribeRoom(room); + // const subscriptions = yield Promise.all([RocketChat.subscribe('stream-room-messages', room.rid, false), RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false)]); const thread = yield fork(usersTyping, { rid: room.rid }); yield race({ open: take(types.ROOM.OPEN), close: take(types.ROOM.CLOSE) }); cancel(thread); - subscriptions.forEach((sub) => { - sub.unsubscribe().catch(e => alert(e)); - }); + sub.stop(); + + // subscriptions.forEach((sub) => { + // sub.unsubscribe().catch(e => alert(e)); + // }); }; const watchuserTyping = function* watchuserTyping({ status }) { @@ -110,13 +111,13 @@ const watchuserTyping = function* watchuserTyping({ status }) { } }; -const updateRoom = function* updateRoom() { - const room = yield select(state => state.room); - if (!room || !room.rid) { - return; - } - yield put(messagesRequest({ rid: room.rid })); -}; +// const updateRoom = function* updateRoom() { +// const room = yield select(state => state.room); +// if (!room || !room.rid) { +// return; +// } +// yield put(messagesRequest({ rid: room.rid })); +// }; const updateLastOpen = function* updateLastOpen() { yield put(setLastOpen()); @@ -157,11 +158,10 @@ const handleEraseRoom = function* handleEraseRoom({ rid }) { const root = function* root() { yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); - yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest); yield takeLatest(types.ROOM.OPEN, watchRoomOpen); yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived); - yield takeLatest(FOREGROUND, updateRoom); - yield takeLatest(FOREGROUND, watchRoomsRequest); + // yield takeLatest(FOREGROUND, updateRoom); + // yield takeLatest(FOREGROUND, watchRoomsRequest); yield takeLatest(BACKGROUND, updateLastOpen); yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); yield takeLatest(types.ROOM.ERASE, handleEraseRoom); diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index ff67573a4..6aaf7ae3f 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -3,8 +3,9 @@ import { delay } from 'redux-saga'; import { AsyncStorage } from 'react-native'; import { SERVER } from '../actions/actionsTypes'; import * as actions from '../actions'; -import { connectRequest, disconnect, disconnect_by_user } from '../actions/connect'; -import { changedServer, serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server'; +import { connectRequest } from '../actions/connect'; +import { serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server'; +import { setRoles } from '../actions/roles'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; import * as NavigationService from '../containers/routes/NavigationService'; @@ -14,16 +15,28 @@ const validate = function* validate(server) { }; const selectServer = function* selectServer({ server }) { - yield database.setActiveDB(server); - yield put(disconnect_by_user()); - yield put(disconnect()); - yield put(changedServer(server)); - yield call([AsyncStorage, 'setItem'], 'currentServer', server); - const settings = database.objects('settings'); - yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); - const permissions = database.objects('permissions'); - yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); - yield put(connectRequest(server)); + try { + yield database.setActiveDB(server); + + // yield RocketChat.disconnect(); + + yield call([AsyncStorage, 'setItem'], 'currentServer', server); + const settings = database.objects('settings'); + yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); + const permissions = database.objects('permissions'); + yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); + const emojis = database.objects('customEmojis'); + yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length)))); + const roles = database.objects('roles'); + yield put(setRoles(roles.reduce((result, role) => { + result[role._id] = role.description; + return result; + }, {}))); + + yield put(connectRequest(server)); + } catch (e) { + console.warn('selectServer', e); + } }; const validateServer = function* validateServer({ server }) { @@ -32,7 +45,7 @@ const validateServer = function* validateServer({ server }) { yield call(validate, server); yield put(serverSuccess()); } catch (e) { - console.log(e); + console.warn('validateServer', e); yield put(serverFailure(e)); } }; diff --git a/app/sagas/snippetedMessages.js b/app/sagas/snippetedMessages.js index 081c0f90f..120cb12f9 100644 --- a/app/sagas/snippetedMessages.js +++ b/app/sagas/snippetedMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readySnippetedMessages } from '../actions/snippetedMessages'; -const watchSnippetedMessagesRoom = function* watchSnippetedMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('snippetedMessages', rid, 50); - yield take(types.SNIPPETED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openSnippetedMessagesRoom = function* openSnippetedMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('snippetedMessages', rid, limit); + yield put(readySnippetedMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openSnippetedMessagesRoom', e)); + } + sub = newSub; +}; + +const closeSnippetedMessagesRoom = function* closeSnippetedMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeSnippetedMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeSnippetedMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.SNIPPETED_MESSAGES.OPEN, watchSnippetedMessagesRoom); + yield takeLatest(types.SNIPPETED_MESSAGES.OPEN, openSnippetedMessagesRoom); + yield takeLatest(types.SNIPPETED_MESSAGES.CLOSE, closeSnippetedMessagesRoom); }; export default root; diff --git a/app/sagas/starredMessages.js b/app/sagas/starredMessages.js index 4d07d65ed..2ee8d2d1b 100644 --- a/app/sagas/starredMessages.js +++ b/app/sagas/starredMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyStarredMessages } from '../actions/starredMessages'; -const watchStarredMessagesRoom = function* watchStarredMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('starredMessages', rid, 50); - yield take(types.STARRED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openStarredMessagesRoom = function* openStarredMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('starredMessages', rid, limit); + yield put(readyStarredMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openStarredMessagesRoom', e)); + } + sub = newSub; +}; + +const closeStarredMessagesRoom = function* closeStarredMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeStarredMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeStarredMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.STARRED_MESSAGES.OPEN, watchStarredMessagesRoom); + yield takeLatest(types.STARRED_MESSAGES.OPEN, openStarredMessagesRoom); + yield takeLatest(types.STARRED_MESSAGES.CLOSE, closeStarredMessagesRoom); }; export default root; diff --git a/static/images/logo.png b/app/static/images/logo.png similarity index 100% rename from static/images/logo.png rename to app/static/images/logo.png diff --git a/static/images/logo_with_text.png b/app/static/images/logo_with_text.png similarity index 100% rename from static/images/logo_with_text.png rename to app/static/images/logo_with_text.png diff --git a/app/static/images/planet.png b/app/static/images/planet.png new file mode 100644 index 0000000000000000000000000000000000000000..29989ae75866db8d1c1532eaddce8f0c3fe317d6 GIT binary patch literal 20718 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 { + const _throttle = (...args) => { const context = scope || this; const now = +new Date(); @@ -19,4 +19,8 @@ export default function throttle(fn, threshhold = 250, scope) { fn.apply(context, args); } }; + + _throttle.stop = () => clearTimeout(deferTimer); + + return _throttle; } diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index acf8a8ec4..8b8e350bc 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -1,18 +1,20 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { TextInput, View, Text, Switch, TouchableOpacity, SafeAreaView } from 'react-native'; -import Spinner from 'react-native-loading-spinner-overlay'; +import { View, Text, Switch, TouchableOpacity, SafeAreaView, ScrollView } from 'react-native'; +import RCTextInput from '../containers/TextInput'; +import Loading from '../containers/Loading'; import LoggedView from './View'; import { createChannelRequest } from '../actions/createChannel'; import styles from './Styles'; import KeyboardView from '../presentation/KeyboardView'; +import scrollPersistTaps from '../utils/scrollPersistTaps'; @connect( state => ({ createChannel: state.createChannel, - users: state.createChannel.users + users: state.selectedUsers.users }), dispatch => ({ create: data => dispatch(createChannelRequest(data)) @@ -84,52 +86,52 @@ export default class CreateChannelView extends LoggedView { render() { return ( - - Channel Name - this.setState({ channelName })} - autoCorrect={false} - returnKeyType='done' - autoCapitalize='none' - autoFocus - placeholder='Type the channel name here' - /> - {this.renderChannelNameError()} - {this.renderTypeSwitch()} - - {this.state.type ? ( - 'Everyone can access this channel' - ) : ( - 'Just invited people can access this channel' - )} - - this.submit()} - style={[styles.buttonContainer_white, styles.enabledButton]} - > - CREATE - - - + + + this.setState({ channelName })} + placeholder='Type the channel name here' + returnKeyType='done' + autoFocus + /> + {this.renderChannelNameError()} + {this.renderTypeSwitch()} + + {this.state.type ? ( + 'Everyone can access this channel' + ) : ( + 'Just invited people can access this channel' + )} + + this.submit()} + style={[ + styles.buttonContainer_white, + this.state.channelName.length === 0 || this.props.createChannel.isFetching + ? styles.disabledButton + : styles.enabledButton + ]} + > + CREATE + + + + ); } diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js index caf2aa6b5..167bb5beb 100644 --- a/app/views/ForgotPasswordView.js +++ b/app/views/ForgotPasswordView.js @@ -1,17 +1,25 @@ import React from 'react'; -import Spinner from 'react-native-loading-spinner-overlay'; import PropTypes from 'prop-types'; -import { Text, TextInput, View, TouchableOpacity, SafeAreaView } from 'react-native'; +import { Text, View, SafeAreaView, ScrollView } from 'react-native'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import LoggedView from './View'; -import * as loginActions from '../actions/login'; +import { forgotPasswordInit, forgotPasswordRequest } from '../actions/login'; import KeyboardView from '../presentation/KeyboardView'; +import TextInput from '../containers/TextInput'; +import Button from '../containers/Button'; +import Loading from '../containers/Loading'; import styles from './Styles'; import { showErrorAlert } from '../utils/info'; +import scrollPersistTaps from '../utils/scrollPersistTaps'; -class ForgotPasswordView extends LoggedView { +@connect(state => ({ + login: state.login +}), dispatch => ({ + forgotPasswordInit: () => dispatch(forgotPasswordInit()), + forgotPasswordRequest: email => dispatch(forgotPasswordRequest(email)) +})) +export default class ForgotPasswordView extends LoggedView { static propTypes = { forgotPasswordInit: PropTypes.func.isRequired, forgotPasswordRequest: PropTypes.func.isRequired, @@ -58,10 +66,11 @@ class ForgotPasswordView extends LoggedView { } resetPassword = () => { - if (this.state.invalidEmail) { + const { email, invalidEmail } = this.state; + if (invalidEmail || !email) { return; } - this.props.forgotPasswordRequest(this.state.email); + this.props.forgotPasswordRequest(email); } backLogin = () => { @@ -74,47 +83,35 @@ class ForgotPasswordView extends LoggedView { contentContainerStyle={styles.container} keyboardVerticalOffset={128} > - - - - this.validate(email)} - keyboardType='email-address' - autoCorrect={false} - returnKeyType='next' - autoCapitalize='none' - underlineColorAndroid='transparent' - onSubmitEditing={() => this.resetPassword()} - placeholder='Email' - /> + + + + + this.validate(email)} + onSubmitEditing={() => this.resetPassword()} + /> - - RESET PASSWORD - + +