diff --git a/.circleci/config.yml b/.circleci/config.yml index 09d001ed8..206b41d8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,7 +46,7 @@ jobs: e2e-test: macos: - xcode: "10.2.1" + xcode: "11.2.1" environment: BASH_ENV: "~/.nvm/nvm.sh" @@ -223,7 +223,7 @@ jobs: ios-build: macos: - xcode: "10.2.1" + xcode: "11.2.1" environment: BASH_ENV: "~/.nvm/nvm.sh" @@ -257,7 +257,8 @@ jobs: - run: name: Update Fastlane command: | - sudo bundle install + echo "ruby-2.6.4" > ~/.ruby-version + bundle install working_directory: ios - run: @@ -319,7 +320,7 @@ jobs: ios-testflight: macos: - xcode: "10.2.1" + xcode: "11.2.1" steps: - checkout @@ -334,7 +335,8 @@ jobs: - run: name: Update Fastlane command: | - sudo bundle install + echo "ruby-2.4" > ~/.ruby-version + bundle install working_directory: ios - run: diff --git a/.eslintrc.js b/.eslintrc.js index 8548429f6..b7c949197 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { "settings": { "import/resolver": { "node": { - "extensions": [".js", ".ios.js", ".android.js"] + "extensions": [".js", ".ios.js", ".android.js", ".native.js", ".tsx"] } } }, diff --git a/README.md b/README.md index 416379bf5..0fa994229 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Readme will guide you on how to config. ## Features | Feature | Status | |--------------------------------------------------------------- |-------- | -| Jitsi Integration | ❌ | +| Jitsi Integration | ✅ | | Federation (Directory) | ✅ | | Discussions | ❌ | | Threads | ✅ | @@ -98,11 +98,11 @@ Readme will guide you on how to config. | Unread counter banner on message list | ✅ | | E2E Encryption | ❌ | | Join a Protected Room | ❌ | -| Optional Analytics | ❌ | +| Optional Analytics | ✅ | | Settings -> About us | ❌ | | Settings -> Contact us | ✅ | | Settings -> Update App Icon | ❌ | -| Settings -> Share | ❌ | +| Settings -> Share | ✅ | | Accessibility (Medium) | ❌ | | Accessibility (Advanced) | ❌ | | Authentication via Meteor | ❌ | diff --git a/__mocks__/react-native-device-info.js b/__mocks__/react-native-device-info.js index 270ca77ea..d3fa5fa66 100644 --- a/__mocks__/react-native-device-info.js +++ b/__mocks__/react-native-device-info.js @@ -1,5 +1,6 @@ export default { getModel: () => '', getReadableVersion: () => '', - getBundleId: () => '' + getBundleId: () => '', + isTablet: () => false }; diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 83b88dfc3..b6bc2022d 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -2,14 +2,9 @@ exports[`Storyshots Message list 1`] = ` @@ -23,6 +18,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -170,14 +169,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -185,15 +188,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -202,6 +209,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -408,15 +430,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -425,6 +451,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -631,15 +672,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -648,6 +693,14 @@ exports[`Storyshots Message list 1`] = ` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -836,15 +892,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -853,6 +913,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -1215,15 +1300,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -1232,6 +1321,14 @@ exports[`Storyshots Message list 1`] = ` Diego Mello @ @@ -1557,15 +1676,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -1574,6 +1697,14 @@ exports[`Storyshots Message list 1`] = ` Diego Mello @ @@ -1776,15 +1914,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -1793,6 +1935,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -1999,15 +2156,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -2016,6 +2177,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -2222,15 +2398,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -2247,12 +2427,16 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -2455,15 +2650,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -2480,12 +2679,16 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -2737,15 +2962,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -2773,7 +3002,13 @@ exports[`Storyshots Message list 1`] = ` ] } > - + @@ -2786,6 +3021,14 @@ exports[`Storyshots Message list 1`] = ` > - + @@ -2851,6 +3099,14 @@ exports[`Storyshots Message list 1`] = ` > - + @@ -2920,6 +3181,14 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -3134,15 +3410,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -3170,7 +3450,13 @@ exports[`Storyshots Message list 1`] = ` ] } > - + 1. @@ -3183,6 +3469,14 @@ exports[`Storyshots Message list 1`] = ` > - + 2. @@ -3250,6 +3549,14 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -3460,15 +3774,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -3496,7 +3814,13 @@ exports[`Storyshots Message list 1`] = ` ] } > - + 1. @@ -3509,6 +3833,14 @@ exports[`Storyshots Message list 1`] = ` > - + 2. @@ -3632,6 +3969,14 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -3842,15 +4194,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -3859,6 +4215,14 @@ exports[`Storyshots Message list 1`] = ` Diego Mello @@ -4065,15 +4436,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -4082,6 +4457,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -4288,15 +4678,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -4305,6 +4699,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -4677,15 +5083,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -4694,6 +5104,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -4906,15 +5330,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -4923,6 +5351,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -5135,15 +5577,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -5152,6 +5598,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -5493,15 +5951,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -5510,6 +5972,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -5721,15 +6199,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -5738,6 +6220,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -5982,15 +6478,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -5999,6 +6499,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -6276,15 +6778,19 @@ exports[`Storyshots Message list 1`] = ` 10 November 2017 @@ -6293,6 +6799,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -6499,15 +7020,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -6516,6 +7041,14 @@ exports[`Storyshots Message list 1`] = ` @@ -6613,15 +7147,19 @@ exports[`Storyshots Message list 1`] = ` 3 @@ -6649,6 +7187,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -6662,7 +7201,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -6670,7 +7208,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -6709,15 +7249,19 @@ exports[`Storyshots Message list 1`] = ` 13 @@ -6745,6 +7289,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -6758,7 +7303,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -6766,7 +7310,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -6782,15 +7328,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -6818,6 +7368,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -6828,16 +7379,20 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -7041,15 +7602,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -7058,6 +7623,14 @@ exports[`Storyshots Message list 1`] = ` @@ -7178,15 +7752,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7214,6 +7792,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7227,7 +7806,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -7235,7 +7813,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -7274,15 +7854,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7310,6 +7894,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7323,7 +7908,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -7331,7 +7915,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -7370,15 +7956,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7406,6 +7996,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7419,7 +8010,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -7427,7 +8017,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -7443,15 +8035,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7479,6 +8075,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7492,7 +8089,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -7500,7 +8096,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -7516,15 +8114,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7552,6 +8154,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7565,7 +8168,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -7573,7 +8175,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -7589,15 +8193,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7625,6 +8233,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7638,7 +8247,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -7646,7 +8254,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -7662,15 +8272,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7698,6 +8312,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7711,7 +8326,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "alignItems": "center", - "borderColor": "#e1e5e8", "borderRadius": 2, "borderWidth": 1, "flexDirection": "row", @@ -7719,7 +8333,9 @@ exports[`Storyshots Message list 1`] = ` "justifyContent": "center", "minWidth": 46.3, }, - false, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -7735,15 +8351,19 @@ exports[`Storyshots Message list 1`] = ` 1 @@ -7771,6 +8391,7 @@ exports[`Storyshots Message list 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "backgroundColor": "#ffffff", "borderRadius": 2, "marginBottom": 6, "marginRight": 6, @@ -7781,16 +8402,20 @@ exports[`Storyshots Message list 1`] = ` > rocket.cat @@ -7994,15 +8625,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -8011,6 +8646,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -8199,15 +8845,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -8216,6 +8866,14 @@ exports[`Storyshots Message list 1`] = ` rocket.cat @@ -8404,15 +9065,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -8421,6 +9086,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -8609,15 +9285,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -8626,6 +9306,14 @@ exports[`Storyshots Message list 1`] = ` rocket.cat @@ -8832,15 +9527,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -8849,6 +9548,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -9102,15 +9809,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -9119,6 +9830,14 @@ exports[`Storyshots Message list 1`] = ` rocket.cat @@ -9441,15 +10168,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -9458,6 +10189,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -9685,15 +10433,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -9702,6 +10454,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -9908,15 +10675,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -9949,14 +10720,17 @@ exports[`Storyshots Message list 1`] = ` Object { "overflow": "hidden", }, - Object { - "borderColor": "#e1e5e8", - "borderRadius": 4, - "borderWidth": 1, - "maxWidth": 400, - "minHeight": 200, - "width": "100%", - }, + Array [ + Object { + "borderRadius": 4, + "borderWidth": 1, + "minHeight": 200, + "width": "100%", + }, + Object { + "borderColor": "#e1e5e8", + }, + ], ] } > @@ -9978,13 +10752,21 @@ exports[`Storyshots Message list 1`] = ` } /> - + diego.mello @@ -10166,15 +10952,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -10207,14 +10997,17 @@ exports[`Storyshots Message list 1`] = ` Object { "overflow": "hidden", }, - Object { - "borderColor": "#e1e5e8", - "borderRadius": 4, - "borderWidth": 1, - "maxWidth": 400, - "minHeight": 200, - "width": "100%", - }, + Array [ + Object { + "borderRadius": 4, + "borderWidth": 1, + "minHeight": 200, + "width": "100%", + }, + Object { + "borderColor": "#e1e5e8", + }, + ], ] } > @@ -10236,13 +11029,21 @@ exports[`Storyshots Message list 1`] = ` } /> - + diego.mello @@ -10474,15 +11283,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -10518,12 +11331,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#ffffff", "fontSize": 54, }, - Object { - "color": "white", - }, + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -10536,13 +11347,21 @@ exports[`Storyshots Message list 1`] = `  - + diego.mello @@ -10754,15 +11577,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -10798,12 +11625,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#ffffff", "fontSize": 54, }, - Object { - "color": "white", - }, + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -10830,6 +11655,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -10977,14 +11806,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -10992,15 +11825,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -11009,17 +11846,22 @@ exports[`Storyshots Message list 1`] = ` View @@ -11056,12 +11898,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#1d74f5", "fontSize": 36, }, - Object { - "color": "#1d74f5", - }, + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -11086,6 +11926,7 @@ exports[`Storyshots Message list 1`] = ` disabled={false} enabled={true} inverted={false} + maximumTrackTintColor="#9ca2a8" maximumValue={0} minimumTrackTintColor="#1d74f5" minimumValue={0} @@ -11117,26 +11958,38 @@ exports[`Storyshots Message list 1`] = ` /> 00:00 - + View @@ -11381,12 +12246,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#1d74f5", "fontSize": 36, }, - Object { - "color": "#1d74f5", - }, + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -11411,6 +12274,7 @@ exports[`Storyshots Message list 1`] = ` disabled={false} enabled={true} inverted={false} + maximumTrackTintColor="#9ca2a8" maximumValue={0} minimumTrackTintColor="#1d74f5" minimumValue={0} @@ -11442,26 +12306,38 @@ exports[`Storyshots Message list 1`] = ` /> 00:00 - + View @@ -11587,12 +12468,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#1d74f5", "fontSize": 36, }, - Object { - "color": "#1d74f5", - }, + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -11617,6 +12496,7 @@ exports[`Storyshots Message list 1`] = ` disabled={false} enabled={true} inverted={false} + maximumTrackTintColor="#9ca2a8" maximumValue={0} minimumTrackTintColor="#1d74f5" minimumValue={0} @@ -11648,14 +12528,18 @@ exports[`Storyshots Message list 1`] = ` /> 00:00 @@ -11718,17 +12602,22 @@ exports[`Storyshots Message list 1`] = ` View @@ -11765,12 +12654,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#1d74f5", "fontSize": 36, }, - Object { - "color": "#1d74f5", - }, + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -11795,6 +12682,7 @@ exports[`Storyshots Message list 1`] = ` disabled={false} enabled={true} inverted={false} + maximumTrackTintColor="#9ca2a8" maximumValue={0} minimumTrackTintColor="#1d74f5" minimumValue={0} @@ -11826,14 +12714,18 @@ exports[`Storyshots Message list 1`] = ` /> 00:00 @@ -11853,6 +12745,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -12000,14 +12896,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -12015,15 +12915,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -12032,6 +12936,14 @@ exports[`Storyshots Message list 1`] = ` I'm a very long long title and I'll break 10:00 AM - + diego.mello @@ -12378,15 +13307,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -12395,6 +13328,14 @@ exports[`Storyshots Message list 1`] = ` rocket.cat 10:00 AM - + diego.mello @@ -12791,15 +13753,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -12808,6 +13774,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -13191,15 +14171,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -13208,6 +14192,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -13653,15 +14655,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -13670,6 +14676,14 @@ exports[`Storyshots Message list 1`] = ` 1 reply @@ -13767,15 +14790,19 @@ exports[`Storyshots Message list 1`] = ` Nov 10 @@ -13924,14 +14951,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -13939,15 +14970,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -13956,6 +14991,14 @@ exports[`Storyshots Message list 1`] = ` +999 replies @@ -14053,15 +15105,19 @@ exports[`Storyshots Message list 1`] = ` Nov 10 @@ -14119,11 +15175,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#1d74f5", "fontSize": 20, }, Object { - "color": "#1d74f5", "marginLeft": 16, "marginRight": 10, }, @@ -14141,14 +15196,18 @@ exports[`Storyshots Message list 1`] = ` How are you? @@ -14170,10 +15229,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -14272,6 +15336,14 @@ exports[`Storyshots Message list 1`] = ` Thread with emoji :) 😂 @@ -14429,10 +15501,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -14531,6 +15608,14 @@ exports[`Storyshots Message list 1`] = ` Markdown: link block code @@ -14688,10 +15773,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -14790,6 +15880,14 @@ exports[`Storyshots Message list 1`] = ` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -14947,10 +16045,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -15049,6 +16152,14 @@ exports[`Storyshots Message list 1`] = ` How are you? @@ -15206,10 +16317,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -15308,6 +16424,14 @@ exports[`Storyshots Message list 1`] = ` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -15431,10 +16557,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -15533,6 +16664,14 @@ exports[`Storyshots Message list 1`] = ` Thread with attachment @@ -15656,10 +16797,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -15758,13 +16904,17 @@ exports[`Storyshots Message list 1`] = ` Sent an attachment @@ -15784,6 +16934,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -15931,14 +17085,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -15946,15 +17104,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -15963,6 +17125,14 @@ exports[`Storyshots Message list 1`] = ` 1 reply @@ -16060,15 +17239,19 @@ exports[`Storyshots Message list 1`] = ` Nov 10 @@ -16204,6 +17387,14 @@ exports[`Storyshots Message list 1`] = ` Sent an attachment @@ -16564,6 +17763,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -16620,11 +17823,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#1d74f5", "fontSize": 20, }, Object { - "color": "#1d74f5", "marginLeft": 16, "marginRight": 10, }, @@ -16642,14 +17844,18 @@ exports[`Storyshots Message list 1`] = ` How are you? @@ -16671,10 +17877,15 @@ exports[`Storyshots Message list 1`] = ` } } style={ - Object { - "height": 20, - "width": 20, - } + Array [ + Object { + "height": 20, + "width": 20, + }, + Object { + "tintColor": "#caced1", + }, + ] } /> @@ -16773,6 +17984,14 @@ exports[`Storyshots Message list 1`] = ` Sent an attachment @@ -17300,6 +18533,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -17447,14 +18684,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -17462,15 +18703,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -17478,28 +18723,36 @@ exports[`Storyshots Message list 1`] = ` Started a discussion: This is a discussion @@ -17550,11 +18803,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#ffffff", "fontSize": 20, }, Object { - "color": "#fff", "marginRight": 6, }, Object { @@ -17570,13 +18822,17 @@ exports[`Storyshots Message list 1`] = ` No messages yet @@ -17584,15 +18840,19 @@ exports[`Storyshots Message list 1`] = ` @@ -17739,14 +18999,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -17754,15 +19018,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -17770,28 +19038,36 @@ exports[`Storyshots Message list 1`] = ` Started a discussion: This is a discussion @@ -17842,11 +19118,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#ffffff", "fontSize": 20, }, Object { - "color": "#fff", "marginRight": 6, }, Object { @@ -17862,13 +19137,17 @@ exports[`Storyshots Message list 1`] = ` 1 message @@ -17876,15 +19155,19 @@ exports[`Storyshots Message list 1`] = ` Nov 10 @@ -18033,14 +19316,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -18048,15 +19335,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -18064,28 +19355,36 @@ exports[`Storyshots Message list 1`] = ` Started a discussion: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -18136,11 +19435,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#ffffff", "fontSize": 20, }, Object { - "color": "#fff", "marginRight": 6, }, Object { @@ -18156,13 +19454,17 @@ exports[`Storyshots Message list 1`] = ` 10 messages @@ -18170,15 +19472,19 @@ exports[`Storyshots Message list 1`] = ` Nov 10 @@ -18327,14 +19633,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -18342,15 +19652,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -18358,28 +19672,36 @@ exports[`Storyshots Message list 1`] = ` Started a discussion: This is a discussion @@ -18430,11 +19752,10 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "color": undefined, + "color": "#ffffff", "fontSize": 20, }, Object { - "color": "#fff", "marginRight": 6, }, Object { @@ -18450,13 +19771,17 @@ exports[`Storyshots Message list 1`] = ` +999 messages @@ -18464,15 +19789,19 @@ exports[`Storyshots Message list 1`] = ` Nov 10 @@ -18492,6 +19821,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -18639,14 +19972,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -18654,15 +19991,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -18740,13 +20081,17 @@ exports[`Storyshots Message list 1`] = ` Rocket.Chat - Free, Open Source, Enterprise Team Chat @@ -18754,13 +20099,17 @@ exports[`Storyshots Message list 1`] = ` Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. @@ -18805,13 +20154,17 @@ exports[`Storyshots Message list 1`] = ` Google @@ -18819,13 +20172,17 @@ exports[`Storyshots Message list 1`] = ` Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. @@ -18975,14 +20332,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -18990,15 +20351,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -19007,6 +20372,14 @@ exports[`Storyshots Message list 1`] = ` Google @@ -19121,13 +20497,17 @@ exports[`Storyshots Message list 1`] = ` Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. @@ -19227,13 +20607,17 @@ exports[`Storyshots Message list 1`] = ` Google @@ -19241,13 +20625,17 @@ exports[`Storyshots Message list 1`] = ` Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. @@ -19268,6 +20656,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -19415,14 +20807,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -19430,15 +20826,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -19447,6 +20847,14 @@ exports[`Storyshots Message list 1`] = ` rocket.cat 10:00 AM - + Field 1 Value 1 @@ -19643,26 +21074,34 @@ exports[`Storyshots Message list 1`] = ` > Field 2 Value 2 @@ -19683,26 +21122,34 @@ exports[`Storyshots Message list 1`] = ` > Field 3 Value 3 @@ -19723,26 +21170,34 @@ exports[`Storyshots Message list 1`] = ` > Field 4 Value 4 @@ -19763,26 +21218,34 @@ exports[`Storyshots Message list 1`] = ` > Field 5 Value 5 @@ -19805,6 +21268,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -19952,14 +21419,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -19967,15 +21438,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -19984,6 +21459,14 @@ exports[`Storyshots Message list 1`] = ` rocket.cat 10:00 AM - + Field 1 Value 1 @@ -20180,26 +21686,34 @@ exports[`Storyshots Message list 1`] = ` > Field 2 Value 2 @@ -20222,7 +21736,7 @@ exports[`Storyshots Message list 1`] = ` style={ Object { "alignItems": "center", - "alignSelf": "flex-end", + "alignSelf": "flex-start", "backgroundColor": "#f3f4f5", "borderColor": "#e1e5e8", "borderRadius": 4, @@ -20255,40 +21769,56 @@ exports[`Storyshots Message list 1`] = ` > rocket.cat 10:00 AM - + Field 1 Value 1 @@ -20374,26 +21912,34 @@ exports[`Storyshots Message list 1`] = ` > Field 2 Value 2 @@ -20416,6 +21962,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -20563,14 +22113,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -20578,15 +22132,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -20595,6 +22153,14 @@ exports[`Storyshots Message list 1`] = ` Reply @@ -20718,6 +22286,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -20865,14 +22437,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -20880,15 +22456,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -20897,6 +22477,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -21086,15 +22681,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -21151,6 +22750,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -21253,15 +22863,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -21318,6 +22932,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -21523,15 +23152,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -21546,6 +23179,14 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -21752,15 +23400,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -21769,6 +23421,14 @@ exports[`Storyshots Message list 1`] = ` Message removed @@ -21972,6 +23639,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -22105,14 +23776,18 @@ exports[`Storyshots Message list 1`] = ` > Has joined the channel @@ -22131,6 +23806,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -22264,14 +23943,18 @@ exports[`Storyshots Message list 1`] = ` > Room name changed to: New name by diego.mello @@ -22290,6 +23973,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -22423,14 +24110,18 @@ exports[`Storyshots Message list 1`] = ` > Message pinned @@ -22449,6 +24140,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -22582,14 +24277,18 @@ exports[`Storyshots Message list 1`] = ` > Has left the channel @@ -22608,6 +24307,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -22741,14 +24444,18 @@ exports[`Storyshots Message list 1`] = ` > User rocket.cat removed by diego.mello @@ -22767,6 +24474,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -22900,14 +24611,18 @@ exports[`Storyshots Message list 1`] = ` > User rocket.cat added by diego.mello @@ -22926,6 +24641,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -23059,14 +24778,18 @@ exports[`Storyshots Message list 1`] = ` > User rocket.cat muted by diego.mello @@ -23085,6 +24808,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -23218,14 +24945,18 @@ exports[`Storyshots Message list 1`] = ` > User rocket.cat unmuted by diego.mello @@ -23244,6 +24975,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -23377,14 +25112,18 @@ exports[`Storyshots Message list 1`] = ` > rocket.cat was set admin by diego.mello @@ -23403,6 +25142,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -23536,14 +25279,18 @@ exports[`Storyshots Message list 1`] = ` > rocket.cat is no longer admin by diego.mello @@ -23562,6 +25309,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -23695,14 +25446,18 @@ exports[`Storyshots Message list 1`] = ` > Room description changed to: new description by diego.mello @@ -23721,6 +25476,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -23854,14 +25613,18 @@ exports[`Storyshots Message list 1`] = ` > Room announcement changed to: new announcement by diego.mello @@ -23880,6 +25643,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -24013,14 +25780,18 @@ exports[`Storyshots Message list 1`] = ` > Room topic changed to: new topic by diego.mello @@ -24039,6 +25810,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -24172,14 +25947,18 @@ exports[`Storyshots Message list 1`] = ` > Room type changed to: public by diego.mello @@ -24198,6 +25977,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -24350,14 +26133,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -24365,15 +26152,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -24382,6 +26173,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -24588,15 +26394,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -24605,6 +26415,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -25129,15 +26943,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -25146,6 +26964,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -25495,15 +27363,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -25512,6 +27384,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -25981,15 +27856,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -25998,6 +27877,14 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -26191,15 +28086,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -26208,6 +28107,14 @@ exports[`Storyshots Message list 1`] = ` code @@ -26255,7 +28165,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "backgroundColor": "transparent", - "color": "#2F343D", "fontFamily": "System", "fontSize": 16, "fontWeight": "400", @@ -26278,13 +28187,17 @@ exports[`Storyshots Message list 1`] = ` back-ticks around @@ -26295,7 +28208,6 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "backgroundColor": "transparent", - "color": "#2F343D", "fontFamily": "System", "fontSize": 16, "fontWeight": "400", @@ -26319,15 +28231,18 @@ exports[`Storyshots Message list 1`] = ` Code block @@ -26348,6 +28263,10 @@ exports[`Storyshots Message list 1`] = ` "marginLeft": 10, "marginTop": 30, }, + Object { + "backgroundColor": "#efeff4", + "color": "#0d0e12", + }, Object { "marginBottom": 0, "marginTop": 30, @@ -26495,14 +28414,18 @@ exports[`Storyshots Message list 1`] = ` diego.mello @@ -26510,15 +28433,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -26535,12 +28462,16 @@ exports[`Storyshots Message list 1`] = ` > diego.mello @@ -26761,15 +28707,19 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM @@ -26805,10 +28755,10 @@ exports[`Storyshots Message list 1`] = ` Array [ Object { "borderBottomWidth": 1, - "borderColor": "#e1e5e8", "borderRightWidth": 1, }, Object { + "borderColor": "#e1e5e8", "maxHeight": 300, "maxWidth": 200, }, @@ -26820,10 +28770,12 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "borderColor": "#e1e5e8", "borderLeftWidth": 1, "borderTopWidth": 1, }, + Object { + "borderColor": "#e1e5e8", + }, ] } > @@ -26834,9 +28786,11 @@ exports[`Storyshots Message list 1`] = ` "flexDirection": "row", }, Object { - "borderBottomWidth": 1, "borderColor": "#e1e5e8", }, + Object { + "borderBottomWidth": 1, + }, ] } > @@ -26844,11 +28798,13 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "borderColor": "#e1e5e8", "justifyContent": "flex-start", "paddingHorizontal": 13, "paddingVertical": 6, }, + Object { + "borderColor": "#e1e5e8", + }, Object { "borderRightWidth": 1, }, @@ -26859,7 +28815,14 @@ exports[`Storyshots Message list 1`] = ` } > @@ -26953,11 +28923,13 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "borderColor": "#e1e5e8", "justifyContent": "flex-start", "paddingHorizontal": 13, "paddingVertical": 6, }, + Object { + "borderColor": "#e1e5e8", + }, Object { "borderRightWidth": 1, }, @@ -26968,7 +28940,14 @@ exports[`Storyshots Message list 1`] = ` } > @@ -27058,11 +29045,13 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "borderColor": "#e1e5e8", "justifyContent": "flex-start", "paddingHorizontal": 13, "paddingVertical": 6, }, + Object { + "borderColor": "#e1e5e8", + }, Object { "borderRightWidth": 1, }, @@ -27073,7 +29062,14 @@ exports[`Storyshots Message list 1`] = ` } > Click to see full table @@ -27179,7 +29184,13 @@ exports[`Storyshots Message list 1`] = ` `; exports[`Storyshots RoomItem list 1`] = ` - + - + \ No newline at end of file diff --git a/android/app/src/debug/res/xml/react_native_config.xml b/android/app/src/debug/res/xml/react_native_config.xml deleted file mode 100644 index ba4b23070..000000000 --- a/android/app/src/debug/res/xml/react_native_config.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - localhost - 10.0.2.2 - 10.0.3.2 - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cf2faf448..e4e2ec403 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network_security_config" > - + \ No newline at end of file diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..bb6ab93df --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 543070623..ced5b25ef 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [ 'CLOSE_SEARCH_HEADER' ]); export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']); -export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']); +export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const SELECTED_USERS = createRequestTypes('SELECTED_USERS', ['ADD_USER', 'REMOVE_USER', 'RESET', 'SET_LOADING']); diff --git a/app/actions/index.js b/app/actions/index.js index ca322276a..494cb1c93 100644 --- a/app/actions/index.js +++ b/app/actions/index.js @@ -20,6 +20,12 @@ export function appInit() { }; } +export function appInitLocalSettings() { + return { + type: APP.INIT_LOCAL_SETTINGS + }; +} + export function setCurrentServer(server) { return { type: types.SET_CURRENT_SERVER, diff --git a/app/actions/login.js b/app/actions/login.js index c406d8e89..455c4fa14 100644 --- a/app/actions/login.js +++ b/app/actions/login.js @@ -1,9 +1,10 @@ import * as types from './actionsTypes'; -export function loginRequest(credentials) { +export function loginRequest(credentials, logoutOnError) { return { type: types.LOGIN.REQUEST, - credentials + credentials, + logoutOnError }; } diff --git a/app/commands.js b/app/commands.js new file mode 100644 index 000000000..5c0ffd599 --- /dev/null +++ b/app/commands.js @@ -0,0 +1,187 @@ +/* eslint-disable no-bitwise */ +import { constants } from 'react-native-keycommands'; + +import I18n from './i18n'; + +const KEY_TYPING = '\t'; +const KEY_PREFERENCES = 'p'; +const KEY_SEARCH = 'f'; +const KEY_PREVIOUS_ROOM = '['; +const KEY_NEXT_ROOM = ']'; +const KEY_NEW_ROOM = __DEV__ ? 'e' : 'n'; +const KEY_ROOM_ACTIONS = __DEV__ ? 'b' : 'i'; +const KEY_UPLOAD = 'u'; +const KEY_REPLY = ';'; +const KEY_SERVER_SELECTION = __DEV__ ? 'o' : '`'; +const KEY_ADD_SERVER = __DEV__ ? 'l' : 'n'; +const KEY_SEND_MESSAGE = '\r'; +const KEY_SELECT = '123456789'; + +export const defaultCommands = [ + { + // Focus messageBox + input: KEY_TYPING, + modifierFlags: 0, + discoverabilityTitle: I18n.t('Type_message') + }, + { + // Send message on textInput to current room + input: KEY_SEND_MESSAGE, + modifierFlags: 0, + discoverabilityTitle: I18n.t('Send') + } +]; + +export const keyCommands = [ + { + // Open Preferences Modal + input: KEY_PREFERENCES, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Preferences') + }, + { + // Focus Room Search + input: KEY_SEARCH, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Room_search') + }, + { + // Select a room by order using 1-9 + input: '1...9', + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Room_selection') + }, + { + // Change room to next on Rooms List + input: KEY_NEXT_ROOM, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Next_room') + }, + { + // Change room to previous on Rooms List + input: KEY_PREVIOUS_ROOM, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Previous_room') + }, + { + // Open New Room Modal + input: KEY_NEW_ROOM, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('New_room') + }, + { + // Open Room Actions + input: KEY_ROOM_ACTIONS, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Room_actions') + }, + { + // Upload a file to room + input: KEY_UPLOAD, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Upload_room') + }, + { + // Search Messages on current room + input: KEY_SEARCH, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Search_messages') + }, + { + // Scroll messages on current room + input: '↑ ↓', + modifierFlags: constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Scroll_messages') + }, + { + // Scroll up messages on current room + input: constants.keyInputUpArrow, + modifierFlags: constants.keyModifierAlternate + }, + { + // Scroll down messages on current room + input: constants.keyInputDownArrow, + modifierFlags: constants.keyModifierAlternate + }, + { + // Reply latest message with Quote + input: KEY_REPLY, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Reply_latest') + }, + { + // Open server dropdown + input: KEY_SERVER_SELECTION, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Server_selection') + }, + { + // Select a server by order using 1-9 + input: '1...9', + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Server_selection_numbers') + }, + { + // Navigate to add new server + input: KEY_ADD_SERVER, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Add_server') + }, + // Refers to select rooms on list + ...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ + input: `${ value }`, + modifierFlags: constants.keyModifierCommand + }))), + // Refers to select servers on list + ...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ + input: `${ value }`, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate + }))) +]; + +export const KEY_COMMAND = 'KEY_COMMAND'; + +export const commandHandle = (event, key, flags = []) => { + const { input, modifierFlags } = event; + let _flags = 0; + if (flags.includes('command') && flags.includes('alternate')) { + _flags = constants.keyModifierCommand | constants.keyModifierAlternate; + } else if (flags.includes('command')) { + _flags = constants.keyModifierCommand; + } else if (flags.includes('alternate')) { + _flags = constants.keyModifierAlternate; + } + return key.includes(input) && modifierFlags === _flags; +}; + +export const handleCommandTyping = event => commandHandle(event, KEY_TYPING); + +export const handleCommandSubmit = event => commandHandle(event, KEY_SEND_MESSAGE); + +export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']); + +export const handleCommandScroll = event => commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']); + +export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']); + +export const handleCommandSearchMessages = event => commandHandle(event, KEY_SEARCH, ['command']); + +export const handleCommandReplyLatest = event => commandHandle(event, KEY_REPLY, ['command']); + +export const handleCommandSelectServer = event => commandHandle(event, KEY_SELECT, ['command', 'alternate']); + +export const handleCommandShowPreferences = event => commandHandle(event, KEY_PREFERENCES, ['command']); + +export const handleCommandSearching = event => commandHandle(event, KEY_SEARCH, ['command', 'alternate']); + +export const handleCommandSelectRoom = event => commandHandle(event, KEY_SELECT, ['command']); + +export const handleCommandPreviousRoom = event => commandHandle(event, KEY_PREVIOUS_ROOM, ['command']); + +export const handleCommandNextRoom = event => commandHandle(event, KEY_NEXT_ROOM, ['command']); + +export const handleCommandShowNewMessage = event => commandHandle(event, KEY_NEW_ROOM, ['command']); + +export const handleCommandAddNewServer = event => commandHandle(event, KEY_ADD_SERVER, ['command', 'alternate']); + +export const handleCommandOpenServerDropdown = event => commandHandle(event, KEY_SERVER_SELECTION, ['command', 'alternate']); diff --git a/app/constants/colors.js b/app/constants/colors.js index b65cbc22a..d38ac2cbf 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -1,32 +1,121 @@ import { isIOS, isAndroid } from '../utils/deviceInfo'; -export const COLOR_DANGER = '#f5455c'; -export const COLOR_SUCCESS = '#2de0a5'; -export const COLOR_PRIMARY = '#1d74f5'; -export const COLOR_WHITE = '#fff'; -export const COLOR_BUTTON_PRIMARY = COLOR_PRIMARY; -export const COLOR_TITLE = '#0C0D0F'; -export const COLOR_TEXT = '#2F343D'; -export const COLOR_TEXT_DESCRIPTION = '#9ca2a8'; -export const COLOR_SEPARATOR = '#A7A7AA'; -export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5'; -export const COLOR_BACKGROUND_NOTIFICATION = '#f8f8f8'; -export const COLOR_BORDER = '#e1e5e8'; -export const COLOR_UNREAD = '#e1e5e8'; -export const COLOR_TOAST = '#0C0D0F'; export const STATUS_COLORS = { online: '#2de0a5', - busy: COLOR_DANGER, + busy: '#f5455c', away: '#ffd21f', offline: '#cbced1' }; -export const HEADER_BACKGROUND = isIOS ? '#f8f8f8' : '#2F343D'; -export const HEADER_TITLE = isIOS ? COLOR_TITLE : COLOR_WHITE; -export const HEADER_BACK = isIOS ? COLOR_PRIMARY : COLOR_WHITE; -export const HEADER_TINT = isIOS ? COLOR_PRIMARY : COLOR_WHITE; - export const SWITCH_TRACK_COLOR = { - false: isAndroid ? COLOR_DANGER : null, - true: COLOR_SUCCESS + false: isAndroid ? '#f5455c' : null, + true: '#2de0a5' +}; + +export const themes = { + light: { + backgroundColor: '#ffffff', + focusedBackground: '#ffffff', + chatComponentBackground: '#f3f4f5', + auxiliaryBackground: '#efeff4', + bannerBackground: '#f1f2f4', + titleText: '#0d0e12', + bodyText: '#2f343d', + backdropColor: '#000000', + dangerColor: '#f5455c', + successColor: '#2de0a5', + borderColor: '#e1e5e8', + controlText: '#54585e', + auxiliaryText: '#9ca2a8', + infoText: '#6d6d72', + tintColor: '#1d74f5', + auxiliaryTintColor: '#caced1', + actionTintColor: '#1d74f5', + separatorColor: '#cbcbcc', + navbarBackground: '#ffffff', + headerBorder: '#B2B2B2', + headerBackground: isIOS ? '#f8f8f8' : '#2f343d', + headerSecondaryBackground: '#ffffff', + headerTintColor: isAndroid ? '#ffffff' : '#1d74f5', + headerTitleColor: isAndroid ? '#ffffff' : '#0d0e12', + headerSecondaryText: isAndroid ? '#9ca2a8' : '#1d74f5', + toastBackground: '#0C0D0F', + videoBackground: '#1f2329', + favoriteBackground: '#ffbb00', + hideBackground: '#54585e', + messageboxBackground: '#ffffff', + searchboxBackground: '#E6E6E7', + buttonBackground: '#414852', + buttonText: '#ffffff' + }, + dark: { + backgroundColor: '#030b1b', + focusedBackground: '#0b182c', + chatComponentBackground: '#192132', + auxiliaryBackground: '#07101e', + bannerBackground: '#0e1f38', + titleText: '#FFFFFF', + bodyText: '#e8ebed', + backdropColor: '#000000', + dangerColor: '#f5455c', + successColor: '#2de0a5', + borderColor: '#0f213d', + controlText: '#dadde6', + auxiliaryText: '#9297a2', + infoText: '#6D6D72', + tintColor: '#1d74f5', + auxiliaryTintColor: '#cdcdcd', + actionTintColor: '#1d74f5', + separatorColor: '#2b2b2d', + navbarBackground: '#0b182c', + headerBorder: '#2F3A4B', + headerBackground: '#0b182c', + headerSecondaryBackground: '#0b182c', + headerTintColor: isAndroid ? '#ffffff' : '#1d74f5', + headerTitleColor: '#FFFFFF', + headerSecondaryText: isAndroid ? '#9297a2' : '#1d74f5', + toastBackground: '#0C0D0F', + videoBackground: '#1f2329', + favoriteBackground: '#ffbb00', + hideBackground: '#54585e', + messageboxBackground: '#0b182c', + searchboxBackground: '#192d4d', + buttonBackground: '#414852', + buttonText: '#ffffff' + }, + black: { + backgroundColor: '#000000', + focusedBackground: '#0d0d0d', + chatComponentBackground: '#16181a', + auxiliaryBackground: '#080808', + bannerBackground: '#1f2329', + titleText: '#f9f9f9', + bodyText: '#e8ebed', + backdropColor: '#000000', + dangerColor: '#f5455c', + successColor: '#2de0a5', + borderColor: '#1f2329', + controlText: '#dadde6', + auxiliaryText: '#b2b8c6', + infoText: '#6d6d72', + tintColor: '#1e9bfe', + auxiliaryTintColor: '#cdcdcd', + actionTintColor: '#1ea1fe', + separatorColor: '#272728', + navbarBackground: '#0d0d0d', + headerBorder: '#323232', + headerBackground: '#0d0d0d', + headerSecondaryBackground: '#0d0d0d', + headerTintColor: isAndroid ? '#ffffff' : '#1e9bfe', + headerTitleColor: '#f9f9f9', + headerSecondaryText: isAndroid ? '#b2b8c6' : '#1e9bfe', + toastBackground: '#0C0D0F', + videoBackground: '#1f2329', + favoriteBackground: '#ffbb00', + hideBackground: '#54585e', + messageboxBackground: '#0d0d0d', + searchboxBackground: '#1f1f1f', + buttonBackground: '#414852', + buttonText: '#ffffff' + } }; diff --git a/app/constants/tablet.js b/app/constants/tablet.js new file mode 100644 index 000000000..16e62f6d0 --- /dev/null +++ b/app/constants/tablet.js @@ -0,0 +1,4 @@ +export const MAX_SIDEBAR_WIDTH = 321; +export const MAX_CONTENT_WIDTH = '90%'; +export const MAX_SCREEN_CONTENT_WIDTH = '45%'; +export const MIN_WIDTH_SPLIT_LAYOUT = 700; diff --git a/app/containers/ActivityIndicator.js b/app/containers/ActivityIndicator.js index f4fa47d4e..964d9c267 100644 --- a/app/containers/ActivityIndicator.js +++ b/app/containers/ActivityIndicator.js @@ -1,12 +1,40 @@ import React from 'react'; import { ActivityIndicator, StyleSheet } from 'react-native'; +import { PropTypes } from 'prop-types'; +import { themes } from '../constants/colors'; const styles = StyleSheet.create({ indicator: { - padding: 10 + padding: 16, + flex: 1 + }, + absolute: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + alignItems: 'center', + justifyContent: 'center' } }); -const RCActivityIndicator = () => ; +const RCActivityIndicator = ({ theme, absolute, ...props }) => ( + +); + +RCActivityIndicator.propTypes = { + theme: PropTypes.string, + absolute: PropTypes.bool, + props: PropTypes.object +}; + +RCActivityIndicator.defaultProps = { + theme: 'light' +}; export default RCActivityIndicator; diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js index c616788cf..39d488efa 100644 --- a/app/containers/Avatar.js +++ b/app/containers/Avatar.js @@ -2,13 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { View } from 'react-native'; import FastImage from 'react-native-fast-image'; +import Touch from '../utils/touch'; const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => ( `${ baseUrl }${ url }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }` ); const Avatar = React.memo(({ - text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token + text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme }) => { const avatarStyle = { width: size, @@ -39,7 +40,7 @@ const Avatar = React.memo(({ } - const image = ( + let image = ( ); + if (onPress) { + image = ( + + {image} + + ); + } + return ( {image} @@ -67,7 +76,9 @@ Avatar.propTypes = { type: PropTypes.string, children: PropTypes.object, userId: PropTypes.string, - token: PropTypes.string + token: PropTypes.string, + theme: PropTypes.string, + onPress: PropTypes.func }; Avatar.defaultProps = { diff --git a/app/containers/Button/index.js b/app/containers/Button/index.js index f7d610aa2..c90ced834 100644 --- a/app/containers/Button/index.js +++ b/app/containers/Button/index.js @@ -1,18 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, Text, ActivityIndicator } from 'react-native'; +import { StyleSheet, Text } from 'react-native'; import { RectButton } from 'react-native-gesture-handler'; -import { COLOR_BUTTON_PRIMARY } from '../../constants/colors'; +import { themes } from '../../constants/colors'; import sharedStyles from '../../views/Styles'; - -const colors = { - background_primary: COLOR_BUTTON_PRIMARY, - background_secondary: 'white', - - text_color_primary: 'white', - text_color_secondary: COLOR_BUTTON_PRIMARY -}; +import ActivityIndicator from '../ActivityIndicator'; /* eslint-disable react-native/no-unused-styles */ const styles = StyleSheet.create({ @@ -26,23 +19,6 @@ const styles = StyleSheet.create({ text: { fontSize: 18, textAlign: 'center' - }, - background_primary: { - backgroundColor: colors.background_primary - }, - background_secondary: { - backgroundColor: colors.background_secondary - }, - text_primary: { - ...sharedStyles.textMedium, - color: colors.text_color_primary - }, - text_secondary: { - ...sharedStyles.textBold, - color: colors.text_color_secondary - }, - disabled: { - backgroundColor: '#e1e5e8' } }); @@ -54,6 +30,7 @@ export default class Button extends React.PureComponent { disabled: PropTypes.bool, backgroundColor: PropTypes.string, loading: PropTypes.bool, + theme: PropTypes.string, style: PropTypes.any } @@ -67,24 +44,37 @@ export default class Button extends React.PureComponent { render() { const { - title, type, onPress, disabled, backgroundColor, loading, style, ...otherProps + title, type, onPress, disabled, backgroundColor, loading, style, theme, ...otherProps } = this.props; + const isPrimary = type === 'primary'; return ( { loading - ? - : {title} + ? + : ( + + {title} + + ) } ); diff --git a/app/containers/Check.js b/app/containers/Check.js index 30c9cbd31..e3f4f5147 100644 --- a/app/containers/Check.js +++ b/app/containers/Check.js @@ -1,18 +1,22 @@ import React from 'react'; import { StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; import { CustomIcon } from '../lib/Icons'; -import sharedStyles from '../views/Styles'; +import { themes } from '../constants/colors'; const styles = StyleSheet.create({ icon: { width: 22, height: 22, - marginHorizontal: 15, - ...sharedStyles.textColorDescription + marginHorizontal: 15 } }); -const Check = React.memo(() => ); +const Check = React.memo(({ theme }) => ); + +Check.propTypes = { + theme: PropTypes.string +}; export default Check; diff --git a/app/containers/DisclosureIndicator.js b/app/containers/DisclosureIndicator.js index 25a284baf..eb6588e1f 100644 --- a/app/containers/DisclosureIndicator.js +++ b/app/containers/DisclosureIndicator.js @@ -1,5 +1,8 @@ import React from 'react'; import { View, Image, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; + +import { themes } from '../constants/colors'; const styles = StyleSheet.create({ disclosureContainer: { @@ -14,12 +17,23 @@ const styles = StyleSheet.create({ } }); -export const DisclosureImage = React.memo(() => ); +export const DisclosureImage = React.memo(({ theme }) => ( + +)); +DisclosureImage.propTypes = { + theme: PropTypes.string +}; -const DisclosureIndicator = React.memo(() => ( +const DisclosureIndicator = React.memo(({ theme }) => ( - + )); +DisclosureIndicator.propTypes = { + theme: PropTypes.string +}; export default DisclosureIndicator; diff --git a/app/containers/EmojiPicker/EmojiCategory.js b/app/containers/EmojiPicker/EmojiCategory.js index e5b572d97..e7a71f513 100644 --- a/app/containers/EmojiPicker/EmojiCategory.js +++ b/app/containers/EmojiPicker/EmojiCategory.js @@ -1,20 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, TouchableOpacity } from 'react-native'; +import { Text, TouchableOpacity, FlatList } from 'react-native'; import { shortnameToUnicode } from 'emoji-toolkit'; import { responsive } from 'react-native-responsive-ui'; -import { OptimizedFlatList } from 'react-native-optimized-flatlist'; import styles from './styles'; import CustomEmoji from './CustomEmoji'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import { isIOS } from '../../utils/deviceInfo'; -const EMOJIS_PER_ROW = isIOS ? 8 : 9; +const EMOJI_SIZE = 50; const renderEmoji = (emoji, size, baseUrl) => { - if (emoji.isCustom) { - return ; + if (emoji && emoji.isCustom) { + return ; } return ( @@ -33,44 +31,41 @@ class EmojiCategory extends React.Component { width: PropTypes.number } - constructor(props) { - super(props); - const { window, width, emojisPerRow } = this.props; - const { width: widthWidth, height: windowHeight } = window; - - this.size = Math.min(width || widthWidth, windowHeight) / (emojisPerRow || EMOJIS_PER_ROW); - this.emojis = props.emojis; - } - - shouldComponentUpdate() { - return false; - } - - renderItem(emoji, size) { + renderItem(emoji) { const { baseUrl, onEmojiSelected } = this.props; return ( onEmojiSelected(emoji)} - testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`} + testID={`reaction-picker-${ emoji && emoji.isCustom ? emoji.content : emoji }`} > - {renderEmoji(emoji, size, baseUrl)} + {renderEmoji(emoji, EMOJI_SIZE, baseUrl)} ); } render() { - const { emojis } = this.props; + const { emojis, width } = this.props; + + if (!width) { + return null; + } + + const numColumns = Math.trunc(width / EMOJI_SIZE); + const marginHorizontal = (width - (numColumns * EMOJI_SIZE)) / 2; return ( - (item.isCustom && item.content) || item} + (item && item.isCustom && item.content) || item} data={emojis} - renderItem={({ item }) => this.renderItem(item, this.size)} - numColumns={EMOJIS_PER_ROW} + extraData={this.props} + renderItem={({ item }) => this.renderItem(item)} + numColumns={numColumns} initialNumToRender={45} - getItemLayout={(data, index) => ({ length: this.size, offset: this.size * index, index })} removeClippedSubviews {...scrollPersistTaps} /> diff --git a/app/containers/EmojiPicker/TabBar.js b/app/containers/EmojiPicker/TabBar.js index c298484ba..834f587a5 100644 --- a/app/containers/EmojiPicker/TabBar.js +++ b/app/containers/EmojiPicker/TabBar.js @@ -2,26 +2,31 @@ import React from 'react'; import PropTypes from 'prop-types'; import { View, TouchableOpacity, Text } from 'react-native'; import styles from './styles'; +import { themes } from '../../constants/colors'; export default class TabBar extends React.Component { static propTypes = { goToPage: PropTypes.func, activeTab: PropTypes.number, tabs: PropTypes.array, - tabEmojiStyle: PropTypes.object + tabEmojiStyle: PropTypes.object, + theme: PropTypes.string } shouldComponentUpdate(nextProps) { - const { activeTab } = this.props; + const { activeTab, theme } = this.props; if (nextProps.activeTab !== activeTab) { return true; } + if (nextProps.theme !== theme) { + return true; + } return false; } render() { const { - tabs, goToPage, tabEmojiStyle, activeTab + tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props; return ( @@ -35,7 +40,7 @@ export default class TabBar extends React.Component { testID={`reaction-picker-${ tab }`} > {tab} - {activeTab === i ? : } + {activeTab === i ? : } ))} diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 1b0a7c172..817b0f602 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; +import { View } from 'react-native'; import PropTypes from 'prop-types'; -import { ScrollView } from 'react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import { shortnameToUnicode } from 'emoji-toolkit'; import equal from 'deep-equal'; @@ -16,6 +16,8 @@ import database from '../../lib/database'; import { emojisByCategory } from '../../emojis'; import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import log from '../../utils/log'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; const scrollProps = { keyboardShouldPersistTaps: 'always', @@ -28,8 +30,7 @@ class EmojiPicker extends Component { customEmojis: PropTypes.object, onEmojiSelected: PropTypes.func, tabEmojiStyle: PropTypes.object, - emojisPerRow: PropTypes.number, - width: PropTypes.number + theme: PropTypes.string }; constructor(props) { @@ -44,7 +45,8 @@ class EmojiPicker extends Component { this.state = { frequentlyUsed: [], customEmojis, - show: false + show: false, + width: null }; } @@ -54,12 +56,15 @@ class EmojiPicker extends Component { } shouldComponentUpdate(nextProps, nextState) { - const { frequentlyUsed, show } = this.state; - const { width } = this.props; + const { frequentlyUsed, show, width } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.show !== show) { return true; } - if (nextProps.width !== width) { + if (nextState.width !== width) { return true; } if (!equal(nextState.frequentlyUsed, frequentlyUsed)) { @@ -91,22 +96,24 @@ class EmojiPicker extends Component { _addFrequentlyUsed = protectedFunction(async(emoji) => { const db = database.active; const freqEmojiCollection = db.collections.get('frequently_used_emojis'); + let freqEmojiRecord; + try { + freqEmojiRecord = await freqEmojiCollection.find(emoji.content); + } catch (error) { + // Do nothing + } + await db.action(async() => { - try { - const freqEmojiRecord = await freqEmojiCollection.find(emoji.content); + if (freqEmojiRecord) { await freqEmojiRecord.update((f) => { f.count += 1; }); - } catch (error) { - try { - await freqEmojiCollection.create((f) => { - f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema); - Object.assign(f, emoji); - f.count = 1; - }); - } catch (e) { - // Do nothing - } + } else { + await freqEmojiCollection.create((f) => { + f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema); + Object.assign(f, emoji); + f.count = 1; + }); } }); }) @@ -124,11 +131,11 @@ class EmojiPicker extends Component { this.setState({ frequentlyUsed }); } - renderCategory(category, i) { - const { frequentlyUsed, customEmojis } = this.state; - const { - emojisPerRow, width, baseUrl - } = this.props; + onLayout = ({ nativeEvent: { layout: { width } } }) => this.setState({ width }); + + renderCategory(category, i, label) { + const { frequentlyUsed, customEmojis, width } = this.state; + const { baseUrl } = this.props; let emojis = []; if (i === 0) { @@ -143,41 +150,36 @@ class EmojiPicker extends Component { emojis={emojis} onEmojiSelected={emoji => this.onEmojiSelected(emoji)} style={styles.categoryContainer} - size={emojisPerRow} width={width} baseUrl={baseUrl} + tabLabel={label} /> ); } render() { const { show, frequentlyUsed } = this.state; - const { tabEmojiStyle } = this.props; + const { tabEmojiStyle, theme } = this.props; if (!show) { return null; } return ( - } - contentProps={scrollProps} - style={styles.background} - > - { - categories.tabs.map((tab, i) => ( - (i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab - : ( - - {this.renderCategory(tab.category, i)} - - ))) - } - + + } + contentProps={scrollProps} + style={{ backgroundColor: themes[theme].focusedBackground }} + > + { + categories.tabs.map((tab, i) => ( + (i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab + : ( + this.renderCategory(tab.category, i, tab.tabLabel) + ))) + } + + ); } } @@ -186,4 +188,4 @@ const mapStateToProps = state => ({ customEmojis: state.customEmojis }); -export default connect(mapStateToProps)(EmojiPicker); +export default connect(mapStateToProps)(withTheme(EmojiPicker)); diff --git a/app/containers/EmojiPicker/styles.js b/app/containers/EmojiPicker/styles.js index d4e993f50..f7fdbfdef 100644 --- a/app/containers/EmojiPicker/styles.js +++ b/app/containers/EmojiPicker/styles.js @@ -1,10 +1,6 @@ import { StyleSheet } from 'react-native'; -import { COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors'; export default StyleSheet.create({ - background: { - backgroundColor: COLOR_WHITE - }, container: { flex: 1 }, @@ -28,7 +24,6 @@ export default StyleSheet.create({ left: 0, right: 0, height: 2, - backgroundColor: COLOR_PRIMARY, bottom: 0 }, tabLine: { @@ -51,11 +46,10 @@ export default StyleSheet.create({ flex: 1 }, categoryEmoji: { - color: 'black', backgroundColor: 'transparent', textAlign: 'center' }, customCategoryEmoji: { - margin: 4 + margin: 8 } }); diff --git a/app/containers/FileModal.js b/app/containers/FileModal.js index bf7e53ec9..2943afa4f 100644 --- a/app/containers/FileModal.js +++ b/app/containers/FileModal.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { - View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView + View, Text, TouchableWithoutFeedback, StyleSheet, SafeAreaView } from 'react-native'; import FastImage from 'react-native-fast-image'; import PropTypes from 'prop-types'; @@ -9,8 +9,10 @@ import ImageViewer from 'react-native-image-zoom-viewer'; import { Video } from 'expo-av'; import sharedStyles from '../views/Styles'; -import { COLOR_WHITE } from '../constants/colors'; import { formatAttachmentUrl } from '../lib/utils'; +import ActivityIndicator from './ActivityIndicator'; +import { themes } from '../constants/colors'; +import { withTheme } from '../theme'; const styles = StyleSheet.create({ safeArea: { @@ -25,40 +27,22 @@ const styles = StyleSheet.create({ marginVertical: 10 }, title: { - color: COLOR_WHITE, textAlign: 'center', fontSize: 16, ...sharedStyles.textSemibold }, description: { - color: COLOR_WHITE, textAlign: 'center', fontSize: 14, ...sharedStyles.textMedium }, - indicator: { - flex: 1 - }, video: { flex: 1 - }, - loading: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - alignItems: 'center', - justifyContent: 'center' } }); -const Indicator = React.memo(() => ( - -)); - const ModalContent = React.memo(({ - attachment, onClose, user, baseUrl + attachment, onClose, user, baseUrl, theme }) => { if (attachment && attachment.image_url) { const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl); @@ -66,8 +50,8 @@ const ModalContent = React.memo(({ - {attachment.title} - {attachment.description ? {attachment.description} : null} + {attachment.title} + {attachment.description ? {attachment.description} : null} null} renderImage={props => } - loadingRender={() => } + loadingRender={() => } /> ); @@ -102,7 +86,7 @@ const ModalContent = React.memo(({ onLoadStart={() => setLoading(true)} onError={console.log} /> - { loading ? : null } + { loading ? : null } ); } @@ -110,7 +94,7 @@ const ModalContent = React.memo(({ }); const FileModal = React.memo(({ - isVisible, onClose, attachment, user, baseUrl + isVisible, onClose, attachment, user, baseUrl, theme }) => ( - + -), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible && prevProps.loading === nextProps.loading); +), (prevProps, nextProps) => ( + prevProps.isVisible === nextProps.isVisible && prevProps.loading === nextProps.loading && prevProps.theme === nextProps.theme +)); FileModal.propTypes = { isVisible: PropTypes.bool, attachment: PropTypes.object, user: PropTypes.object, baseUrl: PropTypes.string, + theme: PropTypes.string, onClose: PropTypes.func }; FileModal.displayName = 'FileModal'; @@ -137,8 +124,9 @@ ModalContent.propTypes = { attachment: PropTypes.object, user: PropTypes.object, baseUrl: PropTypes.string, + theme: PropTypes.string, onClose: PropTypes.func }; ModalContent.displayName = 'FileModalContent'; -export default FileModal; +export default withTheme(FileModal); diff --git a/app/containers/HeaderButton.js b/app/containers/HeaderButton.js index 7741cafa1..bb233c0a0 100644 --- a/app/containers/HeaderButton.js +++ b/app/containers/HeaderButton.js @@ -3,16 +3,25 @@ import PropTypes from 'prop-types'; import { HeaderButtons, HeaderButton, Item } from 'react-navigation-header-buttons'; import { CustomIcon } from '../lib/Icons'; -import { isIOS } from '../utils/deviceInfo'; -import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors'; +import { isIOS, isAndroid } from '../utils/deviceInfo'; +import { themes } from '../constants/colors'; import I18n from '../i18n'; +import { withTheme } from '../theme'; -const color = isIOS ? COLOR_PRIMARY : COLOR_WHITE; export const headerIconSize = 23; -const CustomHeaderButton = React.memo(props => ( - -)); +const CustomHeaderButton = React.memo(withTheme(({ theme, ...props }) => ( + +))); export const CustomHeaderButtons = React.memo(props => ( ( navigation.navigate('LegalView')} testID={testID} /> )); +CustomHeaderButton.propTypes = { + theme: PropTypes.string +}; DrawerButton.propTypes = { navigation: PropTypes.object.isRequired, testID: PropTypes.string.isRequired diff --git a/app/containers/ListItem.js b/app/containers/ListItem.js index 069329343..dcdaa6585 100644 --- a/app/containers/ListItem.js +++ b/app/containers/ListItem.js @@ -1,9 +1,9 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; -import { RectButton } from 'react-native-gesture-handler'; -import { COLOR_TEXT } from '../constants/colors'; +import Touch from '../utils/touch'; +import { themes } from '../constants/colors'; import sharedStyles from '../views/Styles'; const styles = StyleSheet.create({ @@ -12,7 +12,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - height: 56, + height: 46, paddingHorizontal: 15 }, disabled: { @@ -24,24 +24,22 @@ const styles = StyleSheet.create({ }, title: { fontSize: 16, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, subtitle: { fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular } }); const Content = React.memo(({ - title, subtitle, disabled, testID, right + title, subtitle, disabled, testID, right, color, theme }) => ( - {title} + {title} {subtitle - ? {subtitle} + ? {subtitle} : null } @@ -52,25 +50,30 @@ const Content = React.memo(({ const Button = React.memo(({ onPress, ...props }) => ( - - + )); const Item = React.memo(({ ...props }) => { if (props.onPress) { return ); } return ( - ); -}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file)); +}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme); ImageContainer.propTypes = { file: PropTypes.object, @@ -61,19 +66,24 @@ ImageContainer.propTypes = { user: PropTypes.object, useMarkdown: PropTypes.bool, onOpenFileModal: PropTypes.func, - getCustomEmoji: PropTypes.func + theme: PropTypes.string, + getCustomEmoji: PropTypes.func, + split: PropTypes.bool }; ImageContainer.displayName = 'MessageImageContainer'; Image.propTypes = { - img: PropTypes.string + img: PropTypes.string, + theme: PropTypes.string }; ImageContainer.displayName = 'MessageImage'; Button.propTypes = { children: PropTypes.node, - onPress: PropTypes.func + onPress: PropTypes.func, + theme: PropTypes.string, + split: PropTypes.bool }; ImageContainer.displayName = 'MessageButton'; -export default ImageContainer; +export default withSplit(ImageContainer); diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js index e26040d5a..dc2bb6b8e 100644 --- a/app/containers/message/Message.js +++ b/app/containers/message/Message.js @@ -5,7 +5,6 @@ import Touchable from 'react-native-platform-touchable'; import User from './User'; import styles from './styles'; -import sharedStyles from '../../views/Styles'; import RepliedThread from './RepliedThread'; import MessageAvatar from './MessageAvatar'; import Attachments from './Attachments'; @@ -52,11 +51,11 @@ MessageInner.displayName = 'MessageInner'; const Message = React.memo((props) => { if (props.isThreadReply || props.isThreadSequential || props.isInfo) { - const thread = props.isThreadReply ? : null; + const thread = props.isThreadReply ? : null; return ( {thread} - + { @@ -134,7 +134,8 @@ Message.propTypes = { onLongPress: PropTypes.func, onPress: PropTypes.func, isReadReceiptEnabled: PropTypes.bool, - unread: PropTypes.bool + unread: PropTypes.bool, + theme: PropTypes.string }; MessageInner.propTypes = { diff --git a/app/containers/message/MessageError.js b/app/containers/message/MessageError.js index 0392e6443..d30e40996 100644 --- a/app/containers/message/MessageError.js +++ b/app/containers/message/MessageError.js @@ -3,24 +3,25 @@ import Touchable from 'react-native-platform-touchable'; import PropTypes from 'prop-types'; import { CustomIcon } from '../../lib/Icons'; -import { COLOR_DANGER } from '../../constants/colors'; import styles from './styles'; import { BUTTON_HIT_SLOP } from './utils'; +import { themes } from '../../constants/colors'; -const MessageError = React.memo(({ hasError, onErrorPress }) => { +const MessageError = React.memo(({ hasError, onErrorPress, theme }) => { if (!hasError) { return null; } return ( - + ); -}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError); +}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError && prevProps.theme === nextProps.theme); MessageError.propTypes = { hasError: PropTypes.bool, - onErrorPress: PropTypes.func + onErrorPress: PropTypes.func, + theme: PropTypes.string }; MessageError.displayName = 'MessageError'; diff --git a/app/containers/message/Reactions.js b/app/containers/message/Reactions.js index e20dd0298..f6fab3457 100644 --- a/app/containers/message/Reactions.js +++ b/app/containers/message/Reactions.js @@ -7,24 +7,26 @@ import { CustomIcon } from '../../lib/Icons'; import styles from './styles'; import Emoji from './Emoji'; import { BUTTON_HIT_SLOP } from './utils'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; -const AddReaction = React.memo(({ reactionInit }) => ( +const AddReaction = React.memo(({ reactionInit, theme }) => ( - - + + )); const Reaction = React.memo(({ - reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji + reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji, theme }) => { const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1; return ( @@ -33,11 +35,11 @@ const Reaction = React.memo(({ onLongPress={onReactionLongPress} key={reaction.emoji} testID={`message-reaction-${ reaction.emoji }`} - style={[styles.reactionButton, reacted && styles.reactionButtonReacted]} - background={Touchable.Ripple('#fff')} + style={[styles.reactionButton, { backgroundColor: reacted ? themes[theme].bannerBackground : themes[theme].backgroundColor }]} + background={Touchable.Ripple(themes[theme].bannerBackground)} hitSlop={BUTTON_HIT_SLOP} > - + - { reaction.usernames.length } + { reaction.usernames.length } ); }); const Reactions = React.memo(({ - reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji + reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme }) => { if (!reactions || reactions.length === 0) { return null; @@ -68,9 +70,10 @@ const Reactions = React.memo(({ onReactionLongPress={onReactionLongPress} onReactionPress={onReactionPress} getCustomEmoji={getCustomEmoji} + theme={theme} /> ))} - + ); }); @@ -81,7 +84,8 @@ Reaction.propTypes = { baseUrl: PropTypes.string, onReactionPress: PropTypes.func, onReactionLongPress: PropTypes.func, - getCustomEmoji: PropTypes.func + getCustomEmoji: PropTypes.func, + theme: PropTypes.string }; Reaction.displayName = 'MessageReaction'; @@ -92,13 +96,15 @@ Reactions.propTypes = { onReactionPress: PropTypes.func, reactionInit: PropTypes.func, onReactionLongPress: PropTypes.func, - getCustomEmoji: PropTypes.func + getCustomEmoji: PropTypes.func, + theme: PropTypes.string }; Reactions.displayName = 'MessageReactions'; AddReaction.propTypes = { - reactionInit: PropTypes.func + reactionInit: PropTypes.func, + theme: PropTypes.string }; AddReaction.displayName = 'MessageAddReaction'; -export default Reactions; +export default withTheme(Reactions); diff --git a/app/containers/message/ReadReceipt.js b/app/containers/message/ReadReceipt.js index c407e021d..5ca392f6d 100644 --- a/app/containers/message/ReadReceipt.js +++ b/app/containers/message/ReadReceipt.js @@ -1,13 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { COLOR_PRIMARY } from '../../constants/colors'; +import { themes } from '../../constants/colors'; import { CustomIcon } from '../../lib/Icons'; import styles from './styles'; -const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }) => { +const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread, theme }) => { if (isReadReceiptEnabled && !unread && unread !== null) { - return ; + return ; } return null; }); @@ -15,7 +15,8 @@ ReadReceipt.displayName = 'MessageReadReceipt'; ReadReceipt.propTypes = { isReadReceiptEnabled: PropTypes.bool, - unread: PropTypes.bool + unread: PropTypes.bool, + theme: PropTypes.bool }; export default ReadReceipt; diff --git a/app/containers/message/RepliedThread.js b/app/containers/message/RepliedThread.js index 7a6f17b7f..25a572188 100644 --- a/app/containers/message/RepliedThread.js +++ b/app/containers/message/RepliedThread.js @@ -7,11 +7,12 @@ import PropTypes from 'prop-types'; import { CustomIcon } from '../../lib/Icons'; import DisclosureIndicator from '../DisclosureIndicator'; import styles from './styles'; +import { themes } from '../../constants/colors'; const RepliedThread = React.memo(({ - tmid, tmsg, isHeader, isTemp, fetchThreadName, id + tmid, tmsg, isHeader, fetchThreadName, id, theme }) => { - if (!tmid || !isHeader || isTemp) { + if (!tmid || !isHeader) { return null; } @@ -25,9 +26,9 @@ const RepliedThread = React.memo(({ return ( - - {msg} - + + {msg} + ); }, (prevProps, nextProps) => { @@ -40,7 +41,7 @@ const RepliedThread = React.memo(({ if (prevProps.isHeader !== nextProps.isHeader) { return false; } - if (prevProps.isTemp !== nextProps.isTemp) { + if (prevProps.theme !== nextProps.theme) { return false; } return true; @@ -51,7 +52,7 @@ RepliedThread.propTypes = { tmsg: PropTypes.string, id: PropTypes.string, isHeader: PropTypes.bool, - isTemp: PropTypes.bool, + theme: PropTypes.string, fetchThreadName: PropTypes.func }; RepliedThread.displayName = 'MessageRepliedThread'; diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js index 4b2af8e58..58bf14f81 100644 --- a/app/containers/message/Reply.js +++ b/app/containers/message/Reply.js @@ -8,7 +8,8 @@ import isEqual from 'deep-equal'; import Markdown from '../markdown'; import openLink from '../../utils/openLink'; import sharedStyles from '../../views/Styles'; -import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors'; +import { themes } from '../../constants/colors'; +import { withSplit } from '../../split'; const styles = StyleSheet.create({ button: { @@ -16,9 +17,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', marginTop: 6, - alignSelf: 'flex-end', - backgroundColor: COLOR_BACKGROUND_CONTAINER, - borderColor: COLOR_BORDER, + alignSelf: 'flex-start', borderWidth: 1, borderRadius: 4 }, @@ -36,13 +35,11 @@ const styles = StyleSheet.create({ author: { flex: 1, fontSize: 16, - ...sharedStyles.textColorNormal, ...sharedStyles.textMedium }, time: { fontSize: 12, marginLeft: 10, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular, fontWeight: '300' }, @@ -57,12 +54,10 @@ const styles = StyleSheet.create({ }, fieldTitle: { fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textSemibold }, fieldValue: { fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, marginTop: { @@ -70,21 +65,21 @@ const styles = StyleSheet.create({ } }); -const Title = React.memo(({ attachment, timeFormat }) => { +const Title = React.memo(({ attachment, timeFormat, theme }) => { if (!attachment.author_name) { return null; } const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null; return ( - {attachment.author_name ? {attachment.author_name} : null} - {time ? { time } : null} + {attachment.author_name ? {attachment.author_name} : null} + {time ? { time } : null} ); -}, () => true); +}); const Description = React.memo(({ - attachment, baseUrl, user, getCustomEmoji, useMarkdown + attachment, baseUrl, user, getCustomEmoji, useMarkdown, theme }) => { const text = attachment.text || attachment.title; if (!text) { @@ -97,6 +92,7 @@ const Description = React.memo(({ username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} + theme={theme} /> ); }, (prevProps, nextProps) => { @@ -106,10 +102,13 @@ const Description = React.memo(({ if (prevProps.attachment.title !== nextProps.attachment.title) { return false; } + if (prevProps.theme !== nextProps.theme) { + return false; + } return true; }); -const Fields = React.memo(({ attachment }) => { +const Fields = React.memo(({ attachment, theme }) => { if (!attachment.fields) { return null; } @@ -117,16 +116,16 @@ const Fields = React.memo(({ attachment }) => { {attachment.fields.map(field => ( - {field.title} - {field.value} + {field.title} + {field.value} ))} ); -}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields)); +}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme); const Reply = React.memo(({ - attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown + attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown, split, theme }) => { if (!attachment) { return null; @@ -140,17 +139,25 @@ const Reply = React.memo(({ if (attachment.type === 'file') { url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`; } - openLink(url); + openLink(url, theme); }; return ( 0 && styles.marginTop]} - background={Touchable.Ripple('#fff')} + style={[ + styles.button, + index > 0 && styles.marginTop, + { + backgroundColor: themes[theme].chatComponentBackground, + borderColor: themes[theme].borderColor + }, + split && sharedStyles.tabletContent + ]} + background={Touchable.Ripple(themes[theme].bannerBackground)} > - + <Title attachment={attachment} timeFormat={timeFormat} theme={theme} /> <Description attachment={attachment} timeFormat={timeFormat} @@ -158,12 +165,13 @@ const Reply = React.memo(({ user={user} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} + theme={theme} /> - <Fields attachment={attachment} /> + <Fields attachment={attachment} theme={theme} /> </View> </Touchable> ); -}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment)); +}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme); Reply.propTypes = { attachment: PropTypes.object, @@ -172,13 +180,16 @@ Reply.propTypes = { user: PropTypes.object, index: PropTypes.number, useMarkdown: PropTypes.bool, - getCustomEmoji: PropTypes.func + theme: PropTypes.string, + getCustomEmoji: PropTypes.func, + split: PropTypes.bool }; Reply.displayName = 'MessageReply'; Title.propTypes = { attachment: PropTypes.object, - timeFormat: PropTypes.string + timeFormat: PropTypes.string, + theme: PropTypes.string }; Title.displayName = 'MessageReplyTitle'; @@ -187,13 +198,15 @@ Description.propTypes = { baseUrl: PropTypes.string, user: PropTypes.object, useMarkdown: PropTypes.bool, - getCustomEmoji: PropTypes.func + getCustomEmoji: PropTypes.func, + theme: PropTypes.string }; Description.displayName = 'MessageReplyDescription'; Fields.propTypes = { - attachment: PropTypes.object + attachment: PropTypes.object, + theme: PropTypes.string }; Fields.displayName = 'MessageReplyFields'; -export default Reply; +export default withSplit(Reply); diff --git a/app/containers/message/Thread.js b/app/containers/message/Thread.js index c27473489..f72a429b6 100644 --- a/app/containers/message/Thread.js +++ b/app/containers/message/Thread.js @@ -6,9 +6,10 @@ import { formatLastMessage, formatMessageCount } from './utils'; import styles from './styles'; import { CustomIcon } from '../../lib/Icons'; import { THREAD } from './constants'; +import { themes } from '../../constants/colors'; const Thread = React.memo(({ - msg, tcount, tlm, customThreadTimeFormat, isThreadRoom + msg, tcount, tlm, customThreadTimeFormat, isThreadRoom, theme }) => { if (!tlm || isThreadRoom || tcount === 0) { return null; @@ -19,25 +20,29 @@ const Thread = React.memo(({ return ( <View style={styles.buttonContainer}> <View - style={[styles.button, styles.smallButton]} + style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]} testID={`message-thread-button-${ msg }`} > - <CustomIcon name='thread' size={20} style={styles.buttonIcon} /> - <Text style={styles.buttonText}>{buttonText}</Text> + <CustomIcon name='thread' size={20} style={[styles.buttonIcon, { color: themes[theme].buttonText }]} /> + <Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text> </View> - <Text style={styles.time}>{time}</Text> + <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> </View> ); }, (prevProps, nextProps) => { if (prevProps.tcount !== nextProps.tcount) { return false; } + if (prevProps.theme !== nextProps.theme) { + return false; + } return true; }); Thread.propTypes = { msg: PropTypes.string, tcount: PropTypes.string, + theme: PropTypes.string, tlm: PropTypes.string, customThreadTimeFormat: PropTypes.string, isThreadRoom: PropTypes.bool diff --git a/app/containers/message/Urls.js b/app/containers/message/Urls.js index 0275996a9..4c67faa7b 100644 --- a/app/containers/message/Urls.js +++ b/app/containers/message/Urls.js @@ -7,9 +7,9 @@ import isEqual from 'lodash/isEqual'; import openLink from '../../utils/openLink'; import sharedStyles from '../../views/Styles'; -import { - COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY -} from '../../constants/colors'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { withSplit } from '../../split'; const styles = StyleSheet.create({ button: { @@ -19,8 +19,6 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'column', borderRadius: 4, - backgroundColor: COLOR_BACKGROUND_CONTAINER, - borderColor: COLOR_BORDER, borderWidth: 1 }, textContainer: { @@ -31,13 +29,11 @@ const styles = StyleSheet.create({ alignItems: 'flex-start' }, title: { - color: COLOR_PRIMARY, fontSize: 16, ...sharedStyles.textMedium }, description: { fontSize: 16, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, marginTop: { @@ -59,10 +55,10 @@ const UrlImage = React.memo(({ image, user, baseUrl }) => { return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />; }, (prevProps, nextProps) => prevProps.image === nextProps.image); -const UrlContent = React.memo(({ title, description }) => ( +const UrlContent = React.memo(({ title, description, theme }) => ( <View style={styles.textContainer}> - {title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null} - {description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null} + {title ? <Text style={[styles.title, { color: themes[theme].tintColor }]} numberOfLines={2}>{title}</Text> : null} + {description ? <Text style={[styles.description, { color: themes[theme].auxiliaryText }]} numberOfLines={2}>{description}</Text> : null} </View> ), (prevProps, nextProps) => { if (prevProps.title !== nextProps.title) { @@ -71,41 +67,55 @@ const UrlContent = React.memo(({ title, description }) => ( if (prevProps.description !== nextProps.description) { return false; } + if (prevProps.theme !== nextProps.theme) { + return false; + } return true; }); const Url = React.memo(({ - url, index, user, baseUrl + url, index, user, baseUrl, split, theme }) => { if (!url) { return null; } - const onPress = () => openLink(url.url); + const onPress = () => openLink(url.url, theme); return ( <Touchable onPress={onPress} - style={[styles.button, index > 0 && styles.marginTop, styles.container]} - background={Touchable.Ripple('#fff')} + style={[ + styles.button, + index > 0 && styles.marginTop, + styles.container, + { + backgroundColor: themes[theme].chatComponentBackground, + borderColor: themes[theme].borderColor + }, + split && sharedStyles.tabletContent + ]} + background={Touchable.Ripple(themes[theme].bannerBackground)} > <> <UrlImage image={url.image} user={user} baseUrl={baseUrl} /> - <UrlContent title={url.title} description={url.description} /> + <UrlContent title={url.title} description={url.description} theme={theme} /> </> </Touchable> ); -}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url)); +}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); -const Urls = React.memo(({ urls, user, baseUrl }) => { +const Urls = React.memo(({ + urls, user, baseUrl, split, theme +}) => { if (!urls || urls.length === 0) { return null; } return urls.map((url, index) => ( - <Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} /> + <Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} split={split} theme={theme} /> )); -}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls)); +}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); UrlImage.propTypes = { image: PropTypes.string, @@ -116,7 +126,8 @@ UrlImage.displayName = 'MessageUrlImage'; UrlContent.propTypes = { title: PropTypes.string, - description: PropTypes.string + description: PropTypes.string, + theme: PropTypes.string }; UrlContent.displayName = 'MessageUrlContent'; @@ -124,15 +135,19 @@ Url.propTypes = { url: PropTypes.object.isRequired, index: PropTypes.number, user: PropTypes.object, - baseUrl: PropTypes.string + baseUrl: PropTypes.string, + theme: PropTypes.string, + split: PropTypes.bool }; Url.displayName = 'MessageUrl'; Urls.propTypes = { urls: PropTypes.array, user: PropTypes.object, - baseUrl: PropTypes.string + baseUrl: PropTypes.string, + theme: PropTypes.string, + split: PropTypes.bool }; Urls.displayName = 'MessageUrls'; -export default Urls; +export default withTheme(withSplit(Urls)); diff --git a/app/containers/message/User.js b/app/containers/message/User.js index 54634f08e..7795f2c55 100644 --- a/app/containers/message/User.js +++ b/app/containers/message/User.js @@ -3,6 +3,9 @@ import PropTypes from 'prop-types'; import { View, Text, StyleSheet } from 'react-native'; import moment from 'moment'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; + import MessageError from './MessageError'; import sharedStyles from '../../views/Styles'; import messageStyles from './styles'; @@ -16,7 +19,6 @@ const styles = StyleSheet.create({ username: { fontSize: 16, lineHeight: 22, - ...sharedStyles.textColorNormal, ...sharedStyles.textMedium }, titleContainer: { @@ -26,29 +28,28 @@ const styles = StyleSheet.create({ }, alias: { fontSize: 14, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular } }); const User = React.memo(({ - isHeader, useRealName, author, alias, ts, timeFormat, hasError, ...props + isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, ...props }) => { if (isHeader || hasError) { const username = (useRealName && author.name) || author.username; - const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null; + const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null; const time = moment(ts).format(timeFormat); return ( <View style={styles.container}> <View style={styles.titleContainer}> - <Text style={styles.username} numberOfLines={1}> + <Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}> {alias || username} {aliasUsername} </Text> </View> - <Text style={messageStyles.time}>{time}</Text> - { hasError && <MessageError hasError={hasError} {...props} /> } + <Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> + { hasError && <MessageError hasError={hasError} theme={theme} {...props} /> } </View> ); } @@ -62,8 +63,9 @@ User.propTypes = { author: PropTypes.object, alias: PropTypes.string, ts: PropTypes.instanceOf(Date), - timeFormat: PropTypes.string + timeFormat: PropTypes.string, + theme: PropTypes.string }; User.displayName = 'MessageUser'; -export default User; +export default withTheme(User); diff --git a/app/containers/message/Video.js b/app/containers/message/Video.js index 74cedca68..4f85b2735 100644 --- a/app/containers/message/Video.js +++ b/app/containers/message/Video.js @@ -6,9 +6,11 @@ import isEqual from 'deep-equal'; import Markdown from '../markdown'; import openLink from '../../utils/openLink'; -import { isIOS } from '../../utils/deviceInfo'; +import { isIOS, isTablet } from '../../utils/deviceInfo'; import { CustomIcon } from '../../lib/Icons'; import { formatAttachmentUrl } from '../../lib/utils'; +import { themes } from '../../constants/colors'; +import sharedStyles from '../../views/Styles'; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1; @@ -18,22 +20,14 @@ const styles = StyleSheet.create({ flex: 1, borderRadius: 4, height: 150, - backgroundColor: '#1f2329', marginBottom: 6, alignItems: 'center', justifyContent: 'center' - }, - modal: { - margin: 0, - backgroundColor: '#000' - }, - image: { - color: 'white' } }); const Video = React.memo(({ - file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji + file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji, theme }) => { if (!baseUrl) { return null; @@ -44,26 +38,26 @@ const Video = React.memo(({ return onOpenFileModal(file); } const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl); - openLink(uri); + openLink(uri, theme); }; return ( <> <Touchable onPress={onPress} - style={styles.button} - background={Touchable.Ripple('#fff')} + style={[styles.button, { backgroundColor: themes[theme].videoBackground }, isTablet && sharedStyles.tabletContent]} + background={Touchable.Ripple(themes[theme].bannerBackground)} > <CustomIcon name='play' size={54} - style={styles.image} + color={themes[theme].buttonText} /> </Touchable> - <Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} /> + <Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} theme={theme} /> </> ); -}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file)); +}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme); Video.propTypes = { file: PropTypes.object, @@ -71,7 +65,8 @@ Video.propTypes = { user: PropTypes.object, useMarkdown: PropTypes.bool, onOpenFileModal: PropTypes.func, - getCustomEmoji: PropTypes.func + getCustomEmoji: PropTypes.func, + theme: PropTypes.string }; export default Video; diff --git a/app/containers/message/index.js b/app/containers/message/index.js index 1aef420aa..3602d2682 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -6,8 +6,9 @@ import Message from './Message'; import debounce from '../../utils/debounce'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import messagesStatus from '../../constants/messagesStatus'; +import { withTheme } from '../../theme'; -export default class MessageContainer extends React.Component { +class MessageContainer extends React.Component { static propTypes = { item: PropTypes.object.isRequired, user: PropTypes.shape({ @@ -42,13 +43,15 @@ export default class MessageContainer extends React.Component { onOpenFileModal: PropTypes.func, onReactionLongPress: PropTypes.func, navToRoomInfo: PropTypes.func, - callJitsi: PropTypes.func + callJitsi: PropTypes.func, + theme: PropTypes.string } static defaultProps = { onLongPress: () => {}, archived: false, - broadcast: false + broadcast: false, + theme: 'light' } componentDidMount() { @@ -61,7 +64,11 @@ export default class MessageContainer extends React.Component { } } - shouldComponentUpdate() { + shouldComponentUpdate(nextProps) { + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } return false; } @@ -205,7 +212,7 @@ export default class MessageContainer extends React.Component { render() { const { - item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi + item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, theme } = this.props; const { id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, autoTranslate: autoTranslateMessage @@ -272,7 +279,10 @@ export default class MessageContainer extends React.Component { getCustomEmoji={getCustomEmoji} navToRoomInfo={navToRoomInfo} callJitsi={callJitsi} + theme={theme} /> ); } } + +export default withTheme(MessageContainer); diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 3113f542a..6b8559ab7 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -1,9 +1,7 @@ import { StyleSheet } from 'react-native'; import sharedStyles from '../../views/Styles'; -import { - COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE -} from '../../constants/colors'; +import { isTablet } from '../../utils/deviceInfo'; export default StyleSheet.create({ root: { @@ -25,6 +23,9 @@ export default StyleSheet.create({ messageContentWithError: { marginLeft: 0 }, + center: { + alignItems: 'center' + }, flex: { flexDirection: 'row' // flex: 1 @@ -43,27 +44,19 @@ export default StyleSheet.create({ marginBottom: 6, borderRadius: 2 }, - reactionButtonReacted: { - backgroundColor: '#e8f2ff' - }, reactionContainer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', borderRadius: 2, borderWidth: 1, - borderColor: COLOR_BORDER, height: 28, minWidth: 46.3 }, - reactedContainer: { - borderColor: COLOR_PRIMARY - }, reactionCount: { fontSize: 14, marginLeft: 3, marginRight: 8.5, - color: COLOR_PRIMARY, ...sharedStyles.textSemibold }, reactionEmoji: { @@ -81,9 +74,6 @@ export default StyleSheet.create({ avatarSmall: { marginLeft: 16 }, - addReaction: { - color: COLOR_PRIMARY - }, errorButton: { paddingLeft: 10, paddingVertical: 5 @@ -99,18 +89,15 @@ export default StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - backgroundColor: COLOR_PRIMARY, borderRadius: 2 }, smallButton: { height: 30 }, buttonIcon: { - color: COLOR_WHITE, marginRight: 6 }, buttonText: { - color: COLOR_WHITE, fontSize: 14, ...sharedStyles.textMedium }, @@ -121,10 +108,9 @@ export default StyleSheet.create({ }, image: { width: '100%', - maxWidth: 400, - minHeight: 200, + // maxWidth: 400, + minHeight: isTablet ? 300 : 200, borderRadius: 4, - borderColor: COLOR_BORDER, borderWidth: 1 }, imagePressed: { @@ -137,27 +123,23 @@ export default StyleSheet.create({ }, text: { fontSize: 16, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, textInfo: { fontStyle: 'italic', fontSize: 16, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, startedDiscussion: { fontStyle: 'italic', fontSize: 16, marginBottom: 6, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, time: { fontSize: 12, paddingLeft: 10, lineHeight: 22, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular, fontWeight: '300' }, @@ -169,14 +151,12 @@ export default StyleSheet.create({ marginBottom: 12 }, repliedThreadIcon: { - color: COLOR_PRIMARY, marginRight: 10, marginLeft: 16 }, repliedThreadName: { fontSize: 16, flex: 1, - color: COLOR_PRIMARY, ...sharedStyles.textRegular }, readReceipt: { diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 345544195..185ae9907 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -95,6 +95,7 @@ export default { announcement: 'announcement', Announcement: 'Announcement', Apply_Your_Certificate: 'Apply Your Certificate', + Applying_a_theme_will_change_how_the_app_looks: 'Applying a theme will change how the app looks.', ARCHIVE: 'ARCHIVE', archive: 'archive', are_typing: 'are typing', @@ -102,11 +103,13 @@ export default { Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?', Audio: 'Audio', Authenticating: 'Authenticating', + Automatic: 'Automatic', Auto_Translate: 'Auto-Translate', Avatar_changed_successfully: 'Avatar changed successfully!', Avatar_Url: 'Avatar URL', Away: 'Away', Back: 'Back', + Black: 'Black', Block_user: 'Block user', Broadcast_channel_Description: 'Only authorized users can write new messages, but the other users will be able to reply', Broadcast_Channel: 'Broadcast Channel', @@ -136,6 +139,7 @@ export default { connecting_server: 'connecting to server', Connecting: 'Connecting...', Contact_us: 'Contact us', + Contact_your_server_admin: 'Contact your server admin.', Continue_with: 'Continue with', Copied_to_clipboard: 'Copied to clipboard!', Copy: 'Copy', @@ -147,6 +151,8 @@ export default { Created_snippet: 'Created a snippet', Create_a_new_workspace: 'Create a new workspace', Create: 'Create', + Dark: 'Dark', + Dark_level: 'Dark Level', Default: 'Default', Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.', delete: 'delete', @@ -211,6 +217,7 @@ export default { leaving_room: 'leaving room', leave: 'leave', Legal: 'Legal', + Light: 'Light', License: 'License', Livechat: 'Livechat', Login: 'Login', @@ -257,6 +264,7 @@ export default { No_Reactions: 'No Reactions', No_Read_Receipts: 'No Read Receipts', Not_logged: 'Not logged', + Not_RC_Server: 'This is not a Rocket.Chat server.\n{{contact}}', Nothing: 'Nothing', Nothing_to_save: 'Nothing to save!', Notify_active_in_this_room: 'Notify active users in this room', @@ -277,6 +285,7 @@ export default { pinned: 'pinned', Pinned: 'Pinned', Please_enter_your_password: 'Please enter your password', + Preferences: 'Preferences', Preferences_saved: 'Preferences saved!', Privacy_Policy: ' Privacy Policy', Private_Channel: 'Private Channel', @@ -375,7 +384,7 @@ export default { Tap_to_view_servers_list: 'Tap to view servers list', Terms_of_Service: ' Terms of Service ', Theme: 'Theme', - The_URL_is_invalid: 'The URL you entered is invalid. Check it and try again, please!', + The_URL_is_invalid: 'Invalid URL or unable to establish a secure connection.\n{{contact}}', There_was_an_error_while_action: 'There was an error while {{action}}!', This_room_is_blocked: 'This room is blocked', This_room_is_read_only: 'This room is read only', @@ -420,6 +429,7 @@ export default { Video_call: 'Video call', View_Original: 'View Original', Voice_call: 'Voice call', + Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}', Welcome: 'Welcome', Welcome_to_RocketChat: 'Welcome to Rocket.Chat', Whats_your_2fa: 'What\'s your 2FA code?', @@ -438,5 +448,19 @@ export default { Version_no: 'Version: {{version}}', You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!', Change_Language: 'Change Language', - Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order to identify problems and fix it.' + Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order to identify problems and fix it.', + Type_message: 'Type message', + Room_search: 'Rooms search', + Room_selection: 'Room selection 1...9', + Next_room: 'Next room', + Previous_room: 'Previous room', + New_room: 'New room', + Upload_room: 'Upload to room', + Search_messages: 'Search messages', + Scroll_messages: 'Scroll messages', + Reply_latest: 'Reply to latest', + Server_selection: 'Server selection', + Server_selection_numbers: 'Server selection 1...9', + Add_server: 'Add server', + New_line: 'New line' }; diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index dbc66a0c4..fe13a5edf 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -99,16 +99,19 @@ export default { and: 'e', announcement: 'anúncio', Announcement: 'Anúncio', + Applying_a_theme_will_change_how_the_app_looks: 'Aplicar um tema mudará a aparência do app.', ARCHIVE: 'ARQUIVAR', archive: 'arquivar', are_typing: 'estão digitando', Are_you_sure_question_mark: 'Você tem certeza?', Are_you_sure_you_want_to_leave_the_room: 'Tem certeza de que deseja sair da sala {{room}}?', Authenticating: 'Autenticando', + Automatic: 'Automático', Avatar_changed_successfully: 'Avatar alterado com sucesso!', Avatar_Url: 'Avatar URL', Away: 'Ausente', Back: 'Voltar', + Black: 'Preto', Block_user: 'Bloquear usuário', Broadcast_channel_Description: 'Somente usuários autorizados podem escrever novas mensagens, mas os outros usuários poderão responder', Broadcast_Channel: 'Canal de Transmissão', @@ -138,6 +141,7 @@ export default { connecting_server: 'conectando no servidor', Connecting: 'Conectando...', Continue_with: 'Entrar com', + Contact_your_server_admin: 'Contate o administrador do servidor.', Copied_to_clipboard: 'Copiado para a área de transferência!', Copy: 'Copiar', Permalink: 'Link-Permanente', @@ -146,6 +150,8 @@ export default { Created_snippet: 'Criou um snippet', Create_a_new_workspace: 'Criar nova área de trabalho', Create: 'Criar', + Dark: 'Escuro', + Dark_level: 'Nível escuro', Delete_Room_Warning: 'A exclusão de uma sala irá apagar todas as mensagens postadas na sala. Isso não pode ser desfeito.', delete: 'excluir', Delete: 'Excluir', @@ -199,6 +205,7 @@ export default { leaving_room: 'saindo do canal', leave: 'sair', Legal: 'Legal', + Light: 'Claro', Livechat: 'Livechat', Login: 'Entrar', Login_error: 'Suas credenciais foram rejeitadas. Tente novamente por favor!', @@ -242,6 +249,7 @@ export default { Nothing_to_save: 'Nada para salvar!', Notify_active_in_this_room: 'Notificar usuários ativos nesta sala', Notify_all_in_this_room: 'Notificar todos nesta sala', + Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}', Offline: 'Offline', Oops: 'Ops!', Online: 'Online', @@ -255,6 +263,7 @@ export default { pinned: 'fixada', Pinned: 'Mensagens Fixadas', Please_enter_your_password: 'Por favor, digite sua senha', + Preferences: 'Preferências', Preferences_saved: 'Preferências salvas!', Privacy_Policy: ' Política de Privacidade', Private_Channel: 'Canal Privado', @@ -334,7 +343,8 @@ export default { Take_a_photo: 'Tirar uma foto', Take_a_video: 'Gravar um vídeo', Terms_of_Service: ' Termos de Serviço ', - The_URL_is_invalid: 'A URL fornecida é inválida ou não acessível. Por favor tente novamente, mas com uma url diferente.', + Theme: 'Tema', + The_URL_is_invalid: 'A URL fornecida é inválida ou incapaz de estabelecer uma conexão segura.\n{{contact}}', There_was_an_error_while_action: 'Aconteceu um erro {{action}}!', This_room_is_blocked: 'Este quarto está bloqueado', This_room_is_read_only: 'Este quarto é apenas de leitura', @@ -375,6 +385,7 @@ export default { Username_or_email: 'Usuário ou email', Video_call: 'Chamada de vídeo', Voice_call: 'Chamada de voz', + Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}', Welcome: 'Bem vindo', Welcome_to_RocketChat: 'Bem vindo ao Rocket.Chat', Whats_your_2fa: 'Qual seu código de autenticação?', @@ -389,5 +400,20 @@ export default { you: 'você', You: 'Você', You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Você precisa acessar ao menos um servidor Rocket.Chat para compartilhar.', - You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!' + You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!', + Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.', + Type_message: 'Digitar mensagem', + Room_search: 'Busca de sala', + Room_selection: 'Selecionar sala 1...9', + Next_room: 'Próxima sala', + Previous_room: 'Sala anterior', + New_room: 'Nova sala', + Upload_room: 'Enviar arquivo', + Search_messages: 'Buscar mensagens', + Scroll_messages: 'Rolar mensagens', + Reply_latest: 'Responder para última mensagem', + Server_selection: 'Seleção de servidor', + Server_selection_numbers: 'Selecionar servidor 1...9', + Add_server: 'Adicionar servidor', + New_line: 'Nova linha' }; diff --git a/app/i18n/locales/ru.js b/app/i18n/locales/ru.js index 780706f9a..dcc545334 100644 --- a/app/i18n/locales/ru.js +++ b/app/i18n/locales/ru.js @@ -1,5 +1,6 @@ export default { '1_person_reacted': '1 человек отреагировал', + '1_user': '1 пользователь', 'error-action-not-allowed': '{{action}} не допускается', 'error-application-not-found': 'Приложение не найдено', 'error-archived-duplicate-name': 'Есть архивный канал с именем {{room_name}}', @@ -21,7 +22,7 @@ export default { 'error-input-is-not-a-valid-field': '{{input}} недействительно {{field}}', 'error-invalid-actionlink': 'Недействительная ссылка действия', 'error-invalid-arguments': 'Недопустимые аргументы', - 'error-invalid-asset': 'Недопустимый актив', + 'error-invalid-asset': 'Недопустимый ресурс', 'error-invalid-channel': 'Недействительный канал.', 'error-invalid-channel-start-with-chars': 'Недействительный канал. Начните с @ или #', 'error-invalid-custom-field': 'Неверное настраиваемое поле', @@ -65,7 +66,7 @@ export default { 'error-role-in-use': 'Невозможно удалить роль, потому что она используется', 'error-role-name-required': 'Требуется имя роли', 'error-the-field-is-required': 'Требуется поле {{field}}.', - 'error-too-many-requests': 'Ошибка, слишком много запросов. Пожалуйста, помедленнее. Вы должны подождать {{seconds}} секунд, прежде чем повторять попытку.', + 'error-too-many-requests': 'Ошибка, слишком много запросов. Пожалуйста, помедленнее. Вы должны подождать {{seconds}} секунд, прежде чем повторить попытку.', 'error-user-is-not-activated': 'Пользователь не активирован', 'error-user-has-no-roles': 'Пользователь не имеет ролей', 'error-user-limit-exceeded': 'Количество пользователей, которых вы пытаетесь пригласить на #channel_name, превышает лимит, установленный администратором', @@ -75,28 +76,37 @@ export default { 'error-user-registration-secret': 'Регистрация пользователей разрешена только через секретный URL', 'error-you-are-last-owner': 'Вы последний владелец. Пожалуйста, установите нового владельца, прежде чем покинуть комнату.', Actions: 'Действия', + activity: 'активность', + Activity: 'Активность', Add_Reaction: 'Добавить реакцию', Add_Server: 'Добавить сервер', Add_user: 'Добавить пользователя', + Admin_Panel: 'Панель админа', Alert: 'Оповещение', alert: 'оповещение', alerts: 'оповещения', All_users_in_the_channel_can_write_new_messages: 'Все пользователи канала могут писать новые сообщения', All: 'Все', + All_Messages: 'Все сообщения', Allow_Reactions: 'Разрешить реакции', + Alphabetical: 'По алфавиту', and_more: 'и более', and: 'и', announcement: 'объявление', Announcement: 'Объявление', + Apply_Your_Certificate: 'Применить ваш сертификат', ARCHIVE: 'АРХИВ', archive: 'архив', are_typing: 'печатают', Are_you_sure_question_mark: 'Вы уверены?', Are_you_sure_you_want_to_leave_the_room: 'Вы действительно хотите покинуть канал {{room}}?', + Audio: 'Аудио', Authenticating: 'Аутентификация', + Auto_Translate: 'Автоперевод', Avatar_changed_successfully: 'Аватар успешно изменен!', Avatar_Url: 'URL аватара', Away: 'Отошел', + Back: 'Назад', Block_user: 'Блокировать пользователя', Broadcast_channel_Description: 'Только авторизованные пользователи могут писать новые сообщения, но другие пользователи смогут ответить', Broadcast_Channel: 'Широковещательный канал', @@ -106,60 +116,109 @@ export default { Cancel_recording: 'Отменить запись', Cancel: 'Отмена', changing_avatar: 'изменение аватара', + creating_channel: 'создание канала', Channel_Name: 'Название канала', + Channels: 'Каналы', Chats: 'Чаты', + Call_already_ended: 'Вызов уже завершен!', + Click_to_join: 'Нажмите, чтобы присоединиться!', Close: 'Закрыть', Close_emoji_selector: 'Закрыть селектор emoji', Choose: 'Выбрать', Choose_from_library: 'Выбрать из библиотеки', + Choose_file: 'Выбрать файл', Code: 'Код', Collaborative: 'Совместный', + Confirm: 'Подтверждение', Connect: 'Соединение', - Connected_to: 'Подключен к', - Connecting: 'Соединение', + Connect_to_a_server: 'Подключиться к серверу', + Connected: 'Подключено', + connecting_server: 'подключение к серверу', + Connecting: 'Соединение...', + Contact_us: 'Связаться с нами', + Contact_your_server_admin: 'Свяжитесь с администратором сервера.', + Continue_with: 'Продолжить с', Copied_to_clipboard: 'Скопировано в буфер обмена!', Copy: 'Копировать', - Permalink: 'постоянную ссылку', + Permalink: 'Постоянная ссылка', + Certificate_password: 'Пароль сертификата', + Whats_the_password_for_your_certificate: 'Какой пароль для вашего сертификата?', Create_account: 'Создать аккаунт', Create_Channel: 'Создать канал', + Created_snippet: 'Создать сниппет', + Create_a_new_workspace: 'Новое рабочее пространство', Create: 'Создать', + Default: 'По умолчанию', Delete_Room_Warning: 'Удаление канала приведет к удалению всех сообщений, размещенных в нем. Это не может быть отменено.', delete: 'удалить', Delete: 'Удалить', DELETE: 'УДАЛИТЬ', description: 'описание', Description: 'Описание', + DESKTOP_OPTIONS: 'ПАРАМЕТРЫ РАБОЧЕГО СТОЛА', + Directory: 'Директория', + Direct_Messages: 'Личные сообщения', Disable_notifications: 'Отключить уведомления', + Discussions: 'Дискуссии', + Dont_Have_An_Account: 'Нет аккаунта?', + Do_you_have_a_certificate: 'У вас есть сертификат?', Do_you_really_want_to_key_this_room_question_mark: 'Вы действительно хотите {{key}} этот канал?', edit: 'редактировать', + edited: 'отредактировано', Edit: 'Редактировать', Email_or_password_field_is_empty: 'Поле электронной почты или пароля пусты', - Email: 'Электронная почта', + Email: 'Email', + EMAIL: 'EMAIL', + email: 'e-mail', + Enable_Auto_Translate: 'Включить автоперевод', + Enable_markdown: 'Включить markdown', Enable_notifications: 'Включить уведомления', Everyone_can_access_this_channel: 'Каждый может получить доступ к этому каналу', + erasing_room: 'стирание комнаты', Error_uploading: 'Ошибка при загрузке', + Favorite: 'Избранное', + Favorites: 'Избранные', Files: 'Файлы', + File_description: 'Описание файла', + File_name: 'Имя файла', Finish_recording: 'Завершить запись', + Following_thread: 'Следить за темой', For_your_security_you_must_enter_your_current_password_to_continue: 'В целях вашей безопасности вы должны ввести свой текущий пароль для продолжения', Forgot_my_password: 'Забыл свой пароль', Forgot_password_If_this_email_is_registered: 'Если эта электронная почта зарегистрирована, мы отправим инструкции о том, как сбросить пароль. Если вы не получите письмо в ближайшее время, вернитесь и повторите попытку.', Forgot_password: 'Забыли пароль', Forgot_Password: 'Забыли Пароль', + Full_table: 'Нажмите, чтобы увидеть полную таблицу', + Group_by_favorites: 'По избранным', + Group_by_type: 'По типу', + Hide: 'Скрыть', Has_joined_the_channel: 'Присоединился к каналу', + Has_joined_the_conversation: 'Присоединился к беседе', Has_left_the_channel: 'Покинул канал', - I_have_an_account: 'У меня есть аккаунт', + IN_APP_AND_DESKTOP: 'В приложении и на десктопе', + In_App_and_Desktop_Alert_info: 'Отображает баннер в верхней части экрана, когда приложение открыто, и отображает уведомление на рабочем столе.', Invisible: 'Невидимый', + Invite: 'Приглашение', is_a_valid_RocketChat_instance: 'является действительным сервером Rocket.Chat', is_not_a_valid_RocketChat_instance: 'не является действительным сервером Rocket.Chat', is_typing: 'печатает', + Invalid_server_version: 'Сервер, к которому вы пытаетесь подключиться, использует версию, которая больше не поддерживается приложением: {{currentVersion}}.\n\nНам нужна версия {{minVersion}}', + Join_the_community: 'Присоединиться к сообществу', + Join: 'Присоединиться', Just_invited_people_can_access_this_channel: 'Только приглашенные люди могут получить доступ к этому каналу', Language: 'Язык', last_message: 'последнее сообщение', Leave_channel: 'Покинуть канал', + leaving_room: 'покинуть комнату', leave: 'покинуть', - Loading_messages_ellipsis: 'Загрузка сообщений ...', + Legal: 'Правовые аспекты', + License: 'Лицензия', + Livechat: 'Livechat', Login: 'Вход', + Login_error: 'Ваши учетные данные были отклонены! Пожалуйста, попробуйте еще раз.', + Login_with: 'Войти с', Logout: 'Выйти', + members: 'пользователи', Members: 'Пользователи', Mentioned_Messages: 'Упомянутые сообщения', mentioned: 'упомянутые', @@ -168,35 +227,51 @@ export default { Message_actions: 'Действия с сообщением', Message_pinned: 'Сообщение прикреплено', Message_removed: 'Сообщение удалено', + message: 'сообщение', + messages: 'сообщения', Messages: 'Сообщения', - Microphone_Permission_Message: 'Rocket Chat нуждается в доступе к вашему микрофону, чтобы вы могли отправлять аудиосообщения.', + Message_Reported: 'Сообщение отправлено', + Microphone_Permission_Message: 'Rocket Chat нужен доступ к вашему микрофону, чтобы вы могли отправлять аудиосообщения.', Microphone_Permission: 'Разрешение на использование микрофона', Mute: 'Заглушить', muted: 'Заглушен', My_servers: 'Мои серверы', N_person_reacted: '{{n}} людей отреагировало', + N_users: '{{n}} пользователи', + name: 'имя', Name: 'Имя', - New_in_RocketChat_question_mark: 'Новичок в Rocket.Chat?', - New_Message: 'Новое Сообщение', - New_Password: 'Новый Пароль', - New_Server: 'Новый Сервер', + New_Message: 'Новое сообщение', + New_Password: 'Новый пароль', + New_Server: 'Новый сервер', No_files: 'Нет файлов', + Next: 'Далее', No_mentioned_messages: 'Нет упоминаний', No_pinned_messages: 'Нет прикрепленных сообщений', + No_results_found: 'Ничего не найдено', No_starred_messages: 'Нет отмеченных сообщений', + No_thread_messages: 'Нет сообщений в теме', No_announcement_provided: 'Нет объявлений.', No_description_provided: 'Нет описания.', No_topic_provided: 'Нет темы.', No_Message: 'Нет сообщения', + No_messages_yet: 'Пока нет сообщений', No_Reactions: 'Нет реакций', + No_Read_Receipts: 'Нет информации о прочтении', Not_logged: 'Не зарегистрирован', + Not_RC_Server: 'Это не сервер Rocket.Chat.\n{{contact}}', + Nothing: 'Ничего', Nothing_to_save: 'Нечего сохранять!', Notify_active_in_this_room: 'Уведомить всех активных пользователей в этом чате', Notify_all_in_this_room: 'Уведомить всех в этом чате', + Notifications: 'Уведомления', + Notification_Duration: 'Продолжительность уведомлений', + Notification_Preferences: 'Настройки уведомлений', Offline: 'Офлайн', + Oops: 'Упс!', Online: 'Онлайн', Only_authorized_users_can_write_new_messages: 'Только авторизованные пользователи могут писать новые сообщения', Open_emoji_selector: 'Открыть селектор emoji', + Open_Source_Communication: 'Общение с открытым кодом', Password: 'Пароль', Permalink_copied_to_clipboard: 'Постоянная ссылка скопирована в буфер обмена!', Pin: 'Прикрепить сообщение', @@ -205,25 +280,40 @@ export default { Pinned: 'Прикреплено', Please_enter_your_password: 'Пожалуйста введите ваш пароль', Preferences_saved: 'Настройки сохранены!', - Privacy_Policy: ' Политика Конфиденциальности', + Privacy_Policy: ' Политика конфиденциальности', Private_Channel: 'Приватный канал', + Private_Groups: 'Приватные группы', Private: 'Приватный', + Processing: 'Обработка...', Profile_saved_successfully: 'Профиль успешно сохранен!', Profile: 'Профиль', Public_Channel: 'Публичный канал', Public: 'Публичный', + PUSH_NOTIFICATIONS: 'PUSH УВЕДОМЛЕНИЯ', + Push_Notifications_Alert_Info: 'Эти уведомления доставляются вам, когда приложение не открыто', Quote: 'Цитата', Reactions_are_disabled: 'Реакции отключены', Reactions_are_enabled: 'Реакции активированы', Reactions: 'Реакции', + Read: 'Читать', Read_Only_Channel: 'Канал только для чтения', Read_Only: 'Только для чтения', + Read_Receipt: 'Уведомление о прочтении', + Receive_Group_Mentions: 'Получать групповые уведомления', + Receive_Group_Mentions_Info: 'Получать @all и @here уведомления', Register: 'Зарегистрировать', Repeat_Password: 'Повторите пароль', + Replied_on: 'Ответил на:', + replies: 'ответы', + reply: 'ответить', Reply: 'Ответить', + Report: 'Жалоба', + Receive_Notification: 'Получать уведомления', + Receive_notifications_from: 'Получать уведомления от {{name}}', Resend: 'Отправить повторно', Reset_password: 'Сброс пароля', RESET: 'СБРОС', + resetting_password: 'сброс пароля', Roles: 'Роли', Room_actions: 'Действия с каналом', Room_changed_announcement: 'Объявление канала было изменено на: {{объявление}} пользователем {{userBy}}', @@ -243,59 +333,100 @@ export default { saving_settings: 'сохранение настроек', Search_Messages: 'Поиск сообщений', Search: 'Поиск', + Search_by: 'Поиск по', + Search_global_users: 'Поиск глобальных пользователей', + Search_global_users_description: 'При активации станет возможен поиск пользователей на других серверах.', + Seconds: '{{second}} секунд', Select_Avatar: 'Выбор аватара', + Select_Server: 'Выбор сервера', Select_Users: 'Выбор пользователей', Send: 'Отправить', Send_audio_message: 'Отправить аудиосообщение', + Send_crash_report: 'Отправить отчет об ошибке', Send_message: 'Отправить сообщение', + Send_to: 'Отправить...', + Sent_an_attachment: 'Отправить вложение', + Server: 'Сервер', Servers: 'Серверы', + Server_version: 'Версия сервера: {{version}}', + Set_username_subtitle: 'Имя пользователя необходимо для того, чтобы позволить другим упомянуть вас в сообщениях', Settings: 'Настройки', Settings_succesfully_changed: 'Настройки успешно изменены!', Share: 'Поделиться', + Share_this_app: 'Рассказать о приложении', + Show_Unread_Counter: 'Показать счетчик непрочитанных', + Show_Unread_Counter_Info: 'Счетчик непрочитанных отображается в виде значка справа от канала в списке каналов', Sign_in_your_server: 'Войдите на ваш сервер', Sign_Up: 'Регистрация', Some_field_is_invalid_or_empty: 'Некоторые поля недопустимы или пусты', - Star_room: 'Star room', - Star: 'Звезда', - Starred_Messages: 'Помеченные сообщения', + Sorting_by: 'Сортировать по {{key}}', + Sound: 'Звук', + Star_room: 'В избранное', + Star: 'Отметить', + Starred_Messages: 'Отмеченные сообщения', starred: 'отмечено', Starred: 'Отмечено', Start_of_conversation: 'Начало разговора', + Started_discussion: 'Началось обсуждение :', + Started_call: 'Звонок, начатый {{userBy}}', Submit: 'Отправить', + Table: 'Таблица', Take_a_photo: 'Сфотографировать', + Take_a_video: 'Записать видео', tap_to_change_status: 'нажмите для изменения статуса', Tap_to_view_servers_list: 'Нажмите, чтобы просмотреть список серверов', Terms_of_Service: ' Условия использования ', + Theme: 'Тема', + The_URL_is_invalid: 'IНеверный URL или невозможно установить безопасное соединение.\n{{contact}}', There_was_an_error_while_action: 'Произошла ошибка в процессе {{action}}!', This_room_is_blocked: 'Этот канал заблокирован', This_room_is_read_only: 'Этот канал доступен только для чтения', + Thread: 'Тема', + Threads: 'Темы', Timezone: 'Часовой пояс', + To: 'К', topic: 'топик', Topic: 'Топик', + Translate: 'Перевести', Try_again: 'Попробуйте еще раз', + Two_Factor_Authentication: 'Двухфакторная аутентификация', Type_the_channel_name_here: 'Введите название канала здесь', unarchive: 'разархивировать', UNARCHIVE: 'РАЗАРХИВИРОВАТЬ', Unblock_user: 'Разблокировать пользователя', + Unfavorite: 'Удалить из избранного', + Unfollowed_thread: 'Отписаться', Unmute: 'Отменить заглушивание', unmuted: 'Заглушивание отменено', Unpin: 'Открепить', unread_messages: 'непрочитанные', + Unread: 'Непрочитанные', + Unread_on_top: 'Непрочитанные сверху', Unstar: 'Снять отметку', + Updating: 'Обновление...', Uploading: 'Выгрузка', + Upload_file_question_mark: 'Загрузить файл?', + Users: 'Пользователи', User_added_by: 'Пользователь {{userAdded}} добавлен по решению {{userBy}}', User_has_been_key: 'Пользователь был {{key}}!', User_is_no_longer_role_by_: '{{user}} больше не {{role}} по решению {{userBy}}', User_muted_by: 'Пользователь {{userMuted}} заглушен по решению {{userBy}}', User_removed_by: 'Пользователь {{userRemoved}} удален по решению {{userBy}}', + User_sent_an_attachment: '{{user}} отправил вложение', User_unmuted_by: 'Пользователь {{userUnmuted}} перестал быть заглушенным по решению {{userBy}}', User_was_set_role_by_: '{{user}} был назначен {{role}} пользователем {{userBy}}', Username_is_empty: 'Имя пользователя пусто', Username: 'Имя пользователя', + Username_or_email: 'Имя пользователя или email', Validating: 'Проверка', Video_call: 'Видеозвонок', + View_Original: 'Посмотреть оригинал', Voice_call: 'Голосовой вызов', - Welcome: 'Добро пожаловать', + Websocket_disabled: 'Websocket отключен для этого сервера.\n{{contact}}', + Welcome: 'Добро пожаловать,', + Welcome_to_RocketChat: 'Добро пожаловать в Rocket.Chat', + Whats_your_2fa: 'Какой у вас код 2FA?', + Without_Servers: 'Без серверов', Yes_action_it: 'Да, {{action}} это!', Yesterday: 'Вчера', You_are_in_preview_mode: 'Вы находитесь в режиме предварительного просмотра', @@ -303,7 +434,12 @@ export default { You_can_search_using_RegExp_eg: 'Вы можете выполнить поиск с помощью регулярных выражений, например `/^text$/i`', You_colon: 'Вы: ', you_were_mentioned: 'вы были упомянуты', - You_will_not_be_able_to_recover_this_message: 'Вы не сможете восстановить это сообщение!', you: 'вы', - Your_server: 'Ваш сервер' + You: 'Вы', + You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Вам нужно получить доступ как минимум к одному серверу Rocket.Chat, чтобы поделиться чем-то.', + Your_certificate: 'Ваш сертификат', + Version_no: 'Version: {{version}}', + You_will_not_be_able_to_recover_this_message: 'Вы не сможете восстановить это сообщение!', + Change_Language: 'Изменить язык', + Crash_report_disclaimer: 'Мы никогда не отслеживаем содержание ваших чатов. Отчет о сбое содержит только важную для нас информацию для выявления проблем и их устранения.' }; diff --git a/app/index.js b/app/index.js index 9782714fb..2ad65f8aa 100644 --- a/app/index.js +++ b/app/index.js @@ -1,13 +1,23 @@ import React from 'react'; +import { View, Linking, BackHandler } from 'react-native'; import { createAppContainer, createSwitchNavigator } from 'react-navigation'; import { createStackNavigator } from 'react-navigation-stack'; import { createDrawerNavigator } from 'react-navigation-drawer'; +import { AppearanceProvider } from 'react-native-appearance'; import { Provider } from 'react-redux'; -import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved -import { Linking } from 'react-native'; import PropTypes from 'prop-types'; +import RNUserDefaults from 'rn-user-defaults'; +import Modal from 'react-native-modal'; +import KeyCommands, { KeyCommandsEmitter } from 'react-native-keycommands'; -import { appInit } from './actions'; +import { + defaultTheme, + newThemeState, + subscribeTheme, + unsubscribeTheme +} from './utils/theme'; +import EventEmitter from './utils/events'; +import { appInit, appInitLocalSettings } from './actions'; import { deepLinkingOpen } from './actions/deepLinking'; import Navigation from './lib/Navigation'; import Sidebar from './views/SidebarView'; @@ -15,13 +25,24 @@ import parseQuery from './lib/methods/helpers/parseQuery'; import { initializePushNotifications, onNotification } from './notifications/push'; import store from './lib/createStore'; import NotificationBadge from './notifications/inApp'; -import { defaultHeader, onNavigationStateChange } from './utils/navigation'; +import { defaultHeader, onNavigationStateChange, cardStyle } from './utils/navigation'; import { loggerConfig, analytics } from './utils/log'; import Toast from './containers/Toast'; -import RocketChat from './lib/rocketchat'; -import LayoutAnimation from './utils/layoutAnimation'; +import { ThemeContext } from './theme'; +import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat'; +import { MIN_WIDTH_SPLIT_LAYOUT } from './constants/tablet'; +import { + isTablet, isSplited, isIOS, setWidth, supportSystemTheme +} from './utils/deviceInfo'; +import { KEY_COMMAND } from './commands'; +import Tablet, { initTabletNav } from './tablet'; +import sharedStyles from './views/Styles'; +import { SplitContext } from './split'; -useScreens(); +if (isIOS) { + const RNScreens = require('react-native-screens'); + RNScreens.useScreens(); +} const parseDeepLinking = (url) => { if (url) { @@ -62,7 +83,8 @@ const OutsideStack = createStackNavigator({ getScreen: () => require('./views/LegalView').default } }, { - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); const AuthenticationWebViewStack = createStackNavigator({ @@ -70,7 +92,8 @@ const AuthenticationWebViewStack = createStackNavigator({ getScreen: () => require('./views/AuthenticationWebView').default } }, { - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); const OutsideStackModal = createStackNavigator({ @@ -79,17 +102,30 @@ const OutsideStackModal = createStackNavigator({ }, { mode: 'modal', - headerMode: 'none' + headerMode: 'none', + cardStyle }); +const RoomRoutes = { + RoomView: { + getScreen: () => require('./views/RoomView').default + }, + ThreadMessagesView: { + getScreen: () => require('./views/ThreadMessagesView').default + }, + MarkdownTableView: { + getScreen: () => require('./views/MarkdownTableView').default + }, + ReadReceiptsView: { + getScreen: () => require('./views/ReadReceiptView').default + } +}; + // Inside const ChatsStack = createStackNavigator({ RoomsListView: { getScreen: () => require('./views/RoomsListView').default }, - RoomView: { - getScreen: () => require('./views/RoomView').default - }, RoomActionsView: { getScreen: () => require('./views/RoomActionsView').default }, @@ -108,34 +144,35 @@ const ChatsStack = createStackNavigator({ SelectedUsersView: { getScreen: () => require('./views/SelectedUsersView').default }, - ThreadMessagesView: { - getScreen: () => require('./views/ThreadMessagesView').default - }, MessagesView: { getScreen: () => require('./views/MessagesView').default }, AutoTranslateView: { getScreen: () => require('./views/AutoTranslateView').default }, - ReadReceiptsView: { - getScreen: () => require('./views/ReadReceiptView').default - }, DirectoryView: { getScreen: () => require('./views/DirectoryView').default }, - TableView: { - getScreen: () => require('./views/TableView').default - }, NotificationPrefView: { getScreen: () => require('./views/NotificationPreferencesView').default - } + }, + ...RoomRoutes }, { - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle +}); + +// Inside +const RoomStack = createStackNavigator({ + ...RoomRoutes +}, { + defaultNavigationOptions: defaultHeader, + cardStyle }); ChatsStack.navigationOptions = ({ navigation }) => { let drawerLockMode = 'unlocked'; - if (navigation.state.index > 0) { + if (navigation.state.index > 0 || isSplited()) { drawerLockMode = 'locked-closed'; } return { @@ -148,7 +185,8 @@ const ProfileStack = createStackNavigator({ getScreen: () => require('./views/ProfileView').default } }, { - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); ProfileStack.navigationOptions = ({ navigation }) => { @@ -167,9 +205,13 @@ const SettingsStack = createStackNavigator({ }, LanguageView: { getScreen: () => require('./views/LanguageView').default + }, + ThemeView: { + getScreen: () => require('./views/ThemeView').default } }, { - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); const AdminPanelStack = createStackNavigator({ @@ -177,7 +219,8 @@ const AdminPanelStack = createStackNavigator({ getScreen: () => require('./views/AdminPanelView').default } }, { - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); SettingsStack.navigationOptions = ({ navigation }) => { @@ -211,7 +254,8 @@ const NewMessageStack = createStackNavigator({ getScreen: () => require('./views/CreateChannelView').default } }, { - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); const InsideStackModal = createStackNavigator({ @@ -223,35 +267,210 @@ const InsideStackModal = createStackNavigator({ }, { mode: 'modal', - headerMode: 'none' + headerMode: 'none', + cardStyle }); const SetUsernameStack = createStackNavigator({ SetUsernameView: { getScreen: () => require('./views/SetUsernameView').default } +}, +{ + cardStyle }); class CustomInsideStack extends React.Component { static router = InsideStackModal.router; static propTypes = { - navigation: PropTypes.object + navigation: PropTypes.object, + screenProps: PropTypes.object } render() { - const { navigation } = this.props; + const { navigation, screenProps } = this.props; return ( <> - <InsideStackModal navigation={navigation} /> - <NotificationBadge navigation={navigation} /> + <InsideStackModal navigation={navigation} screenProps={screenProps} /> + { !isTablet ? <NotificationBadge navigation={navigation} /> : null } + { !isTablet ? <Toast /> : null } + </> + ); + } +} + +class CustomRoomStack extends React.Component { + static router = RoomStack.router; + + static propTypes = { + navigation: PropTypes.object, + screenProps: PropTypes.object + } + + render() { + const { navigation, screenProps } = this.props; + return ( + <> + <RoomStack navigation={navigation} screenProps={screenProps} /> <Toast /> </> ); } } -const App = createAppContainer(createSwitchNavigator( +const MessagesStack = createStackNavigator({ + NewMessageView: { + getScreen: () => require('./views/NewMessageView').default + }, + SelectedUsersViewCreateChannel: { + getScreen: () => require('./views/SelectedUsersView').default + }, + CreateChannelView: { + getScreen: () => require('./views/CreateChannelView').default + } +}, { + defaultNavigationOptions: defaultHeader, + cardStyle +}); + +const DirectoryStack = createStackNavigator({ + DirectoryView: { + getScreen: () => require('./views/DirectoryView').default + } +}, { + defaultNavigationOptions: defaultHeader, + cardStyle +}); + +const SidebarStack = createStackNavigator({ + SettingsView: { + getScreen: () => require('./views/SettingsView').default + }, + ProfileView: { + getScreen: () => require('./views/ProfileView').default + }, + AdminPanelView: { + getScreen: () => require('./views/AdminPanelView').default + } +}, { + defaultNavigationOptions: defaultHeader, + cardStyle +}); + +const RoomActionsStack = createStackNavigator({ + RoomActionsView: { + getScreen: () => require('./views/RoomActionsView').default + }, + RoomInfoView: { + getScreen: () => require('./views/RoomInfoView').default + }, + RoomInfoEditView: { + getScreen: () => require('./views/RoomInfoEditView').default + }, + RoomMembersView: { + getScreen: () => require('./views/RoomMembersView').default + }, + SearchMessagesView: { + getScreen: () => require('./views/SearchMessagesView').default + }, + SelectedUsersView: { + getScreen: () => require('./views/SelectedUsersView').default + }, + MessagesView: { + getScreen: () => require('./views/MessagesView').default + }, + AutoTranslateView: { + getScreen: () => require('./views/AutoTranslateView').default + }, + ReadReceiptsView: { + getScreen: () => require('./views/ReadReceiptView').default + }, + NotificationPrefView: { + getScreen: () => require('./views/NotificationPreferencesView').default + } +}, { + defaultNavigationOptions: defaultHeader, + cardStyle +}); + + +const ModalSwitch = createSwitchNavigator({ + MessagesStack, + DirectoryStack, + SidebarStack, + RoomActionsStack, + SettingsStack, + AuthLoading: () => null +}, +{ + initialRouteName: 'AuthLoading' +}); + +class CustomModalStack extends React.Component { + static router = ModalSwitch.router; + + static propTypes = { + navigation: PropTypes.object, + showModal: PropTypes.bool, + closeModal: PropTypes.func, + screenProps: PropTypes.object + } + + componentDidMount() { + this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.closeModal); + } + + componentWillUnmount() { + this.backHandler.remove(); + } + + closeModal = () => { + const { closeModal, navigation } = this.props; + const { state } = navigation; + if (state && state.routes[state.index] && state.routes[state.index].index === 0) { + closeModal(); + return true; + } + return false; + } + + render() { + const { + navigation, showModal, closeModal, screenProps + } = this.props; + return ( + <Modal + useNativeDriver + coverScreen={false} + isVisible={showModal} + onBackdropPress={closeModal} + hideModalContentWhileAnimating + avoidKeyboard + > + <View style={sharedStyles.modal}> + <ModalSwitch navigation={navigation} screenProps={screenProps} /> + </View> + </Modal> + ); + } +} + +class CustomNotificationStack extends React.Component { + static router = InsideStackModal.router; + + static propTypes = { + navigation: PropTypes.object, + screenProps: PropTypes.object + } + + render() { + const { navigation, screenProps } = this.props; + return <NotificationBadge navigation={navigation} screenProps={screenProps} />; + } +} + +export const App = createAppContainer(createSwitchNavigator( { OutsideStack: OutsideStackModal, InsideStack: CustomInsideStack, @@ -265,11 +484,30 @@ const App = createAppContainer(createSwitchNavigator( } )); +export const RoomContainer = createAppContainer(CustomRoomStack); + +export const ModalContainer = createAppContainer(CustomModalStack); + +export const NotificationContainer = createAppContainer(CustomNotificationStack); + export default class Root extends React.Component { constructor(props) { super(props); this.init(); this.initCrashReport(); + this.state = { + split: false, + inside: false, + showModal: false, + theme: defaultTheme(), + themePreferences: { + currentTheme: supportSystemTheme() ? 'automatic' : 'light', + darkLevel: 'dark' + } + }; + if (isTablet) { + this.initTablet(); + } } componentDidMount() { @@ -283,13 +521,36 @@ export default class Root extends React.Component { }, 5000); } + // eslint-disable-next-line no-unused-vars + componentDidUpdate(_, prevState) { + if (isTablet) { + const { split, inside } = this.state; + if (inside && split !== prevState.split) { + // Reset app on split mode changes + Navigation.navigate('RoomsListView'); + this.closeModal(); + } + } + } + componentWillUnmount() { clearTimeout(this.listenerTimeout); + + unsubscribeTheme(); + + if (this.onKeyCommands && this.onKeyCommands.remove) { + this.onKeyCommands.remove(); + } } init = async() => { + if (isIOS) { + await RNUserDefaults.setName('group.ios.chat.rocket'); + } + RNUserDefaults.objectForKey(THEME_PREFERENCES_KEY).then(this.setTheme); const [notification, deepLinking] = await Promise.all([initializePushNotifications(), Linking.getInitialURL()]); const parsedDeepLinkingURL = parseDeepLinking(deepLinking); + store.dispatch(appInitLocalSettings()); if (notification) { onNotification(notification); } else if (parsedDeepLinkingURL) { @@ -299,6 +560,24 @@ export default class Root extends React.Component { } } + setTheme = (newTheme = {}) => { + // change theme state + this.setState(prevState => newThemeState(prevState, newTheme), () => { + const { themePreferences } = this.state; + // subscribe to Appearance changes + subscribeTheme(themePreferences, this.setTheme); + }); + } + + initTablet = async() => { + initTabletNav(args => this.setState(args)); + await KeyCommands.setKeyCommands([]); + this.onKeyCommands = KeyCommandsEmitter.addListener( + 'onKeyCommand', + command => EventEmitter.emit(KEY_COMMAND, { event: command }) + ); + } + initCrashReport = () => { RocketChat.getAllowCrashReport() .then((allowCrashReport) => { @@ -310,18 +589,59 @@ export default class Root extends React.Component { }); } + onLayout = ({ nativeEvent: { layout: { width } } }) => (isTablet ? this.setSplit(width) : null); + + setSplit = (width) => { + this.setState({ split: width > MIN_WIDTH_SPLIT_LAYOUT }); + setWidth(width); + } + + closeModal = () => this.setState({ showModal: false }); + render() { + const { split, themePreferences, theme } = this.state; + + let content = ( + <App + ref={(navigatorRef) => { + Navigation.setTopLevelNavigator(navigatorRef); + }} + screenProps={{ split, theme }} + onNavigationStateChange={onNavigationStateChange} + /> + ); + + if (isTablet) { + const { inside, showModal } = this.state; + content = ( + <SplitContext.Provider value={{ split }}> + <Tablet + theme={theme} + tablet={split} + inside={inside} + showModal={showModal} + closeModal={this.closeModal} + onLayout={this.onLayout} + > + {content} + </Tablet> + </SplitContext.Provider> + ); + } return ( - <Provider store={store}> - <LayoutAnimation> - <App - ref={(navigatorRef) => { - Navigation.setTopLevelNavigator(navigatorRef); + <AppearanceProvider> + <Provider store={store}> + <ThemeContext.Provider + value={{ + theme, + themePreferences, + setTheme: this.setTheme }} - onNavigationStateChange={onNavigationStateChange} - /> - </LayoutAnimation> - </Provider> + > + {content} + </ThemeContext.Provider> + </Provider> + </AppearanceProvider> ); } } diff --git a/app/lib/ModalNavigation.js b/app/lib/ModalNavigation.js new file mode 100644 index 000000000..3474a7451 --- /dev/null +++ b/app/lib/ModalNavigation.js @@ -0,0 +1,21 @@ +import { NavigationActions } from 'react-navigation'; + +let _navigatorModal; + +function setTopLevelNavigator(navigatorRef) { + _navigatorModal = navigatorRef; +} + +function navigate(routeName, params) { + _navigatorModal.dispatch( + NavigationActions.navigate({ + routeName, + params + }) + ); +} + +export default { + navigate, + setTopLevelNavigator +}; diff --git a/app/lib/database/index.js b/app/lib/database/index.js index 7216779d4..1508d11fc 100644 --- a/app/lib/database/index.js +++ b/app/lib/database/index.js @@ -44,15 +44,46 @@ class DB { } get active() { - return this.databases.activeDB; + return this.databases.shareDB || this.databases.activeDB; + } + + get share() { + return this.databases.shareDB; + } + + set share(db) { + this.databases.shareDB = db; } get servers() { return this.databases.serversDB; } + setShareDB(database = '') { + const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//, '.'); + const dbName = `${ appGroupPath }${ path }.db`; + + const adapter = new SQLiteAdapter({ + dbName, + schema: appSchema, + migrations + }); + + this.databases.shareDB = new Database({ + adapter, + modelClasses: [ + Subscription, + Message, + Thread, + ThreadMessage, + Upload + ], + actionsEnabled: true + }); + } + setActiveDB(database = '') { - const path = database.replace(/(^\w+:|^)\/\//, ''); + const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//, '.'); const dbName = `${ appGroupPath }${ path }.db`; const adapter = new SQLiteAdapter({ diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js index 577bec584..630c640b3 100644 --- a/app/lib/database/model/Subscription.js +++ b/app/lib/database/model/Subscription.js @@ -87,4 +87,6 @@ export default class Subscription extends Model { @children('threads') threads; @children('thread_messages') threadMessages; + + @field('hide_unread_status') hideUnreadStatus; } diff --git a/app/lib/database/model/migrations.js b/app/lib/database/model/migrations.js index f3a7f57b5..3c78dec36 100644 --- a/app/lib/database/model/migrations.js +++ b/app/lib/database/model/migrations.js @@ -12,6 +12,17 @@ export default schemaMigrations({ ] }) ] + }, + { + toVersion: 3, + steps: [ + addColumns({ + table: 'subscriptions', + columns: [ + { name: 'hide_unread_status', type: 'boolean', isOptional: true } + ] + }) + ] } ] }); diff --git a/app/lib/database/schema/app.js b/app/lib/database/schema/app.js index 563d2fed2..8e38f76a6 100644 --- a/app/lib/database/schema/app.js +++ b/app/lib/database/schema/app.js @@ -1,7 +1,7 @@ import { appSchema, tableSchema } from '@nozbe/watermelondb'; export default appSchema({ - version: 2, + version: 3, tables: [ tableSchema({ name: 'subscriptions', @@ -38,7 +38,8 @@ export default appSchema({ { name: 'last_thread_sync', type: 'number', isOptional: true }, { name: 'jitsi_timeout', type: 'number', isOptional: true }, { name: 'auto_translate', type: 'boolean', isOptional: true }, - { name: 'auto_translate_language', type: 'string' } + { name: 'auto_translate_language', type: 'string' }, + { name: 'hide_unread_status', type: 'boolean', isOptional: true } ] }), tableSchema({ diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js index d7dd56580..9454b9013 100644 --- a/app/lib/methods/getCustomEmojis.js +++ b/app/lib/methods/getCustomEmojis.js @@ -91,7 +91,7 @@ export function getCustomEmojis() { const updatedSince = await getUpdatedSince(allRecords); // if server version is lower than 0.75.0, fetches from old api - if (semver.lt(serverVersion, '0.75.0')) { + if (serverVersion && semver.lt(serverVersion, '0.75.0')) { // RC 0.61.0 const result = await this.sdk.get('emoji-custom'); diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index b6a2dcd02..0d3829808 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -79,7 +79,7 @@ export default function() { const allRecords = await permissionsCollection.query().fetch(); // if server version is lower than 0.73.0, fetches from old api - if (semver.lt(serverVersion, '0.73.0')) { + if (serverVersion && semver.lt(serverVersion, '0.73.0')) { // RC 0.66.0 const result = await this.sdk.get('permissions.list'); if (!result.success) { diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js index 5cd8a04d0..997e3c533 100644 --- a/app/lib/methods/getSettings.js +++ b/app/lib/methods/getSettings.js @@ -2,12 +2,14 @@ import { InteractionManager } from 'react-native'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { Q } from '@nozbe/watermelondb'; +import RocketChat from '../rocketchat'; import reduxStore from '../createStore'; import * as actions from '../../actions'; import settings from '../../constants/settings'; import log from '../../utils/log'; import database from '../database'; import protectedFunction from './helpers/protectedFunction'; +import fetch from '../../utils/fetch'; const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize']; @@ -50,6 +52,20 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => { }); }; +export async function setSettings() { + const db = database.active; + const settingsCollection = db.collections.get('settings'); + const settingsRecords = await settingsCollection.query().fetch(); + const parsed = Object.values(settingsRecords).map(item => ({ + _id: item.id, + valueAsString: item.valueAsString, + valueAsBoolean: item.valueAsBoolean, + valueAsNumber: item.valueAsNumber, + _updatedAt: item._updatedAt + })); + reduxStore.dispatch(actions.setAllSettings(RocketChat.parseSettings(parsed.slice(0, parsed.length)))); +} + export default async function() { try { const db = database.active; diff --git a/app/lib/methods/helpers/parseUrls.js b/app/lib/methods/helpers/parseUrls.js index e0896e3a7..d49c54c47 100644 --- a/app/lib/methods/helpers/parseUrls.js +++ b/app/lib/methods/helpers/parseUrls.js @@ -9,6 +9,13 @@ export default urls => urls.filter(url => url.meta && !url.ignoreParse).map((url decodedOgImage = meta.ogImage.replace(/&/g, '&'); } tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl; + if (tmp.image) { + if (tmp.image.indexOf('//') === 0) { + tmp.image = `${ url.parsedUrl.protocol }${ tmp.image }`; + } else if (tmp.image.indexOf('/') === 0 && (url.parsedUrl && url.parsedUrl.host)) { + tmp.image = `${ url.parsedUrl.protocol }//${ url.parsedUrl.host }${ tmp.image }`; + } + } tmp.url = url.url; return tmp; }); diff --git a/app/lib/methods/readMessages.js b/app/lib/methods/readMessages.js index 9e61cfc44..552cd1e0e 100644 --- a/app/lib/methods/readMessages.js +++ b/app/lib/methods/readMessages.js @@ -3,12 +3,14 @@ import log from '../../utils/log'; export default async function readMessages(rid, lastOpen) { try { - // RC 0.61.0 - const data = await this.sdk.post('subscriptions.read', { rid }); const db = database.active; + const subscription = await db.collections.get('subscriptions').find(rid); + + // RC 0.61.0 + await this.sdk.post('subscriptions.read', { rid }); + await db.action(async() => { try { - const subscription = await db.collections.get('subscriptions').find(rid); await subscription.update((s) => { s.open = true; s.alert = false; @@ -22,7 +24,6 @@ export default async function readMessages(rid, lastOpen) { // Do nothing } }); - return data; } catch (e) { log(e); } diff --git a/app/lib/methods/sendFileMessage.js b/app/lib/methods/sendFileMessage.js index dc8674119..511339158 100644 --- a/app/lib/methods/sendFileMessage.js +++ b/app/lib/methods/sendFileMessage.js @@ -2,6 +2,7 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import database from '../database'; import log from '../../utils/log'; +import { headers } from '../../utils/fetch'; const uploadQueue = {}; @@ -78,6 +79,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) { xhr.setRequestHeader('X-Auth-Token', token); xhr.setRequestHeader('X-User-Id', id); + xhr.setRequestHeader('User-Agent', headers['User-Agent']); xhr.upload.onprogress = async({ total, loaded }) => { try { diff --git a/app/lib/methods/sendMessage.js b/app/lib/methods/sendMessage.js index 5b7f051ca..7b9e341ec 100644 --- a/app/lib/methods/sendMessage.js +++ b/app/lib/methods/sendMessage.js @@ -5,81 +5,156 @@ import database from '../database'; import log from '../../utils/log'; import random from '../../utils/random'; -export const getMessage = async(rid, msg = '', tmid, user) => { - const _id = random(17); - const { id, username } = user; - try { - const db = database.active; - const msgCollection = db.collections.get('messages'); - let message; - await db.action(async() => { - message = await msgCollection.create((m) => { - m._raw = sanitizedRaw({ id: _id }, msgCollection.schema); - m.subscription.id = rid; - m.msg = msg; - m.tmid = tmid; - m.ts = new Date(); - m._updatedAt = new Date(); - m.status = messagesStatus.TEMP; - m.u = { - _id: id || '1', - username - }; - }); - }); - return message; - } catch (error) { - console.warn('getMessage', error); - } -}; - export async function sendMessageCall(message) { const { id: _id, subscription: { id: rid }, msg, tmid } = message; - // RC 0.60.0 - const data = await this.sdk.post('chat.sendMessage', { - message: { - _id, rid, msg, tmid + try { + const sdk = this.shareSDK || this.sdk; + // RC 0.60.0 + await sdk.post('chat.sendMessage', { + message: { + _id, rid, msg, tmid + } + }); + } catch (e) { + const db = database.active; + const msgCollection = db.collections.get('messages'); + const threadMessagesCollection = db.collections.get('thread_messages'); + const errorBatch = []; + const messageRecord = await msgCollection.find(_id); + errorBatch.push( + messageRecord.prepareUpdate((m) => { + m.status = messagesStatus.ERROR; + }) + ); + + if (tmid) { + const threadMessageRecord = await threadMessagesCollection.find(_id); + errorBatch.push( + threadMessageRecord.prepareUpdate((tm) => { + tm.status = messagesStatus.ERROR; + }) + ); } - }); - return data; + + await db.action(async() => { + await db.batch(...errorBatch); + }); + } } export default async function(rid, msg, tmid, user) { try { const db = database.active; - const subsCollections = db.collections.get('subscriptions'); - const message = await getMessage(rid, msg, tmid, user); - if (!message) { - return; + const subsCollection = db.collections.get('subscriptions'); + const msgCollection = db.collections.get('messages'); + const threadCollection = db.collections.get('threads'); + const threadMessagesCollection = db.collections.get('thread_messages'); + const messageId = random(17); + const batch = []; + const message = { + id: messageId, subscription: { id: rid }, msg, tmid + }; + const messageDate = new Date(); + let tMessageRecord; + + // If it's replying to a thread + if (tmid) { + try { + // Find thread message header in Messages collection + tMessageRecord = await msgCollection.find(tmid); + batch.push( + tMessageRecord.prepareUpdate((m) => { + m.tlm = messageDate; + m.tcount += 1; + }) + ); + + try { + // Find thread message header in Threads collection + await threadCollection.find(tmid); + } catch (error) { + // If there's no record, create one + batch.push( + threadCollection.prepareCreate((tm) => { + tm._raw = sanitizedRaw({ id: tmid }, threadCollection.schema); + tm.subscription.id = rid; + tm.tmid = tmid; + tm.msg = tMessageRecord.msg; + tm.ts = tMessageRecord.ts; + tm._updatedAt = messageDate; + tm.status = messagesStatus.SENT; // Original message was sent already + tm.u = tMessageRecord.u; + }) + ); + } + + // Create the message sent in ThreadMessages collection + batch.push( + threadMessagesCollection.prepareCreate((tm) => { + tm._raw = sanitizedRaw({ id: messageId }, threadMessagesCollection.schema); + tm.subscription.id = rid; + tm.rid = tmid; + tm.msg = msg; + tm.ts = messageDate; + tm._updatedAt = messageDate; + tm.status = messagesStatus.TEMP; + tm.u = { + _id: user.id || '1', + username: user.username + }; + }) + ); + } catch (e) { + log(e); + } } + // Create the message sent in Messages collection + batch.push( + msgCollection.prepareCreate((m) => { + m._raw = sanitizedRaw({ id: messageId }, msgCollection.schema); + m.subscription.id = rid; + m.msg = msg; + m.ts = messageDate; + m._updatedAt = messageDate; + m.status = messagesStatus.TEMP; + m.u = { + _id: user.id || '1', + username: user.username + }; + if (tmid) { + m.tmid = tmid; + m.tlm = messageDate; + m.tmsg = tMessageRecord.msg; + } + }) + ); + try { - const room = await subsCollections.find(rid); - await db.action(async() => { - await room.update((r) => { - r.draftMessage = null; - }); - }); + const room = await subsCollection.find(rid); + if (room.draftMessage) { + batch.push( + room.prepareUpdate((r) => { + r.draftMessage = null; + }) + ); + } } catch (e) { // Do nothing } try { - await sendMessageCall.call(this, message); await db.action(async() => { - await message.update((m) => { - m.status = messagesStatus.SENT; - }); + await db.batch(...batch); }); } catch (e) { - await db.action(async() => { - await message.update((m) => { - m.status = messagesStatus.ERROR; - }); - }); + log(e); + return; } + + await sendMessageCall.call(this, message); } catch (e) { log(e); } diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js index 1a3de160b..46e3bba18 100644 --- a/app/lib/methods/subscriptions/room.js +++ b/app/lib/methods/subscriptions/room.js @@ -8,6 +8,7 @@ import buildMessage from '../helpers/buildMessage'; import database from '../../database'; import reduxStore from '../../createStore'; import { addUserTyping, removeUserTyping, clearUserTyping } from '../../../actions/usersTyping'; +import debounce from '../../../utils/debounce'; const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom'))); const removeListener = listener => listener.stop(); @@ -85,6 +86,10 @@ export default function subscribeRoom({ rid }) { } }); + const read = debounce((lastOpen) => { + this.readMessages(rid, lastOpen); + }, 300); + const handleMessageReceived = protectedFunction((ddpMessage) => { const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0])); const lastOpen = new Date(); @@ -94,20 +99,29 @@ export default function subscribeRoom({ rid }) { InteractionManager.runAfterInteractions(async() => { const db = database.active; const batch = []; - const subCollection = db.collections.get('subscriptions'); const msgCollection = db.collections.get('messages'); const threadsCollection = db.collections.get('threads'); const threadMessagesCollection = db.collections.get('thread_messages'); + let messageRecord; + let threadRecord; + let threadMessageRecord; // Create or update message try { - const messageRecord = await msgCollection.find(message._id); - batch.push( - messageRecord.prepareUpdate((m) => { - Object.assign(m, message); - }) - ); + messageRecord = await msgCollection.find(message._id); } catch (error) { + // Do nothing + } + if (messageRecord) { + try { + const update = messageRecord.prepareUpdate((m) => { + Object.assign(m, message); + }); + batch.push(update); + } catch (e) { + console.log(e); + } + } else { batch.push( msgCollection.prepareCreate(protectedFunction((m) => { m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema); @@ -120,13 +134,18 @@ export default function subscribeRoom({ rid }) { // Create or update thread if (message.tlm) { try { - const threadRecord = await threadsCollection.find(message._id); - batch.push( - threadRecord.prepareUpdate((t) => { - Object.assign(t, message); - }) - ); + threadRecord = await threadsCollection.find(message._id); } catch (error) { + // Do nothing + } + + if (threadRecord) { + batch.push( + threadRecord.prepareUpdate(protectedFunction((t) => { + Object.assign(t, message); + })) + ); + } else { batch.push( threadsCollection.prepareCreate(protectedFunction((t) => { t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema); @@ -140,15 +159,20 @@ export default function subscribeRoom({ rid }) { // Create or update thread message if (message.tmid) { try { - const threadMessageRecord = await threadMessagesCollection.find(message._id); + threadMessageRecord = await threadMessagesCollection.find(message._id); + } catch (error) { + // Do nothing + } + + if (threadMessageRecord) { batch.push( - threadMessageRecord.prepareUpdate((tm) => { + threadMessageRecord.prepareUpdate(protectedFunction((tm) => { Object.assign(tm, message); tm.rid = message.tmid; delete tm.tmid; - }) + })) ); - } catch (error) { + } else { batch.push( threadMessagesCollection.prepareCreate(protectedFunction((tm) => { tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema); @@ -161,12 +185,7 @@ export default function subscribeRoom({ rid }) { } } - try { - await subCollection.find(rid); - this.readMessages(rid, lastOpen); - } catch (e) { - console.log('Subscription not found. We probably subscribed to a not joined channel. No need to mark as read.'); - } + read(lastOpen); try { await db.action(async() => { diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js index c6b940c9e..91be40fc9 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.js @@ -66,10 +66,10 @@ const createOrUpdateSubscription = async(subscription, room) => { } catch (error) { try { await db.action(async() => { - await roomsCollection.create((r) => { + await roomsCollection.create(protectedFunction((r) => { r._raw = sanitizedRaw({ id: room._id }, roomsCollection.schema); Object.assign(r, room); - }); + })); }); } catch (e) { // Do nothing @@ -96,20 +96,66 @@ const createOrUpdateSubscription = async(subscription, room) => { const tmp = merge(subscription, room); await db.action(async() => { + let sub; try { - const sub = await subCollection.find(tmp.rid); - await sub.update((s) => { - Object.assign(s, tmp); - }); + sub = await subCollection.find(tmp.rid); } catch (error) { - await subCollection.create((s) => { - s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema); - Object.assign(s, tmp); - if (s.roomUpdatedAt) { - s.roomUpdatedAt = new Date(); - } - }); + // Do nothing } + + const batch = []; + if (sub) { + try { + const update = sub.prepareUpdate((s) => { + Object.assign(s, tmp); + }); + batch.push(update); + } catch (e) { + console.log(e); + } + } else { + try { + const create = subCollection.prepareCreate((s) => { + s._raw = sanitizedRaw({ id: tmp.rid }, subCollection.schema); + Object.assign(s, tmp); + if (s.roomUpdatedAt) { + s.roomUpdatedAt = new Date(); + } + }); + batch.push(create); + } catch (e) { + console.log(e); + } + } + + // if (tmp.lastMessage) { + // const lastMessage = buildMessage(tmp.lastMessage); + // const messagesCollection = db.collections.get('messages'); + // let messageRecord; + // try { + // messageRecord = await messagesCollection.find(lastMessage._id); + // } catch (error) { + // // Do nothing + // } + + // if (messageRecord) { + // batch.push( + // messageRecord.prepareUpdate(() => { + // Object.assign(messageRecord, lastMessage); + // }) + // ); + // } else { + // batch.push( + // messagesCollection.prepareCreate((m) => { + // m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema); + // m.subscription.id = lastMessage.rid; + // return Object.assign(m, lastMessage); + // }) + // ); + // } + // } + + await db.batch(...batch); }); } catch (e) { log(e); @@ -149,7 +195,7 @@ export default function subscribeRooms() { sub.prepareDestroyPermanently(), ...messagesToDelete, ...threadsToDelete, - ...threadMessagesToDelete, + ...threadMessagesToDelete ); }); } catch (e) { diff --git a/app/lib/methods/updateMessages.js b/app/lib/methods/updateMessages.js index c19bbf746..77a0ced43 100644 --- a/app/lib/methods/updateMessages.js +++ b/app/lib/methods/updateMessages.js @@ -6,7 +6,7 @@ import log from '../../utils/log'; import database from '../database'; import protectedFunction from './helpers/protectedFunction'; -export default function updateMessages({ rid, update, remove }) { +export default function updateMessages({ rid, update = [], remove = [] }) { try { if (!((update && update.length) || (remove && remove.length))) { return; @@ -22,7 +22,7 @@ export default function updateMessages({ rid, update, remove }) { console.log('updateMessages: subscription not found'); } - const messagesIds = update.map(m => m._id); + const messagesIds = [...update.map(m => m._id), ...remove.map(m => m._id)]; const msgCollection = db.collections.get('messages'); const threadCollection = db.collections.get('threads'); const threadMessagesCollection = db.collections.get('thread_messages'); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index e62548457..fb79d6b45 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,6 +1,6 @@ import { AsyncStorage, InteractionManager } from 'react-native'; import semver from 'semver'; -import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; +import { Rocketchat as RocketchatClient, settings as RocketChatSettings } from '@rocket.chat/sdk'; import RNUserDefaults from 'rn-user-defaults'; import { Q } from '@nozbe/watermelondb'; import * as FileSystem from 'expo-file-system'; @@ -12,10 +12,9 @@ import database from './database'; import log from '../utils/log'; import { isIOS, getBundleId } from '../utils/deviceInfo'; import { extractHostname } from '../utils/server'; +import fetch, { headers } from '../utils/fetch'; -import { - setUser, setLoginServices, loginRequest, loginFailure, logout -} from '../actions/login'; +import { setUser, setLoginServices, loginRequest } from '../actions/login'; import { disconnect, connectSuccess, connectRequest } from '../actions/connect'; import { shareSelectServer, shareSetUser @@ -26,7 +25,7 @@ import subscribeRoom from './methods/subscriptions/room'; import protectedFunction from './methods/helpers/protectedFunction'; import readMessages from './methods/readMessages'; -import getSettings from './methods/getSettings'; +import getSettings, { setSettings } from './methods/getSettings'; import getRooms from './methods/getRooms'; import getPermissions from './methods/getPermissions'; @@ -39,7 +38,7 @@ import loadMessagesForRoom from './methods/loadMessagesForRoom'; import loadMissedMessages from './methods/loadMissedMessages'; import loadThreadMessages from './methods/loadThreadMessages'; -import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage'; +import sendMessage, { sendMessageCall } from './methods/sendMessage'; import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage'; import callJitsi from './methods/callJitsi'; @@ -47,16 +46,20 @@ import callJitsi from './methods/callJitsi'; import { getDeviceToken } from '../notifications/push'; import { SERVERS, SERVER_URL } from '../constants/userDefaults'; import { setActiveUsers } from '../actions/activeUsers'; +import I18n from '../i18n'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; export const MARKDOWN_KEY = 'RC_MARKDOWN_KEY'; +export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY'; export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY'; const returnAnArray = obj => obj || []; const MIN_ROCKETCHAT_VERSION = '0.70.0'; const STATUSES = ['offline', 'online', 'away', 'busy']; +RocketChatSettings.customHeaders = headers; + const RocketChat = { TOKEN_KEY, callJitsi, @@ -85,9 +88,53 @@ const RocketChat = { console.warn(`RNUserDefaults error: ${ error.message }`); } }, - async getServerInfo(server) { + async getWebsocketInfo({ server }) { + // Use useSsl: false only if server url starts with http:// + const useSsl = !/http:\/\//.test(server); + + const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl }); + try { - const result = await fetch(`${ server }/api/info`).then(response => response.json()); + await sdk.connect(); + } catch (err) { + if (err.message && err.message.includes('400')) { + return { + success: false, + message: 'Websocket_disabled', + messageOptions: { + contact: I18n.t('Contact_your_server_admin') + } + }; + } + } + + sdk.disconnect(); + + return { + success: true + }; + }, + async getServerInfo(server) { + const notRCServer = { + success: false, + message: 'Not_RC_Server', + messageOptions: { + contact: I18n.t('Contact_your_server_admin') + } + }; + try { + const result = await fetch(`${ server }/api/info`).then(async(response) => { + let res = notRCServer; + try { + res = await response.json(); + if (!(res && res.success)) { + return notRCServer; + } + } catch (e) { + // do nothing + } + return res; + }); if (result.success) { if (semver.lt(result.version, MIN_ROCKETCHAT_VERSION)) { return { @@ -99,20 +146,23 @@ const RocketChat = { } }; } - return result; } + return result; } catch (e) { log(e); } return { success: false, - message: 'The_URL_is_invalid' + message: 'The_URL_is_invalid', + messageOptions: { + contact: I18n.t('Contact_your_server_admin') + } }; }, stopListener(listener) { return listener && listener.stop(); }, - connect({ server, user }) { + connect({ server, user, logoutOnError = false }) { return new Promise((resolve) => { if (!this.sdk || this.sdk.client.host !== server) { database.setActiveDB(server); @@ -157,7 +207,7 @@ const RocketChat = { this.sdk.connect() .then(() => { if (user && user.token) { - reduxStore.dispatch(loginRequest({ resume: user.token })); + reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); } }) .catch((err) => { @@ -209,17 +259,17 @@ const RocketChat = { }, async shareExtensionInit(server) { - database.setActiveDB(server); + database.setShareDB(server); - if (this.sdk) { - this.sdk.disconnect(); - this.sdk = null; + if (this.shareSDK) { + this.shareSDK.disconnect(); + this.shareSDK = null; } // Use useSsl: false only if server url starts with http:// const useSsl = !/http:\/\//.test(server); - this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl }); + this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl }); // set Server const serversDB = database.servers; @@ -248,6 +298,13 @@ const RocketChat = { log(e); } }, + closeShareExtension() { + if (this.shareSDK) { + this.shareSDK.disconnect(); + this.shareSDK = null; + } + database.share = null; + }, updateJitsiTimeout(rid) { return this.sdk.methodCall('jitsi:updateTimeout', rid); @@ -281,7 +338,8 @@ const RocketChat = { }; } else if (state.settings.CROWD_Enable) { params = { - ...params, + username: user, + crowdPassword: password, crowd: true }; } @@ -312,9 +370,10 @@ const RocketChat = { async login(params) { try { + const sdk = this.shareSDK || this.sdk; // RC 0.64.0 - await this.sdk.login(params); - const { result } = this.sdk.currentLogin; + await sdk.login(params); + const { result } = sdk.currentLogin; const user = { id: result.userId, token: result.authToken, @@ -328,11 +387,6 @@ const RocketChat = { }; return user; } catch (e) { - if (e.data && e.data.message && /you've been logged out by the server/i.test(e.data.message)) { - reduxStore.dispatch(logout({ server: this.sdk.client.host })); - } else { - reduxStore.dispatch(loginFailure(e)); - } throw e; } }, @@ -378,8 +432,13 @@ const RocketChat = { const serversDB = database.servers; await serversDB.action(async() => { const usersCollection = serversDB.collections.get('users'); - const user = await usersCollection.find(userId); - await user.destroyPermanently(); + const userRecord = await usersCollection.find(userId); + const serverCollection = serversDB.collections.get('servers'); + const serverRecord = await serverCollection.find(server); + await serversDB.batch( + userRecord.prepareDestroyPermanently(), + serverRecord.prepareDestroyPermanently() + ); }); } catch (error) { // Do nothing @@ -427,11 +486,10 @@ const RocketChat = { loadMissedMessages, loadMessagesForRoom, loadThreadMessages, - getMessage, sendMessage, getRooms, readMessages, - async resendMessage(message) { + async resendMessage(message, tmid) { const db = database.active; try { await db.action(async() => { @@ -439,17 +497,20 @@ const RocketChat = { m.status = messagesStatus.TEMP; }); }); - await sendMessageCall.call(this, message); - } catch (error) { - try { - await db.action(async() => { - await message.update((m) => { - m.status = messagesStatus.ERROR; - }); - }); - } catch (e) { - log(e); + let m = { + id: message.id, + msg: message.msg, + subscription: { id: message.subscription.id } + }; + if (tmid) { + m = { + ...m, + tmid + }; } + await sendMessageCall.call(this, m); + } catch (e) { + log(e); } }, @@ -527,6 +588,7 @@ const RocketChat = { cancelUpload, isUploadActive, getSettings, + setSettings, getPermissions, getCustomEmojis, setCustomEmojis, @@ -971,7 +1033,7 @@ const RocketChat = { const serverVersion = reduxStore.getState().server.version; // if server is lower than 1.1.0 - if (semver.lt(semver.coerce(serverVersion), '1.1.0')) { + if (serverVersion && semver.lt(semver.coerce(serverVersion), '1.1.0')) { if (this.activeUsersSubTimeout) { clearTimeout(this.activeUsersSubTimeout); this.activeUsersSubTimeout = false; diff --git a/app/notifications/inApp/index.js b/app/notifications/inApp/index.js index 7e99903af..c03f2093d 100644 --- a/app/notifications/inApp/index.js +++ b/app/notifications/inApp/index.js @@ -8,13 +8,14 @@ import equal from 'deep-equal'; import { responsive } from 'react-native-responsive-ui'; import Touchable from 'react-native-platform-touchable'; -import { isNotch, isIOS } from '../../utils/deviceInfo'; +import { isNotch, isIOS, isTablet } from '../../utils/deviceInfo'; import { CustomIcon } from '../../lib/Icons'; -import { COLOR_BACKGROUND_NOTIFICATION, COLOR_SEPARATOR, COLOR_TEXT } from '../../constants/colors'; +import { themes } from '../../constants/colors'; import Avatar from '../../containers/Avatar'; import { removeNotification as removeNotificationAction } from '../../actions/notification'; import sharedStyles from '../../views/Styles'; import { ROW_HEIGHT } from '../../presentation/RoomItem'; +import { withTheme } from '../../theme'; const AVATAR_SIZE = 48; const ANIMATION_DURATION = 300; @@ -38,33 +39,31 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', position: 'absolute', zIndex: 2, - backgroundColor: COLOR_BACKGROUND_NOTIFICATION, width: '100%', - borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: COLOR_SEPARATOR + borderBottomWidth: StyleSheet.hairlineWidth }, content: { flex: 1, flexDirection: 'row', alignItems: 'center' }, + inner: { + flex: 1 + }, avatar: { marginRight: 10 }, roomName: { fontSize: 17, lineHeight: 20, - ...sharedStyles.textColorNormal, ...sharedStyles.textMedium }, message: { fontSize: 14, lineHeight: 17, - ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal + ...sharedStyles.textRegular }, close: { - color: COLOR_TEXT, marginLeft: 10 } }); @@ -77,7 +76,8 @@ class NotificationBadge extends React.Component { userId: PropTypes.string, notification: PropTypes.object, window: PropTypes.object, - removeNotification: PropTypes.func + removeNotification: PropTypes.func, + theme: PropTypes.string } constructor(props) { @@ -88,8 +88,11 @@ class NotificationBadge extends React.Component { shouldComponentUpdate(nextProps) { const { notification: nextNotification } = nextProps; const { - notification: { payload }, window + notification: { payload }, window, theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (!equal(nextNotification.payload, payload)) { return true; } @@ -120,7 +123,7 @@ class NotificationBadge extends React.Component { { toValue: 1, ...ANIMATION_PROPS - }, + } ).start(() => { this.clearTimeout(); this.timeout = setTimeout(() => { @@ -136,7 +139,7 @@ class NotificationBadge extends React.Component { { toValue: 0, ...ANIMATION_PROPS - }, + } ).start(); setTimeout(removeNotification, ANIMATION_DURATION); } @@ -155,26 +158,26 @@ class NotificationBadge extends React.Component { } goToRoom = async() => { - const { notification: { payload }, navigation } = this.props; + const { notification: { payload }, navigation, baseUrl } = this.props; const { rid, type, prid } = payload; if (!rid) { return; } - const name = type === 'p' ? payload.name : payload.sender.username; + const name = type === 'd' ? payload.sender.username : payload.name; await navigation.navigate('RoomsListView'); navigation.navigate('RoomView', { - rid, name, t: type, prid + rid, name, t: type, prid, baseUrl }); this.hide(); } render() { const { - baseUrl, token, userId, notification, window + baseUrl, token, userId, notification, window, theme } = this.props; const { message, payload } = notification; const { type } = payload; - const name = type === 'p' ? payload.name : payload.sender.username; + const name = type === 'd' ? payload.sender.username : payload.name; let top = 0; if (isIOS) { @@ -182,18 +185,25 @@ class NotificationBadge extends React.Component { if (portrait) { top = isNotch ? 45 : 20; } else { - top = 0; + top = isTablet ? 20 : 0; } } - const maxWidthMessage = window.width - 110; - const translateY = this.animatedValue.interpolate({ inputRange: [0, 1], outputRange: [-top - ROW_HEIGHT, top] }); return ( - <Animated.View style={[styles.container, { transform: [{ translateY }] }]}> + <Animated.View + style={[ + styles.container, + { + transform: [{ translateY }], + backgroundColor: themes[theme].focusedBackground, + borderColor: themes[theme].separatorColor + } + ]} + > <Touchable style={styles.content} onPress={this.goToRoom} @@ -202,14 +212,14 @@ class NotificationBadge extends React.Component { > <> <Avatar text={name} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} /> - <View> - <Text style={styles.roomName}>{name}</Text> - <Text style={[styles.message, { maxWidth: maxWidthMessage }]} numberOfLines={1}>{message}</Text> + <View style={styles.inner}> + <Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text> + <Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{message}</Text> </View> </> </Touchable> <TouchableOpacity onPress={this.hide}> - <CustomIcon name='circle-cross' style={styles.close} size={20} /> + <CustomIcon name='circle-cross' style={[styles.close, { color: themes[theme].titleText }]} size={20} /> </TouchableOpacity> </Animated.View> ); @@ -227,4 +237,4 @@ const mapDispatchToProps = dispatch => ({ removeNotification: () => dispatch(removeNotificationAction()) }); -export default responsive(connect(mapStateToProps, mapDispatchToProps)(NotificationBadge)); +export default responsive(connect(mapStateToProps, mapDispatchToProps)(withTheme(NotificationBadge))); diff --git a/app/presentation/DirectoryItem/index.js b/app/presentation/DirectoryItem/index.js index f9c0d8883..c77bc4c26 100644 --- a/app/presentation/DirectoryItem/index.js +++ b/app/presentation/DirectoryItem/index.js @@ -2,25 +2,31 @@ import React from 'react'; import { Text, View } from 'react-native'; import PropTypes from 'prop-types'; -import Avatar from '../../containers/Avatar'; import Touch from '../../utils/touch'; +import Avatar from '../../containers/Avatar'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; import styles, { ROW_HEIGHT } from './styles'; +import { themes } from '../../constants/colors'; export { ROW_HEIGHT }; -const DirectoryItemLabel = React.memo(({ text }) => { +const DirectoryItemLabel = React.memo(({ text, theme }) => { if (!text) { return null; } - return <Text style={styles.directoryItemLabel}>{text}</Text>; + return <Text style={[styles.directoryItemLabel, { color: themes[theme].auxiliaryText }]}>{text}</Text>; }); const DirectoryItem = ({ - title, description, avatar, onPress, testID, style, baseUrl, user, rightLabel, type + title, description, avatar, onPress, testID, style, baseUrl, user, rightLabel, type, theme }) => ( - <Touch onPress={onPress} style={styles.directoryItemButton} testID={testID}> - <View style={[styles.directoryItemContainer, style]}> + <Touch + onPress={onPress} + style={{ backgroundColor: themes[theme].backgroundColor }} + testID={testID} + theme={theme} + > + <View style={[styles.directoryItemContainer, styles.directoryItemButton, style]}> <Avatar text={avatar} size={30} @@ -32,12 +38,12 @@ const DirectoryItem = ({ /> <View style={styles.directoryItemTextContainer}> <View style={styles.directoryItemTextTitle}> - <RoomTypeIcon type={type} /> - <Text style={styles.directoryItemName} numberOfLines={1}>{title}</Text> + <RoomTypeIcon type={type} theme={theme} /> + <Text style={[styles.directoryItemName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text> </View> - { description ? <Text style={styles.directoryItemUsername} numberOfLines={1}>{description}</Text> : null } + { description ? <Text style={[styles.directoryItemUsername, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{description}</Text> : null } </View> - <DirectoryItemLabel text={rightLabel} /> + <DirectoryItemLabel text={rightLabel} theme={theme} /> </View> </Touch> ); @@ -55,11 +61,13 @@ DirectoryItem.propTypes = { onPress: PropTypes.func.isRequired, testID: PropTypes.string.isRequired, style: PropTypes.any, - rightLabel: PropTypes.string + rightLabel: PropTypes.string, + theme: PropTypes.string }; DirectoryItemLabel.propTypes = { - text: PropTypes.string + text: PropTypes.string, + theme: PropTypes.string }; export default DirectoryItem; diff --git a/app/presentation/DirectoryItem/styles.js b/app/presentation/DirectoryItem/styles.js index 0e8382d06..55fa36c47 100644 --- a/app/presentation/DirectoryItem/styles.js +++ b/app/presentation/DirectoryItem/styles.js @@ -1,14 +1,12 @@ import { StyleSheet } from 'react-native'; -import { COLOR_WHITE } from '../../constants/colors'; import sharedStyles from '../../views/Styles'; export const ROW_HEIGHT = 54; export default StyleSheet.create({ directoryItemButton: { - height: ROW_HEIGHT, - backgroundColor: COLOR_WHITE + height: ROW_HEIGHT }, directoryItemContainer: { flex: 1, @@ -32,18 +30,15 @@ export default StyleSheet.create({ directoryItemName: { flex: 1, fontSize: 17, - ...sharedStyles.textMedium, - ...sharedStyles.textColorNormal + ...sharedStyles.textMedium }, directoryItemUsername: { fontSize: 14, - ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription + ...sharedStyles.textRegular }, directoryItemLabel: { fontSize: 14, paddingLeft: 10, - ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription + ...sharedStyles.textRegular } }); diff --git a/app/presentation/RoomItem/Actions.js b/app/presentation/RoomItem/Actions.js index 7bdebcb1b..e0b8c29b2 100644 --- a/app/presentation/RoomItem/Actions.js +++ b/app/presentation/RoomItem/Actions.js @@ -6,9 +6,10 @@ import PropTypes from 'prop-types'; import I18n from '../../i18n'; import styles, { ACTION_WIDTH, LONG_SWIPE } from './styles'; import { CustomIcon } from '../../lib/Icons'; +import { themes } from '../../constants/colors'; export const LeftActions = React.memo(({ - transX, isRead, width, onToggleReadPress + theme, transX, isRead, width, onToggleReadPress }) => { const translateX = transX.interpolate({ inputRange: [0, ACTION_WIDTH], @@ -21,7 +22,7 @@ export const LeftActions = React.memo(({ }); return ( <View - style={styles.actionsContainer} + style={[styles.actionsContainer, styles.actionLeftContainer]} pointerEvents='box-none' > <Animated.View @@ -30,7 +31,8 @@ export const LeftActions = React.memo(({ { right: width - ACTION_WIDTH, width, - transform: [{ translateX }] + transform: [{ translateX }], + backgroundColor: themes[theme].tintColor } ]} > @@ -46,7 +48,7 @@ export const LeftActions = React.memo(({ <RectButton style={styles.actionButton} onPress={onToggleReadPress}> <> <CustomIcon size={20} name={isRead ? 'flag' : 'check'} color='white' /> - <Text style={styles.actionText}>{I18n.t(isRead ? 'Unread' : 'Read')}</Text> + <Text style={[styles.actionText, { color: themes[theme].buttonText }]}>{I18n.t(isRead ? 'Unread' : 'Read')}</Text> </> </RectButton> </Animated.View> @@ -56,7 +58,7 @@ export const LeftActions = React.memo(({ }); export const RightActions = React.memo(({ - transX, favorite, width, toggleFav, onHidePress + transX, favorite, width, toggleFav, onHidePress, theme }) => { const translateXFav = transX.interpolate({ inputRange: [-width / 2, -ACTION_WIDTH * 2, 0], @@ -82,14 +84,15 @@ export const RightActions = React.memo(({ styles.actionRightButtonContainer, { width, - transform: [{ translateX: translateXFav }] + transform: [{ translateX: translateXFav }], + backgroundColor: themes[theme].hideBackground } ]} > - <RectButton style={[styles.actionButton, { backgroundColor: '#ffbb00' }]} onPress={toggleFav}> + <RectButton style={[styles.actionButton, { backgroundColor: themes[theme].favoriteBackground }]} onPress={toggleFav}> <> - <CustomIcon size={20} name={favorite ? 'Star-filled' : 'star'} color='white' /> - <Text style={styles.actionText}>{I18n.t(favorite ? 'Unfavorite' : 'Favorite')}</Text> + <CustomIcon size={20} name={favorite ? 'Star-filled' : 'star'} color={themes[theme].buttonText} /> + <Text style={[styles.actionText, { color: themes[theme].buttonText }]}>{I18n.t(favorite ? 'Unfavorite' : 'Favorite')}</Text> </> </RectButton> </Animated.View> @@ -102,10 +105,10 @@ export const RightActions = React.memo(({ } ]} > - <RectButton style={[styles.actionButton, { backgroundColor: '#54585e' }]} onPress={onHidePress}> + <RectButton style={[styles.actionButton, { backgroundColor: themes[theme].hideBackground }]} onPress={onHidePress}> <> - <CustomIcon size={20} name='eye-off' color='white' /> - <Text style={styles.actionText}>{I18n.t('Hide')}</Text> + <CustomIcon size={20} name='eye-off' color={themes[theme].buttonText} /> + <Text style={[styles.actionText, { color: themes[theme].buttonText }]}>{I18n.t('Hide')}</Text> </> </RectButton> </Animated.View> @@ -114,6 +117,7 @@ export const RightActions = React.memo(({ }); LeftActions.propTypes = { + theme: PropTypes.string, transX: PropTypes.object, isRead: PropTypes.bool, width: PropTypes.number, @@ -121,6 +125,7 @@ LeftActions.propTypes = { }; RightActions.propTypes = { + theme: PropTypes.string, transX: PropTypes.object, favorite: PropTypes.bool, width: PropTypes.number, diff --git a/app/presentation/RoomItem/LastMessage.js b/app/presentation/RoomItem/LastMessage.js index bafd0acc8..52724b469 100644 --- a/app/presentation/RoomItem/LastMessage.js +++ b/app/presentation/RoomItem/LastMessage.js @@ -6,6 +6,7 @@ import _ from 'lodash'; import I18n from '../../i18n'; import styles from './styles'; import Markdown from '../../containers/markdown'; +import { themes } from '../../constants/colors'; const formatMsg = ({ lastMessage, type, showLastMessage, username @@ -45,20 +46,22 @@ const formatMsg = ({ const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps); const LastMessage = React.memo(({ - lastMessage, type, showLastMessage, username, alert + lastMessage, type, showLastMessage, username, alert, theme }) => ( <Markdown msg={formatMsg({ lastMessage, type, showLastMessage, username })} - style={[styles.markdownText, alert && styles.markdownTextAlert]} + style={[styles.markdownText, { color: alert ? themes[theme].bodyText : themes[theme].auxiliaryText }]} customEmojis={false} numberOfLines={2} preview + theme={theme} /> ), arePropsEqual); LastMessage.propTypes = { + theme: PropTypes.string, lastMessage: PropTypes.object, type: PropTypes.string, showLastMessage: PropTypes.bool, diff --git a/app/presentation/RoomItem/Touchable.js b/app/presentation/RoomItem/Touchable.js new file mode 100644 index 000000000..5107a21da --- /dev/null +++ b/app/presentation/RoomItem/Touchable.js @@ -0,0 +1,219 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Animated } from 'react-native'; +import { PanGestureHandler, State } from 'react-native-gesture-handler'; + +import Touch from '../../utils/touch'; +import { + ACTION_WIDTH, + SMALL_SWIPE, + LONG_SWIPE +} from './styles'; +import { themes } from '../../constants/colors'; +import { LeftActions, RightActions } from './Actions'; + +class Touchable extends React.Component { + static propTypes = { + type: PropTypes.string.isRequired, + onPress: PropTypes.func, + testID: PropTypes.string, + width: PropTypes.number, + favorite: PropTypes.bool, + isRead: PropTypes.bool, + rid: PropTypes.string, + toggleFav: PropTypes.func, + toggleRead: PropTypes.func, + hideChannel: PropTypes.func, + children: PropTypes.element, + theme: PropTypes.string + } + + constructor(props) { + super(props); + this.dragX = new Animated.Value(0); + this.rowOffSet = new Animated.Value(0); + this.transX = Animated.add( + this.rowOffSet, + this.dragX + ); + this.state = { + rowState: 0 // 0: closed, 1: right opened, -1: left opened + }; + this._onGestureEvent = Animated.event( + [{ nativeEvent: { translationX: this.dragX } }] + ); + this._value = 0; + } + + _onHandlerStateChange = ({ nativeEvent }) => { + if (nativeEvent.oldState === State.ACTIVE) { + this._handleRelease(nativeEvent); + } + } + + + _handleRelease = (nativeEvent) => { + const { translationX } = nativeEvent; + const { rowState } = this.state; + this._value = this._value + translationX; + + let toValue = 0; + if (rowState === 0) { // if no option is opened + if (translationX > 0 && translationX < LONG_SWIPE) { + toValue = ACTION_WIDTH; // open left option if he swipe right but not enough to trigger action + this.setState({ rowState: -1 }); + } else if (translationX >= LONG_SWIPE) { + toValue = 0; + this.toggleRead(); + } else if (translationX < 0 && translationX > -LONG_SWIPE) { + toValue = -2 * ACTION_WIDTH; // open right option if he swipe left + this.setState({ rowState: 1 }); + } else if (translationX <= -LONG_SWIPE) { + toValue = 0; + this.setState({ rowState: 0 }); + this.hideChannel(); + } else { + toValue = 0; + } + } + + if (rowState === -1) { // if left option is opened + if (this._value < SMALL_SWIPE) { + toValue = 0; + this.setState({ rowState: 0 }); + } else if (this._value > LONG_SWIPE) { + toValue = 0; + this.setState({ rowState: 0 }); + this.toggleRead(); + } else { + toValue = ACTION_WIDTH; + } + } + + if (rowState === 1) { // if right option is opened + if (this._value > -2 * SMALL_SWIPE) { + toValue = 0; + this.setState({ rowState: 0 }); + } else if (this._value < -LONG_SWIPE) { + toValue = 0; + this.setState({ rowState: 0 }); + this.hideChannel(); + } else { + toValue = -2 * ACTION_WIDTH; + } + } + this._animateRow(toValue); + } + + _animateRow = (toValue) => { + this.rowOffSet.setValue(this._value); + this._value = toValue; + this.dragX.setValue(0); + Animated.spring(this.rowOffSet, { + toValue, + bounciness: 0, + useNativeDriver: true + }).start(); + } + + close = () => { + this.setState({ rowState: 0 }); + this._animateRow(0); + } + + toggleFav = () => { + const { toggleFav, rid, favorite } = this.props; + if (toggleFav) { + toggleFav(rid, favorite); + } + this.close(); + }; + + toggleRead = () => { + const { toggleRead, rid, isRead } = this.props; + if (toggleRead) { + toggleRead(rid, isRead); + } + }; + + hideChannel = () => { + const { hideChannel, rid, type } = this.props; + if (hideChannel) { + hideChannel(rid, type); + } + }; + + onToggleReadPress = () => { + this.toggleRead(); + this.close(); + }; + + onHidePress = () => { + this.hideChannel(); + this.close(); + }; + + onPress = () => { + const { rowState } = this.state; + if (rowState !== 0) { + this.close(); + return; + } + const { onPress } = this.props; + if (onPress) { + onPress(); + } + }; + + render() { + const { + testID, isRead, width, favorite, children, theme + } = this.props; + + return ( + + <PanGestureHandler + minDeltaX={20} + onGestureEvent={this._onGestureEvent} + onHandlerStateChange={this._onHandlerStateChange} + > + <Animated.View> + <LeftActions + transX={this.transX} + isRead={isRead} + width={width} + onToggleReadPress={this.onToggleReadPress} + theme={theme} + /> + <RightActions + transX={this.transX} + favorite={favorite} + width={width} + toggleFav={this.toggleFav} + onHidePress={this.onHidePress} + theme={theme} + /> + <Animated.View + style={{ + transform: [{ translateX: this.transX }] + }} + > + <Touch + onPress={this.onPress} + theme={theme} + testID={testID} + style={{ + backgroundColor: themes[theme].backgroundColor + }} + > + {children} + </Touch> + </Animated.View> + </Animated.View> + + </PanGestureHandler> + ); + } +} + +export default Touchable; diff --git a/app/presentation/RoomItem/TypeIcon.js b/app/presentation/RoomItem/TypeIcon.js index 4be82c934..0f8079427 100644 --- a/app/presentation/RoomItem/TypeIcon.js +++ b/app/presentation/RoomItem/TypeIcon.js @@ -5,14 +5,17 @@ import Status from '../../containers/Status/Status'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; import styles from './styles'; -const TypeIcon = React.memo(({ type, prid, status }) => { +const TypeIcon = React.memo(({ + theme, type, prid, status +}) => { if (type === 'd') { return <Status style={styles.status} size={10} status={status} />; } - return <RoomTypeIcon type={prid ? 'discussion' : type} />; + return <RoomTypeIcon theme={theme} type={prid ? 'discussion' : type} />; }); TypeIcon.propTypes = { + theme: PropTypes.string, type: PropTypes.string, status: PropTypes.string, prid: PropTypes.string diff --git a/app/presentation/RoomItem/UnreadBadge.js b/app/presentation/RoomItem/UnreadBadge.js index eb6a72bad..af745f811 100644 --- a/app/presentation/RoomItem/UnreadBadge.js +++ b/app/presentation/RoomItem/UnreadBadge.js @@ -3,8 +3,11 @@ import PropTypes from 'prop-types'; import { View, Text } from 'react-native'; import styles from './styles'; +import { themes } from '../../constants/colors'; -const UnreadBadge = React.memo(({ unread, userMentions, type }) => { +const UnreadBadge = React.memo(({ + theme, unread, userMentions, type +}) => { if (!unread || unread <= 0) { return; } @@ -14,13 +17,25 @@ const UnreadBadge = React.memo(({ unread, userMentions, type }) => { const mentioned = userMentions > 0 && type !== 'd'; return ( - <View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentionedContainer]}> - <Text style={[styles.unreadText, mentioned && styles.unreadMentionedText]}>{ unread }</Text> + <View + style={[ + styles.unreadNumberContainer, + { backgroundColor: mentioned ? themes[theme].tintColor : themes[theme].borderColor } + ]} + > + <Text + style={[ + styles.unreadText, + { color: mentioned ? themes[theme].buttonText : themes[theme].bodyText } + ]} + >{ unread } + </Text> </View> ); }); UnreadBadge.propTypes = { + theme: PropTypes.string, unread: PropTypes.number, userMentions: PropTypes.number, type: PropTypes.string diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js index f0e05a3b1..898df036e 100644 --- a/app/presentation/RoomItem/index.js +++ b/app/presentation/RoomItem/index.js @@ -1,26 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, Text, Animated } from 'react-native'; -import { - RectButton, - PanGestureHandler, - State -} from 'react-native-gesture-handler'; +import { View, Text } from 'react-native'; import { connect } from 'react-redux'; import Avatar from '../../containers/Avatar'; import I18n from '../../i18n'; import styles, { - ROW_HEIGHT, - ACTION_WIDTH, - SMALL_SWIPE, - LONG_SWIPE + ROW_HEIGHT } from './styles'; import UnreadBadge from './UnreadBadge'; import TypeIcon from './TypeIcon'; import LastMessage from './LastMessage'; import { capitalize, formatDate } from '../../utils/room'; -import { LeftActions, RightActions } from './Actions'; +import Touchable from './Touchable'; +import { themes } from '../../constants/colors'; export { ROW_HEIGHT }; @@ -34,312 +27,181 @@ const attrs = [ 'width', 'isRead', 'favorite', - 'status' + 'status', + 'theme' ]; -class RoomItem extends React.Component { - static propTypes = { - type: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - baseUrl: PropTypes.string.isRequired, - showLastMessage: PropTypes.bool, - _updatedAt: PropTypes.string, - lastMessage: PropTypes.object, - alert: PropTypes.bool, - unread: PropTypes.number, - userMentions: PropTypes.number, - id: PropTypes.string, - prid: PropTypes.string, - onPress: PropTypes.func, - userId: PropTypes.string, - username: PropTypes.string, - token: PropTypes.string, - avatarSize: PropTypes.number, - testID: PropTypes.string, - width: PropTypes.number, - favorite: PropTypes.bool, - isRead: PropTypes.bool, - rid: PropTypes.string, - status: PropTypes.string, - toggleFav: PropTypes.func, - toggleRead: PropTypes.func, - hideChannel: PropTypes.func, - avatar: PropTypes.bool +const arePropsEqual = (oldProps, newProps) => { + const { _updatedAt: _updatedAtOld } = oldProps; + const { _updatedAt: _updatedAtNew } = newProps; + if (_updatedAtOld && _updatedAtNew && _updatedAtOld.toISOString() !== _updatedAtNew.toISOString()) { + return false; + } + return attrs.every(key => oldProps[key] === newProps[key]); +}; + +const RoomItem = React.memo(({ + onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, theme +}) => { + const date = formatDate(_updatedAt); + + let accessibilityLabel = name; + if (unread === 1) { + accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`; + } else if (unread > 1) { + accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`; } - static defaultProps = { - avatarSize: 48 - }; - - constructor(props) { - super(props); - this.dragX = new Animated.Value(0); - this.rowOffSet = new Animated.Value(0); - this.transX = Animated.add( - this.rowOffSet, - this.dragX - ); - this.state = { - rowState: 0 // 0: closed, 1: right opened, -1: left opened - }; - this._onGestureEvent = Animated.event( - [{ nativeEvent: { translationX: this.dragX } }] - ); - this._value = 0; + if (userMentions > 0) { + accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`; } - shouldComponentUpdate(nextProps) { - const { _updatedAt } = this.props; - if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt.toISOString() !== _updatedAt.toISOString()) { - return true; - } - // eslint-disable-next-line react/destructuring-assignment - return attrs.some(key => nextProps[key] !== this.props[key]); + if (date) { + accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`; } - _onHandlerStateChange = ({ nativeEvent }) => { - if (nativeEvent.oldState === State.ACTIVE) { - this._handleRelease(nativeEvent); - } - }; - - _handleRelease = (nativeEvent) => { - const { translationX } = nativeEvent; - const { rowState } = this.state; - this._value = this._value + translationX; - - let toValue = 0; - if (rowState === 0) { // if no option is opened - if (translationX > 0 && translationX < LONG_SWIPE) { - toValue = ACTION_WIDTH; // open left option if he swipe right but not enough to trigger action - this.setState({ rowState: -1 }); - } else if (translationX >= LONG_SWIPE) { - toValue = 0; - this.toggleRead(); - } else if (translationX < 0 && translationX > -LONG_SWIPE) { - toValue = -2 * ACTION_WIDTH; // open right option if he swipe left - this.setState({ rowState: 1 }); - } else if (translationX <= -LONG_SWIPE) { - toValue = 0; - this.setState({ rowState: 0 }); - this.hideChannel(); - } else { - toValue = 0; - } - } - - if (rowState === -1) { // if left option is opened - if (this._value < SMALL_SWIPE) { - toValue = 0; - this.setState({ rowState: 0 }); - } else if (this._value > LONG_SWIPE) { - toValue = 0; - this.setState({ rowState: 0 }); - this.toggleRead(); - } else { - toValue = ACTION_WIDTH; - } - } - - if (rowState === 1) { // if right option is opened - if (this._value > -2 * SMALL_SWIPE) { - toValue = 0; - this.setState({ rowState: 0 }); - } else if (this._value < -LONG_SWIPE) { - toValue = 0; - this.setState({ rowState: 0 }); - this.hideChannel(); - } else { - toValue = -2 * ACTION_WIDTH; - } - } - this._animateRow(toValue); - } - - _animateRow = (toValue) => { - this.rowOffSet.setValue(this._value); - this._value = toValue; - this.dragX.setValue(0); - Animated.spring(this.rowOffSet, { - toValue, - bounciness: 0, - useNativeDriver: true - }).start(); - } - - close = () => { - this.setState({ rowState: 0 }); - this._animateRow(0); - } - - toggleFav = () => { - const { toggleFav, rid, favorite } = this.props; - if (toggleFav) { - toggleFav(rid, favorite); - } - this.close(); - }; - - toggleRead = () => { - const { toggleRead, rid, isRead } = this.props; - if (toggleRead) { - toggleRead(rid, isRead); - } - }; - - hideChannel = () => { - const { hideChannel, rid, type } = this.props; - if (hideChannel) { - hideChannel(rid, type); - } - }; - - onToggleReadPress = () => { - this.toggleRead(); - this.close(); - }; - - onHidePress = () => { - this.hideChannel(); - this.close(); - }; - - onPress = () => { - const { rowState } = this.state; - if (rowState !== 0) { - this.close(); - return; - } - const { onPress } = this.props; - if (onPress) { - onPress(); - } - }; - - render() { - const { - unread, userMentions, name, _updatedAt, alert, testID, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage, isRead, width, favorite, status, avatar - } = this.props; - - const date = formatDate(_updatedAt); - - let accessibilityLabel = name; - if (unread === 1) { - accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`; - } else if (unread > 1) { - accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`; - } - - if (userMentions > 0) { - accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`; - } - - if (date) { - accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`; - } - - return ( - <PanGestureHandler - minDeltaX={20} - onGestureEvent={this._onGestureEvent} - onHandlerStateChange={this._onHandlerStateChange} + return ( + <Touchable + onPress={onPress} + width={width} + favorite={favorite} + toggleFav={toggleFav} + isRead={isRead} + rid={rid} + toggleRead={toggleRead} + hideChannel={hideChannel} + testID={testID} + type={type} + theme={theme} + > + <View + style={styles.container} + accessibilityLabel={accessibilityLabel} > - <Animated.View> - <LeftActions - transX={this.transX} - isRead={isRead} - width={width} - onToggleReadPress={this.onToggleReadPress} - /> - <RightActions - transX={this.transX} - favorite={favorite} - width={width} - toggleFav={this.toggleFav} - onHidePress={this.onHidePress} - /> - <Animated.View - style={{ - transform: [{ translateX: this.transX }] - }} - > - <RectButton - onPress={this.onPress} - activeOpacity={0.8} - underlayColor='#e1e5e8' - testID={testID} - style={styles.button} + <Avatar + text={avatar} + size={avatarSize} + type={type} + baseUrl={baseUrl} + style={styles.avatar} + userId={userId} + token={token} + /> + <View + style={[ + styles.centerContainer, + { + borderColor: themes[theme].separatorColor + } + ]} + > + <View style={styles.titleContainer}> + <TypeIcon + type={type} + id={id} + prid={prid} + status={status} + theme={theme} + /> + <Text + style={[ + styles.title, + alert && !hideUnreadStatus && styles.alert, + { color: themes[theme].titleText } + ]} + ellipsizeMode='tail' + numberOfLines={1} > - <View - style={styles.container} - accessibilityLabel={accessibilityLabel} + {name} + </Text> + {_updatedAt ? ( + <Text + style={[ + styles.date, + { + color: + themes[theme] + .auxiliaryText + }, + alert && !hideUnreadStatus && [ + styles.updateAlert, + { + color: + themes[theme] + .tintColor + } + ] + ]} + ellipsizeMode='tail' + numberOfLines={1} > - <Avatar - text={avatar} - size={avatarSize} - type={type} - baseUrl={baseUrl} - style={styles.avatar} - userId={userId} - token={token} - /> - <View style={styles.centerContainer}> - <View style={styles.titleContainer}> - <TypeIcon - type={type} - id={id} - prid={prid} - status={status} - /> - <Text - style={[ - styles.title, - alert && styles.alert - ]} - ellipsizeMode='tail' - numberOfLines={1} - > - {name} - </Text> - {_updatedAt ? ( - <Text - style={[ - styles.date, - alert && styles.updateAlert - ]} - ellipsizeMode='tail' - numberOfLines={1} - > - {capitalize(date)} - </Text> - ) : null} - </View> - <View style={styles.row}> - <LastMessage - lastMessage={lastMessage} - type={type} - showLastMessage={showLastMessage} - username={username} - alert={alert} - /> - <UnreadBadge - unread={unread} - userMentions={userMentions} - type={type} - /> - </View> - </View> - </View> - </RectButton> - </Animated.View> - </Animated.View> - </PanGestureHandler> - ); - } -} + {capitalize(date)} + </Text> + ) : null} + </View> + <View style={styles.row}> + <LastMessage + lastMessage={lastMessage} + type={type} + showLastMessage={showLastMessage} + username={username} + alert={alert && !hideUnreadStatus} + theme={theme} + /> + <UnreadBadge + unread={unread} + userMentions={userMentions} + type={type} + theme={theme} + /> + </View> + </View> + </View> + </Touchable> + ); +}, arePropsEqual); + +RoomItem.propTypes = { + type: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + baseUrl: PropTypes.string.isRequired, + showLastMessage: PropTypes.bool, + _updatedAt: PropTypes.string, + lastMessage: PropTypes.object, + alert: PropTypes.bool, + unread: PropTypes.number, + userMentions: PropTypes.number, + id: PropTypes.string, + prid: PropTypes.string, + onPress: PropTypes.func, + userId: PropTypes.string, + username: PropTypes.string, + token: PropTypes.string, + avatarSize: PropTypes.number, + testID: PropTypes.string, + width: PropTypes.number, + favorite: PropTypes.bool, + isRead: PropTypes.bool, + rid: PropTypes.string, + status: PropTypes.string, + toggleFav: PropTypes.func, + toggleRead: PropTypes.func, + hideChannel: PropTypes.func, + avatar: PropTypes.bool, + hideUnreadStatus: PropTypes.bool, + theme: PropTypes.string +}; + +RoomItem.defaultProps = { + avatarSize: 48, + status: 'offline' +}; const mapStateToProps = (state, ownProps) => ({ - status: state.meteor.connected && ownProps.type === 'd' ? state.activeUsers[ownProps.id] : 'offline' + status: + state.meteor.connected && ownProps.type === 'd' + ? state.activeUsers[ownProps.id] + : 'offline' }); export default connect(mapStateToProps)(RoomItem); diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js index 6ad29fd6d..1a74d19c6 100644 --- a/app/presentation/RoomItem/styles.js +++ b/app/presentation/RoomItem/styles.js @@ -1,9 +1,6 @@ import { StyleSheet, PixelRatio } from 'react-native'; import sharedStyles from '../../views/Styles'; -import { - COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_UNREAD, COLOR_TEXT -} from '../../constants/colors'; export const ROW_HEIGHT = 75 * PixelRatio.getFontScale(); export const ACTION_WIDTH = 80; @@ -17,21 +14,16 @@ export default StyleSheet.create({ paddingLeft: 14, height: ROW_HEIGHT }, - button: { - backgroundColor: COLOR_WHITE - }, centerContainer: { flex: 1, paddingVertical: 10, paddingRight: 14, - borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: COLOR_SEPARATOR + borderBottomWidth: StyleSheet.hairlineWidth }, title: { flex: 1, fontSize: 17, lineHeight: 20, - ...sharedStyles.textColorNormal, ...sharedStyles.textMedium }, alert: { @@ -51,11 +43,9 @@ export default StyleSheet.create({ date: { fontSize: 13, marginLeft: 4, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, updateAlert: { - color: COLOR_PRIMARY, ...sharedStyles.textSemibold }, unreadNumberContainer: { @@ -64,25 +54,17 @@ export default StyleSheet.create({ paddingVertical: 3, paddingHorizontal: 5, borderRadius: 10.5, - backgroundColor: COLOR_UNREAD, alignItems: 'center', justifyContent: 'center', marginLeft: 10 }, - unreadMentionedContainer: { - backgroundColor: COLOR_PRIMARY - }, unreadText: { - color: COLOR_TEXT, overflow: 'hidden', fontSize: 13, ...sharedStyles.textMedium, letterSpacing: 0.56, textAlign: 'center' }, - unreadMentionedText: { - color: COLOR_WHITE - }, status: { marginRight: 7, marginTop: 3 @@ -91,11 +73,7 @@ export default StyleSheet.create({ flex: 1, fontSize: 14, lineHeight: 17, - ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription - }, - markdownTextAlert: { - ...sharedStyles.textColorNormal + ...sharedStyles.textRegular }, avatar: { marginRight: 10 @@ -110,9 +88,7 @@ export default StyleSheet.create({ height: ROW_HEIGHT }, actionText: { - color: COLOR_WHITE, fontSize: 15, - backgroundColor: 'transparent', justifyContent: 'center', marginTop: 4, ...sharedStyles.textSemibold @@ -120,7 +96,6 @@ export default StyleSheet.create({ actionLeftButtonContainer: { position: 'absolute', height: ROW_HEIGHT, - backgroundColor: COLOR_PRIMARY, justifyContent: 'center', top: 0 }, @@ -128,8 +103,7 @@ export default StyleSheet.create({ position: 'absolute', height: ROW_HEIGHT, justifyContent: 'center', - top: 0, - backgroundColor: '#54585e' + top: 0 }, actionButton: { width: ACTION_WIDTH, diff --git a/app/presentation/ServerItem/index.js b/app/presentation/ServerItem/index.js index 1a3d7e25a..094b86a94 100644 --- a/app/presentation/ServerItem/index.js +++ b/app/presentation/ServerItem/index.js @@ -2,17 +2,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import { View, Text } from 'react-native'; import FastImage from 'react-native-fast-image'; -import { RectButton } from 'react-native-gesture-handler'; +import Touch from '../../utils/touch'; import Check from '../../containers/Check'; import styles, { ROW_HEIGHT } from './styles'; +import { themes } from '../../constants/colors'; export { ROW_HEIGHT }; const ServerItem = React.memo(({ - server, item, onPress, hasCheck + server, item, onPress, hasCheck, theme }) => ( - <RectButton onPress={onPress} style={styles.serverItem} testID={`rooms-list-header-server-${ item.id }`}> + <Touch + onPress={onPress} + style={[styles.serverItem, { backgroundColor: themes[theme].backgroundColor }]} + testID={`rooms-list-header-server-${ item.id }`} + theme={theme} + > <View style={styles.serverItemContainer}> {item.iconURL ? ( @@ -34,19 +40,20 @@ const ServerItem = React.memo(({ ) } <View style={styles.serverTextContainer}> - <Text style={styles.serverName}>{item.name || item.id}</Text> - <Text style={styles.serverUrl}>{item.id}</Text> + <Text style={[styles.serverName, { color: themes[theme].titleText }]}>{item.name || item.id}</Text> + <Text style={[styles.serverUrl, { color: themes[theme].auxiliaryText }]}>{item.id}</Text> </View> - {item.id === server && hasCheck ? <Check /> : null} + {item.id === server && hasCheck ? <Check theme={theme} /> : null} </View> - </RectButton> + </Touch> )); ServerItem.propTypes = { onPress: PropTypes.func.isRequired, item: PropTypes.object.isRequired, hasCheck: PropTypes.bool, - server: PropTypes.string + server: PropTypes.string, + theme: PropTypes.string }; export default ServerItem; diff --git a/app/presentation/ServerItem/styles.js b/app/presentation/ServerItem/styles.js index 07669158f..34ed0c158 100644 --- a/app/presentation/ServerItem/styles.js +++ b/app/presentation/ServerItem/styles.js @@ -1,14 +1,12 @@ import { StyleSheet } from 'react-native'; import sharedStyles from '../../views/Styles'; -import { COLOR_WHITE } from '../../constants/colors'; export const ROW_HEIGHT = 56; export default StyleSheet.create({ serverItem: { height: ROW_HEIGHT, - backgroundColor: COLOR_WHITE, justifyContent: 'center' }, serverItemContainer: { @@ -28,12 +26,10 @@ export default StyleSheet.create({ }, serverName: { fontSize: 18, - ...sharedStyles.textColorNormal, ...sharedStyles.textSemibold }, serverUrl: { fontSize: 15, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular } }); diff --git a/app/presentation/TextInput.js b/app/presentation/TextInput.js new file mode 100644 index 000000000..ef63db236 --- /dev/null +++ b/app/presentation/TextInput.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { TextInput } from 'react-native'; +import PropTypes from 'prop-types'; + +import { themes } from '../constants/colors'; + +const ThemedTextInput = React.forwardRef(({ style, theme, ...props }, ref) => ( + <TextInput + ref={ref} + style={[{ color: themes[theme].titleText }, style]} + placeholderTextColor={themes[theme].auxiliaryText} + keyboardAppearance={theme === 'light' ? 'light' : 'dark'} + {...props} + /> +)); + +ThemedTextInput.propTypes = { + style: PropTypes.object, + theme: PropTypes.string +}; + +export default ThemedTextInput; diff --git a/app/presentation/UserItem.js b/app/presentation/UserItem.js index 94f42cbba..7ac25739f 100644 --- a/app/presentation/UserItem.js +++ b/app/presentation/UserItem.js @@ -1,17 +1,17 @@ import React from 'react'; import { Text, View, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; +import { LongPressGestureHandler, State } from 'react-native-gesture-handler'; import Avatar from '../containers/Avatar'; -import Touch from '../utils/touch'; import { CustomIcon } from '../lib/Icons'; import sharedStyles from '../views/Styles'; -import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors'; +import { themes } from '../constants/colors'; +import Touch from '../utils/touch'; const styles = StyleSheet.create({ button: { - height: 54, - backgroundColor: COLOR_WHITE + height: 54 }, container: { flexDirection: 'row' @@ -27,35 +27,50 @@ const styles = StyleSheet.create({ }, name: { fontSize: 17, - ...sharedStyles.textMedium, - ...sharedStyles.textColorNormal + ...sharedStyles.textMedium }, username: { fontSize: 14, - ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription + ...sharedStyles.textRegular }, icon: { marginHorizontal: 15, - alignSelf: 'center', - color: COLOR_PRIMARY + alignSelf: 'center' } }); const UserItem = ({ - name, username, onPress, testID, onLongPress, style, icon, baseUrl, user -}) => ( - <Touch onPress={onPress} onLongPress={onLongPress} style={styles.button} testID={testID}> - <View style={[styles.container, style]}> - <Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} /> - <View style={styles.textContainer}> - <Text style={styles.name}>{name}</Text> - <Text style={styles.username}>@{username}</Text> - </View> - {icon ? <CustomIcon name={icon} size={22} style={styles.icon} /> : null} - </View> - </Touch> -); + name, username, onPress, testID, onLongPress, style, icon, baseUrl, user, theme +}) => { + const longPress = ({ nativeEvent }) => { + if (nativeEvent.state === State.ACTIVE) { + onLongPress(); + } + }; + + return ( + <LongPressGestureHandler + onHandlerStateChange={longPress} + minDurationMs={800} + > + <Touch + onPress={onPress} + style={{ backgroundColor: themes[theme].backgroundColor }} + testID={testID} + theme={theme} + > + <View style={[styles.container, styles.button, style]}> + <Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} /> + <View style={styles.textContainer}> + <Text style={[styles.name, { color: themes[theme].titleText }]}>{name}</Text> + <Text style={[styles.username, { color: themes[theme].auxiliaryText }]}>@{username}</Text> + </View> + {icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null} + </View> + </Touch> + </LongPressGestureHandler> + ); +}; UserItem.propTypes = { name: PropTypes.string.isRequired, @@ -69,7 +84,8 @@ UserItem.propTypes = { testID: PropTypes.string.isRequired, onLongPress: PropTypes.func, style: PropTypes.any, - icon: PropTypes.string + icon: PropTypes.string, + theme: PropTypes.string }; export default UserItem; diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index 2706be47d..061db014a 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -53,17 +53,13 @@ const handleOpen = function* handleOpen({ params }) { // TODO: needs better test // if deep link is from same server - if (server === host) { - if (user) { - const connected = yield select(state => state.server.connected); - if (!connected) { - yield put(selectServerRequest(host)); - yield take(types.SERVER.SELECT_SUCCESS); - } - yield navigate({ params }); - } else { - yield put(appStart('outside')); + if (server === host && user) { + const connected = yield select(state => state.server.connected); + if (!connected) { + yield put(selectServerRequest(host)); + yield take(types.SERVER.SELECT_SUCCESS); } + yield navigate({ params }); } else { // search if deep link's server already exists const serversDB = database.servers; @@ -80,13 +76,21 @@ const handleOpen = function* handleOpen({ params }) { // do nothing? } // if deep link is from a different server - const result = yield RocketChat.getServerInfo(server); + const result = yield RocketChat.getServerInfo(host); if (!result.success) { return; } Navigation.navigate('OnboardingView', { previousServer: server }); yield delay(1000); EventEmitter.emit('NewServer', { server: host }); + + if (params.token) { + yield take(types.SERVER.SELECT_SUCCESS); + yield RocketChat.connect({ server: host, user: { token: params.token } }); + } + Navigation.navigate('OnboardingView', { previousServer: server }); + yield delay(1000); + EventEmitter.emit('NewServer', { server: host }); } }; diff --git a/app/sagas/init.js b/app/sagas/init.js index 022177c44..f41daf72f 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -20,11 +20,21 @@ import { isIOS } from '../utils/deviceInfo'; import database from '../lib/database'; import protectedFunction from '../lib/methods/helpers/protectedFunction'; +export const initLocalSettings = function* initLocalSettings() { + const sortPreferences = yield RocketChat.getSortPreferences(); + yield put(setAllPreferences(sortPreferences)); + + const useMarkdown = yield RocketChat.getUseMarkdown(); + yield put(toggleMarkdown(useMarkdown)); + + const allowCrashReport = yield RocketChat.getAllowCrashReport(); + yield put(toggleCrashReport(allowCrashReport)); +}; + const restore = function* restore() { try { let hasMigration; if (isIOS) { - yield RNUserDefaults.setName('group.ios.chat.rocket'); hasMigration = yield AsyncStorage.getItem('hasMigration'); } @@ -84,12 +94,6 @@ const restore = function* restore() { } } - const sortPreferences = yield RocketChat.getSortPreferences(); - yield put(setAllPreferences(sortPreferences)); - - const useMarkdown = yield RocketChat.getUseMarkdown(); - yield put(toggleMarkdown(useMarkdown)); - const allowCrashReport = yield RocketChat.getAllowCrashReport(); yield put(toggleCrashReport(allowCrashReport)); @@ -127,5 +131,6 @@ const start = function* start({ root }) { const root = function* root() { yield takeLatest(APP.INIT, restore); yield takeLatest(APP.START, start); + yield takeLatest(APP.INIT_LOCAL_SETTINGS, initLocalSettings); }; export default root; diff --git a/app/sagas/login.js b/app/sagas/login.js index b9d20a55b..3e0415914 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -9,7 +9,9 @@ import 'moment/min/locales'; import * as types from '../actions/actionsTypes'; import { appStart } from '../actions'; import { serverFinishAdd, selectServerRequest } from '../actions/server'; -import { loginFailure, loginSuccess, setUser } from '../actions/login'; +import { + loginFailure, loginSuccess, setUser, logout +} from '../actions/login'; import { roomsRequest } from '../actions/rooms'; import { toMomentLocale } from '../utils/moment'; import RocketChat from '../lib/rocketchat'; @@ -24,7 +26,7 @@ const loginWithPasswordCall = args => RocketChat.loginWithPassword(args); const loginCall = args => RocketChat.login(args); const logoutCall = args => RocketChat.logout(args); -const handleLoginRequest = function* handleLoginRequest({ credentials }) { +const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnError = false }) { try { let result; if (credentials.resume) { @@ -34,7 +36,11 @@ const handleLoginRequest = function* handleLoginRequest({ credentials }) { } return yield put(loginSuccess(result)); } catch (error) { - yield put(loginFailure(error)); + if (logoutOnError) { + yield put(logout()); + } else { + yield put(loginFailure(error)); + } } }; @@ -114,7 +120,10 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { yield put(serverFinishAdd()); yield put(appStart('inside')); } else { - yield put(appStart('inside')); + const currentRoot = yield select(state => state.app.root); + if (currentRoot !== 'inside') { + yield put(appStart('inside')); + } } } catch (e) { log(e); @@ -130,16 +139,9 @@ const handleLogout = function* handleLogout() { const serversDB = database.servers; // all servers const serversCollection = serversDB.collections.get('servers'); - - // filter logging out server and delete it - yield serversDB.action(async() => { - const serverRecord = await serversCollection.find(server); - await serverRecord.destroyPermanently(); - }); - const servers = yield serversCollection.query().fetch(); - // see if there's other logged in servers and selects first one + // see if there're other logged in servers and selects first one if (servers.length > 0) { const newServer = servers[0].id; const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`); diff --git a/app/sagas/messages.js b/app/sagas/messages.js index 24c1a7046..75ba17069 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -7,24 +7,30 @@ import RocketChat from '../lib/rocketchat'; import database from '../lib/database'; import log from '../utils/log'; -const goRoom = function goRoom({ rid, name, message }) { +const goRoom = function goRoom({ + rid, name, fname, message +}) { Navigation.navigate('RoomsListView'); Navigation.navigate('RoomView', { - rid, name, t: 'd', message + rid, name, fname, t: 'd', message }); }; const handleReplyBroadcast = function* handleReplyBroadcast({ message }) { try { const db = database.active; - const { username } = message.u; + const { username, name } = message.u; const subsCollection = db.collections.get('subscriptions'); const subscriptions = yield subsCollection.query(Q.where('name', username)).fetch(); if (subscriptions.length) { - yield goRoom({ rid: subscriptions[0].rid, name: username, message }); + yield goRoom({ + rid: subscriptions[0].rid, name: username, fname: name, message + }); } else { const room = yield RocketChat.createDirectMessage(username); - yield goRoom({ rid: room.rid, name: username, message }); + yield goRoom({ + rid: room.rid, name: username, fname: name, message + }); } } catch (e) { log(e); diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index ef1d42dcf..17da0b07f 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -11,6 +11,7 @@ import database from '../lib/database'; import log from '../utils/log'; import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms'; import RocketChat from '../lib/rocketchat'; +// import buildMessage from '../lib/methods/helpers/buildMessage'; const handleRoomsRequest = function* handleRoomsRequest() { try { @@ -26,17 +27,27 @@ const handleRoomsRequest = function* handleRoomsRequest() { const db = database.active; yield db.action(async() => { - const subCollection = db.collections.get('subscriptions'); if (!subscriptions.length) { return; } + const subCollection = db.collections.get('subscriptions'); + // const messagesCollection = db.collections.get('messages'); + const subsIds = subscriptions.map(sub => sub.rid); const existingSubs = await subCollection.query(Q.where('id', Q.oneOf(subsIds))).fetch(); const subsToUpdate = existingSubs.filter(i1 => subscriptions.find(i2 => i1._id === i2._id)); const subsToCreate = subscriptions.filter(i1 => !existingSubs.find(i2 => i1._id === i2._id)); // TODO: subsToDelete? + // const lastMessages = subscriptions + // .map(sub => sub.lastMessage && buildMessage(sub.lastMessage)) + // .filter(lm => lm); + // const lastMessagesIds = lastMessages.map(lm => lm._id); + // const existingMessages = await messagesCollection.query(Q.where('id', Q.oneOf(lastMessagesIds))).fetch(); + // const messagesToUpdate = existingMessages.filter(i1 => lastMessages.find(i2 => i1.id === i2._id)); + // const messagesToCreate = lastMessages.filter(i1 => !existingMessages.find(i2 => i1._id === i2.id)); + const allRecords = [ ...subsToCreate.map(subscription => subCollection.prepareCreate((s) => { s._raw = sanitizedRaw({ id: subscription.rid }, subCollection.schema); @@ -48,6 +59,17 @@ const handleRoomsRequest = function* handleRoomsRequest() { Object.assign(subscription, newSub); }); }) + // ...messagesToCreate.map(message => messagesCollection.prepareCreate((m) => { + // m._raw = sanitizedRaw({ id: message._id }, messagesCollection.schema); + // m.subscription.id = message.rid; + // return Object.assign(m, message); + // })), + // ...messagesToUpdate.map((message) => { + // const newMessage = lastMessages.find(m => m._id === message.id); + // return message.prepareUpdate(() => { + // Object.assign(message, newMessage); + // }); + // }) ]; try { diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 83fdc2976..6b1a2078a 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -22,9 +22,14 @@ import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults'; const getServerInfo = function* getServerInfo({ server, raiseError = true }) { try { const serverInfo = yield RocketChat.getServerInfo(server); - if (!serverInfo.success) { + let websocketInfo = { success: true }; + if (raiseError) { + websocketInfo = yield RocketChat.getWebsocketInfo({ server }); + } + if (!serverInfo.success || !websocketInfo.success) { if (raiseError) { - Alert.alert(I18n.t('Oops'), I18n.t(serverInfo.message, serverInfo.messageOptions)); + const info = serverInfo.success ? websocketInfo : serverInfo; + Alert.alert(I18n.t('Oops'), I18n.t(info.message, info.messageOptions)); } yield put(serverFailure()); return; @@ -82,7 +87,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch } if (user) { - yield RocketChat.connect({ server, user }); + yield RocketChat.connect({ server, user, logoutOnError: true }); yield put(setUser(user)); yield put(actions.appStart('inside')); } else { @@ -90,17 +95,10 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch yield put(actions.appStart('outside')); } - const db = database.active; - const serversCollection = db.collections.get('settings'); - const settingsRecords = yield serversCollection.query().fetch(); - const settings = Object.values(settingsRecords).map(item => ({ - _id: item.id, - valueAsString: item.valueAsString, - valueAsBoolean: item.valueAsBoolean, - valueAsNumber: item.valueAsNumber, - _updatedAt: item._updatedAt - })); - yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); + // We can't use yield here because fetch of Settings & Custom Emojis is slower + // and block the selectServerSuccess raising multiples errors + RocketChat.setSettings(); + RocketChat.setCustomEmojis(); yield RocketChat.setCustomEmojis(); diff --git a/app/share.js b/app/share.js index 73d6f06b4..3d2484a7d 100644 --- a/app/share.js +++ b/app/share.js @@ -1,17 +1,24 @@ import React from 'react'; import { View } from 'react-native'; import { createAppContainer, createSwitchNavigator } from 'react-navigation'; +import { AppearanceProvider } from 'react-native-appearance'; import { createStackNavigator } from 'react-navigation-stack'; import { Provider } from 'react-redux'; import RNUserDefaults from 'rn-user-defaults'; +import { + defaultTheme, + newThemeState, + subscribeTheme, + unsubscribeTheme +} from './utils/theme'; import Navigation from './lib/ShareNavigation'; import store from './lib/createStore'; import sharedStyles from './views/Styles'; -import { isNotch, isIOS } from './utils/deviceInfo'; -import { defaultHeader, onNavigationStateChange } from './utils/navigation'; -import RocketChat from './lib/rocketchat'; -import LayoutAnimation from './utils/layoutAnimation'; +import { isNotch, isIOS, supportSystemTheme } from './utils/deviceInfo'; +import { defaultHeader, onNavigationStateChange, cardStyle } from './utils/navigation'; +import RocketChat, { THEME_PREFERENCES_KEY } from './lib/rocketchat'; +import { ThemeContext } from './theme'; const InsideNavigator = createStackNavigator({ ShareListView: { @@ -25,7 +32,8 @@ const InsideNavigator = createStackNavigator({ } }, { initialRouteName: 'ShareListView', - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); const OutsideNavigator = createStackNavigator({ @@ -34,7 +42,8 @@ const OutsideNavigator = createStackNavigator({ } }, { initialRouteName: 'WithoutServersView', - defaultNavigationOptions: defaultHeader + defaultNavigationOptions: defaultHeader, + cardStyle }); const AppContainer = createAppContainer(createSwitchNavigator({ @@ -52,15 +61,26 @@ class Root extends React.Component { constructor(props) { super(props); this.state = { - isLandscape: false + isLandscape: false, + theme: defaultTheme(), + themePreferences: { + currentTheme: supportSystemTheme() ? 'automatic' : 'light', + darkLevel: 'dark' + } }; this.init(); } + componentWillUnmount() { + RocketChat.closeShareExtension(); + unsubscribeTheme(); + } + init = async() => { if (isIOS) { await RNUserDefaults.setName('group.ios.chat.rocket'); } + RNUserDefaults.objectForKey(THEME_PREFERENCES_KEY).then(this.setTheme); const currentServer = await RNUserDefaults.get('currentServer'); const token = await RNUserDefaults.get(RocketChat.TOKEN_KEY); @@ -72,29 +92,41 @@ class Root extends React.Component { } } + setTheme = (newTheme = {}) => { + // change theme state + this.setState(prevState => newThemeState(prevState, newTheme), () => { + const { themePreferences } = this.state; + // subscribe to Appearance changes + subscribeTheme(themePreferences, this.setTheme); + }); + } + handleLayout = (event) => { const { width, height } = event.nativeEvent.layout; this.setState({ isLandscape: width > height }); } render() { - const { isLandscape } = this.state; + const { isLandscape, theme } = this.state; return ( - <View - style={[sharedStyles.container, isLandscape && isNotch ? sharedStyles.notchLandscapeContainer : {}]} - onLayout={this.handleLayout} - > - <Provider store={store}> - <LayoutAnimation> - <AppContainer - ref={(navigatorRef) => { - Navigation.setTopLevelNavigator(navigatorRef); - }} - onNavigationStateChange={onNavigationStateChange} - /> - </LayoutAnimation> - </Provider> - </View> + <AppearanceProvider> + <View + style={[sharedStyles.container, isLandscape && isNotch ? sharedStyles.notchLandscapeContainer : {}]} + onLayout={this.handleLayout} + > + <Provider store={store}> + <ThemeContext.Provider value={{ theme }}> + <AppContainer + ref={(navigatorRef) => { + Navigation.setTopLevelNavigator(navigatorRef); + }} + onNavigationStateChange={onNavigationStateChange} + screenProps={{ theme }} + /> + </ThemeContext.Provider> + </Provider> + </View> + </AppearanceProvider> ); } } diff --git a/app/split.js b/app/split.js new file mode 100644 index 000000000..44136bbcb --- /dev/null +++ b/app/split.js @@ -0,0 +1,19 @@ +import React from 'react'; +import hoistNonReactStatics from 'hoist-non-react-statics'; + +import { isTablet } from './utils/deviceInfo'; + +export const SplitContext = React.createContext(null); + +export function withSplit(Component) { + if (isTablet) { + const SplitComponent = props => ( + <SplitContext.Consumer> + {contexts => <Component {...props} {...contexts} />} + </SplitContext.Consumer> + ); + hoistNonReactStatics(SplitComponent, Component); + return SplitComponent; + } + return Component; +} diff --git a/app/tablet.js b/app/tablet.js new file mode 100644 index 000000000..c503e0f7a --- /dev/null +++ b/app/tablet.js @@ -0,0 +1,202 @@ +import React from 'react'; +import { View } from 'react-native'; +import PropTypes from 'prop-types'; +import { NavigationActions, StackActions } from 'react-navigation'; +import KeyCommands from 'react-native-keycommands'; + +import Navigation from './lib/Navigation'; +import { isSplited } from './utils/deviceInfo'; +import { + App, RoomContainer, ModalContainer, NotificationContainer +} from './index'; +import { MAX_SIDEBAR_WIDTH } from './constants/tablet'; +import ModalNavigation from './lib/ModalNavigation'; +import { keyCommands, defaultCommands } from './commands'; +import { themes } from './constants/colors'; + +import sharedStyles from './views/Styles'; + +let modalRef; +let roomRef; +let notificationRef; + +export const initTabletNav = (setState) => { + let inCall = false; + + const defaultApp = App.router.getStateForAction; + const defaultModal = ModalContainer.router.getStateForAction; + const defaultRoom = RoomContainer.router.getStateForAction; + const defaultNotification = NotificationContainer.router.getStateForAction; + + NotificationContainer.router.getStateForAction = (action, state) => { + if (action.type === NavigationActions.NAVIGATE && isSplited()) { + const { routeName, params } = action; + if (routeName === 'RoomView') { + const resetAction = StackActions.reset({ + index: 0, + actions: [NavigationActions.navigate({ routeName, params })] + }); + roomRef.dispatch(resetAction); + } + } + return defaultNotification(action, state); + }; + + RoomContainer.router.getStateForAction = (action, state) => { + if (action.type === NavigationActions.NAVIGATE && isSplited()) { + const { routeName, params } = action; + if (routeName === 'RoomActionsView') { + modalRef.dispatch(NavigationActions.navigate({ routeName, params })); + setState({ showModal: true }); + return null; + } + } + if (action.type === 'Navigation/RESET' && isSplited()) { + const { params } = action.actions[action.index]; + const routes = state.routes[state.index] && state.routes[state.index].params; + if (params && params.rid && routes && routes.rid && params.rid === routes.rid) { + return null; + } + } + return defaultRoom(action, state); + }; + + ModalContainer.router.getStateForAction = (action, state) => { + if (action.type === 'Navigation/POP' && isSplited()) { + modalRef.dispatch(NavigationActions.navigate({ routeName: 'AuthLoading' })); + setState({ showModal: false }); + } + if (action.type === NavigationActions.NAVIGATE && isSplited()) { + const { routeName, params } = action; + if (routeName === 'RoomView') { + Navigation.navigate(routeName, params); + } + } + return defaultModal(action, state); + }; + + App.router.getStateForAction = (action, state) => { + if (action.type === NavigationActions.NAVIGATE) { + const { routeName, params } = action; + + if (routeName === 'InsideStack') { + let commands = defaultCommands; + let newState = { inside: true }; + if (isSplited()) { + commands = [...commands, ...keyCommands]; + newState = { ...newState, showModal: false }; + } + KeyCommands.setKeyCommands(commands); + setState(newState); + } + if (isSplited()) { + if (routeName === 'ReadReceiptsView') { + roomRef.dispatch(NavigationActions.navigate({ routeName, params })); + return null; + } + if (routeName === 'OutsideStack') { + KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]); + setState({ inside: false, showModal: false }); + } + if (routeName === 'JitsiMeetView') { + inCall = true; + KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]); + setState({ inside: false, showModal: false }); + } + if (routeName === 'OnboardingView') { + KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]); + setState({ inside: false, showModal: false }); + } + + if (routeName === 'RoomView') { + const resetAction = StackActions.reset({ + index: 0, + actions: [NavigationActions.navigate({ routeName, params })] + }); + roomRef.dispatch(resetAction); + notificationRef.dispatch(resetAction); + setState({ showModal: false }); + return null; + } + + if (routeName === 'NewMessageView') { + modalRef.dispatch(NavigationActions.navigate({ routeName, params })); + setState({ showModal: true }); + return null; + } + if (routeName === 'DirectoryView') { + modalRef.dispatch(NavigationActions.navigate({ routeName })); + setState({ showModal: true }); + return null; + } + } + } + if (action.type === 'Navigation/TOGGLE_DRAWER' && isSplited()) { + modalRef.dispatch(NavigationActions.navigate({ routeName: 'SettingsView' })); + setState({ showModal: true }); + return null; + } + if (action.type === 'Navigation/POP' && inCall) { + KeyCommands.setKeyCommands([...defaultCommands, ...keyCommands]); + setState({ inside: true, showModal: false }); + } + return defaultApp(action, state); + }; +}; + +const Split = ({ + split, tablet, showModal, closeModal, setModalRef, theme +}) => { + if (split) { + return ( + <> + <View style={[sharedStyles.container, sharedStyles.separatorLeft, { borderColor: themes[theme].separatorColor }]}> + <RoomContainer ref={ref => roomRef = ref} screenProps={{ split: tablet, theme }} /> + </View> + <ModalContainer showModal={showModal} closeModal={closeModal} ref={setModalRef} screenProps={{ split: tablet, theme }} /> + </> + ); + } + return null; +}; + +const Tablet = ({ + children, tablet, theme, inside, showModal, closeModal, onLayout +}) => { + const setModalRef = (ref) => { + modalRef = ref; + ModalNavigation.setTopLevelNavigator(modalRef); + }; + + const split = tablet && inside; + return ( + <View style={sharedStyles.containerSplitView} onLayout={onLayout}> + <View style={[sharedStyles.container, split && { maxWidth: MAX_SIDEBAR_WIDTH }]}> + {children} + </View> + <Split split={split} tablet={tablet} theme={theme} showModal={showModal} closeModal={closeModal} setModalRef={setModalRef} /> + <NotificationContainer ref={ref => notificationRef = ref} screenProps={{ theme }} /> + </View> + ); +}; + +Split.propTypes = { + split: PropTypes.bool, + tablet: PropTypes.bool, + showModal: PropTypes.bool, + closeModal: PropTypes.func, + setModalRef: PropTypes.func, + theme: PropTypes.string +}; + +Tablet.propTypes = { + children: PropTypes.node, + tablet: PropTypes.bool, + inside: PropTypes.bool, + showModal: PropTypes.bool, + closeModal: PropTypes.func, + onLayout: PropTypes.func, + theme: PropTypes.string +}; + +export default Tablet; diff --git a/app/theme.js b/app/theme.js new file mode 100644 index 000000000..05476a7ed --- /dev/null +++ b/app/theme.js @@ -0,0 +1,14 @@ +import React from 'react'; +import hoistNonReactStatics from 'hoist-non-react-statics'; + +export const ThemeContext = React.createContext(null); + +export function withTheme(Component) { + const ThemedComponent = props => ( + <ThemeContext.Consumer> + {contexts => <Component {...props} {...contexts} />} + </ThemeContext.Consumer> + ); + hoistNonReactStatics(ThemedComponent, Component); + return ThemedComponent; +} diff --git a/app/utils/deviceInfo.js b/app/utils/deviceInfo.js index cc2b493b7..cc9236ab1 100644 --- a/app/utils/deviceInfo.js +++ b/app/utils/deviceInfo.js @@ -1,6 +1,8 @@ import { Platform } from 'react-native'; import DeviceInfo from 'react-native-device-info'; +import { MIN_WIDTH_SPLIT_LAYOUT } from '../constants/tablet'; + const NOTCH_DEVICES = ['iPhone X', 'iPhone XS', 'iPhone XS Max', 'iPhone XR']; export const isNotch = NOTCH_DEVICES.includes(DeviceInfo.getModel()); @@ -9,3 +11,19 @@ export const isAndroid = !isIOS; export const getReadableVersion = DeviceInfo.getReadableVersion(); export const getBundleId = DeviceInfo.getBundleId(); export const getDeviceModel = DeviceInfo.getModel(); + +// Theme is supported by system on iOS 13+ or Android 10+ +export const supportSystemTheme = () => { + const systemVersion = parseInt(DeviceInfo.getSystemVersion(), 10); + return systemVersion >= (isIOS ? 13 : 10); +}; + +// Tablet info +export const isTablet = DeviceInfo.isTablet(); + +// We need to use this when app is used on splitview with another app +// to handle cases on app view not-larger sufficient to show splited views (room list/room) +// https://github.com/RocketChat/Rocket.Chat.ReactNative/pull/1300#discussion_r341405245 +let _width = null; +export const setWidth = width => _width = width; +export const isSplited = () => isTablet && _width > MIN_WIDTH_SPLIT_LAYOUT; diff --git a/app/utils/fetch.js b/app/utils/fetch.js new file mode 100644 index 000000000..1e526af2d --- /dev/null +++ b/app/utils/fetch.js @@ -0,0 +1,12 @@ +import { Platform } from 'react-native'; +import DeviceInfo from 'react-native-device-info'; + +export const headers = { 'User-Agent': `RC-RN Mobile/${ DeviceInfo.getVersion() } (build: ${ DeviceInfo.getBuildNumber() }; os: ${ Platform.OS } ${ DeviceInfo.getSystemVersion() })` }; + +export default (url, options = {}) => { + let customOptions = { ...options, headers }; + if (options && options.headers) { + customOptions = { ...customOptions, headers: { ...options.headers, ...headers } }; + } + return fetch(url, customOptions); +}; diff --git a/app/utils/layoutAnimation.js b/app/utils/layoutAnimation.js index a558404cd..3b4292d35 100644 --- a/app/utils/layoutAnimation.js +++ b/app/utils/layoutAnimation.js @@ -1,44 +1,10 @@ -import React from 'react'; -import { Transition, Transitioning } from 'react-native-reanimated'; -import PropTypes from 'prop-types'; +import { LayoutAnimation } from 'react-native'; import debounce from './debounce'; import { isIOS } from './deviceInfo'; -import sharedStyles from '../views/Styles'; - -const transition = ( - <Transition.Together> - <Transition.In type='fade' /> - <Transition.Out type='fade' /> - <Transition.Change interpolation='easeInOut' /> - </Transition.Together> -); - -const TRANSITION_REF = React.createRef(); export const animateNextTransition = debounce(() => { if (isIOS) { - TRANSITION_REF.current.animateNextTransition(); + LayoutAnimation.easeInEaseOut(); } }, 200, true); - -const LayoutAnimation = ({ children }) => { - if (isIOS) { - return ( - <Transitioning.View - style={sharedStyles.root} - transition={transition} - ref={TRANSITION_REF} - > - {children} - </Transitioning.View> - ); - } - return children; -}; - -LayoutAnimation.propTypes = { - children: PropTypes.node -}; - -export default LayoutAnimation; diff --git a/app/utils/navigation.js b/app/utils/navigation.js index 539aff9ec..729ea5202 100644 --- a/app/utils/navigation.js +++ b/app/utils/navigation.js @@ -1,18 +1,31 @@ -import { analytics, leaveBreadcrumb } from './log'; +import { StyleSheet } from 'react-native'; -import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from '../constants/colors'; +import { analytics, leaveBreadcrumb } from './log'; +import { themes } from '../constants/colors'; export const defaultHeader = { - headerStyle: { - backgroundColor: HEADER_BACKGROUND - }, - headerTitleStyle: { - color: HEADER_TITLE - }, - headerBackTitle: null, - headerTintColor: HEADER_BACK + headerBackTitle: null }; +export const cardStyle = { + backgroundColor: 'rgba(0,0,0,0)' +}; + +const borderBottom = theme => ({ + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: themes[theme].headerBorder, + elevation: 0 +}); + +export const themedHeader = theme => ({ + headerStyle: { + ...borderBottom(theme), + backgroundColor: themes[theme].headerBackground + }, + headerTintColor: themes[theme].headerTintColor, + headerTitleStyle: { color: themes[theme].headerTitleColor } +}); + // gets the current screen from navigation state export const getActiveRouteName = (navigationState) => { if (!navigationState) { diff --git a/app/utils/openLink.js b/app/utils/openLink.js index 36988d293..9978590ad 100644 --- a/app/utils/openLink.js +++ b/app/utils/openLink.js @@ -1,10 +1,10 @@ import * as WebBrowser from 'expo-web-browser'; -import { HEADER_TINT, HEADER_BACKGROUND } from '../constants/colors'; +import { themes } from '../constants/colors'; -const openLink = url => WebBrowser.openBrowserAsync(url, { - toolbarColor: HEADER_BACKGROUND, - controlsColor: HEADER_TINT, +const openLink = (url, theme = 'light') => WebBrowser.openBrowserAsync(url, { + toolbarColor: themes[theme].headerBackground, + controlsColor: themes[theme].headerTintColor, collapseToolbar: true, showTitle: true }); diff --git a/app/utils/scaling.js b/app/utils/scaling.js index 17fc1b93e..1168cc614 100644 --- a/app/utils/scaling.js +++ b/app/utils/scaling.js @@ -1,9 +1,11 @@ import { Dimensions } from 'react-native'; +import { isTablet } from './deviceInfo'; + const { width, height } = Dimensions.get('window'); -const guidelineBaseWidth = 375; -const guidelineBaseHeight = 667; +const guidelineBaseWidth = isTablet ? 600 : 375; +const guidelineBaseHeight = isTablet ? 800 : 667; const scale = size => (width / guidelineBaseWidth) * size; const verticalScale = size => (height / guidelineBaseHeight) * size; diff --git a/app/utils/theme.js b/app/utils/theme.js new file mode 100644 index 000000000..24405d34a --- /dev/null +++ b/app/utils/theme.js @@ -0,0 +1,65 @@ +import { Appearance } from 'react-native-appearance'; +import changeNavigationBarColor from 'react-native-navigation-bar-color'; +import setRootViewColor from 'rn-root-view'; + +import { isAndroid } from './deviceInfo'; +import { themes } from '../constants/colors'; + +let themeListener; + +export const defaultTheme = () => { + const systemTheme = Appearance.getColorScheme(); + if (systemTheme && systemTheme !== 'no-preference') { + return systemTheme; + } + return 'light'; +}; + +export const getTheme = (themePreferences) => { + const { darkLevel, currentTheme } = themePreferences; + let theme = currentTheme; + if (currentTheme === 'automatic') { + theme = defaultTheme(); + } + return theme === 'dark' ? darkLevel : 'light'; +}; + +export const newThemeState = (prevState, newTheme) => { + // new theme preferences + const themePreferences = { + ...prevState.themePreferences, + ...newTheme + }; + // set new state of themePreferences + // and theme (based on themePreferences) + return { themePreferences, theme: getTheme(themePreferences) }; +}; + +export const setNativeTheme = (themePreferences) => { + const theme = getTheme(themePreferences); + if (isAndroid) { + const iconsLight = theme === 'light'; + changeNavigationBarColor(themes[theme].navbarBackground, iconsLight); + } + setRootViewColor(themes[theme].backgroundColor); +}; + +export const unsubscribeTheme = () => { + if (themeListener && themeListener.remove) { + themeListener.remove(); + themeListener = null; + } +}; + +export const subscribeTheme = (themePreferences, setTheme) => { + const { currentTheme } = themePreferences; + if (!themeListener && currentTheme === 'automatic') { + // not use listener params because we use getTheme + themeListener = Appearance.addChangeListener(() => setTheme()); + } else if (currentTheme !== 'automatic') { + // unsubscribe appearance changes when automatic was disabled + unsubscribeTheme(); + } + // set native components theme + setNativeTheme(themePreferences); +}; diff --git a/app/utils/touch.js b/app/utils/touch.js new file mode 100644 index 000000000..505e6a02a --- /dev/null +++ b/app/utils/touch.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { RectButton } from 'react-native-gesture-handler'; + +import { themes } from '../constants/colors'; + +class Touch extends React.Component { + setNativeProps(props) { + this.ref.setNativeProps(props); + } + + getRef = (ref) => { + this.ref = ref; + }; + + render() { + const { + children, onPress, theme, ...props + } = this.props; + + return ( + <RectButton + ref={this.getRef} + onPress={onPress} + activeOpacity={1} + underlayColor={themes[theme].bannerBackground} + rippleColor={themes[theme].bannerBackground} + {...props} + > + {children} + </RectButton> + ); + } +} + +Touch.propTypes = { + children: PropTypes.node, + onPress: PropTypes.func, + theme: PropTypes.string +}; + +export default Touch; diff --git a/app/utils/touch/index.android.js b/app/utils/touch/index.android.js deleted file mode 100644 index 9eb368a42..000000000 --- a/app/utils/touch/index.android.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { TouchableNativeFeedback, View } from 'react-native'; -import PropTypes from 'prop-types'; - -const Touch = ({ - children, style, onPress, ...props -}) => ( - <TouchableNativeFeedback - onPress={onPress} - {...props} - > - <View style={style}> - {children} - </View> - </TouchableNativeFeedback> -); - -Touch.propTypes = { - children: PropTypes.node.isRequired, - style: PropTypes.any, - onPress: PropTypes.func.isRequired -}; - -export default Touch; diff --git a/app/utils/touch/index.ios.js b/app/utils/touch/index.ios.js deleted file mode 100644 index 493559503..000000000 --- a/app/utils/touch/index.ios.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { TouchableHighlight } from 'react-native'; -import PropTypes from 'prop-types'; -import { COLOR_WHITE } from '../../constants/colors'; - -const Touch = ({ children, onPress, ...props }) => ( - <TouchableHighlight - underlayColor={COLOR_WHITE} - activeOpacity={0.5} - onPress={onPress} - {...props} - > - {children} - </TouchableHighlight> -); - -Touch.propTypes = { - children: PropTypes.node.isRequired, - onPress: PropTypes.func -}; - -export default Touch; diff --git a/app/views/AdminPanelView/index.js b/app/views/AdminPanelView/index.js index c31529680..1fc0ba021 100644 --- a/app/views/AdminPanelView/index.js +++ b/app/views/AdminPanelView/index.js @@ -8,26 +8,31 @@ import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; import { DrawerButton } from '../../containers/HeaderButton'; import styles from '../Styles'; +import { themedHeader } from '../../utils/navigation'; +import { withTheme } from '../../theme'; +import { themes } from '../../constants/colors'; class AdminPanelView extends React.Component { - static navigationOptions = ({ navigation }) => ({ + static navigationOptions = ({ navigation, screenProps }) => ({ + ...themedHeader(screenProps.theme), headerLeft: <DrawerButton navigation={navigation} />, title: I18n.t('Admin_Panel') }) static propTypes = { baseUrl: PropTypes.string, - authToken: PropTypes.string + authToken: PropTypes.string, + theme: PropTypes.string } render() { - const { baseUrl, authToken } = this.props; + const { baseUrl, authToken, theme } = this.props; if (!baseUrl) { return null; } return ( - <SafeAreaView style={styles.container} testID='terms-view'> - <StatusBar /> + <SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='terms-view'> + <StatusBar theme={theme} /> <WebView source={{ uri: `${ baseUrl }/admin/info?layout=embedded` }} injectedJavaScript={`Meteor.loginWithToken('${ authToken }', function() { })`} @@ -42,4 +47,4 @@ const mapStateToProps = state => ({ authToken: state.login.user && state.login.user.token }); -export default connect(mapStateToProps)(AdminPanelView); +export default connect(mapStateToProps)(withTheme(AdminPanelView)); diff --git a/app/views/AuthLoadingView.js b/app/views/AuthLoadingView.js index b0195d634..cf55170b1 100644 --- a/app/views/AuthLoadingView.js +++ b/app/views/AuthLoadingView.js @@ -3,17 +3,28 @@ import { StyleSheet, Image } from 'react-native'; import StatusBar from '../containers/StatusBar'; import { isAndroid } from '../utils/deviceInfo'; +import { withTheme } from '../theme'; const styles = StyleSheet.create({ image: { width: '100%', - height: '100%' + height: '100%', + backgroundColor: 'white' } }); -export default React.memo(() => ( +export default React.memo(withTheme(({ theme }) => ( <> - <StatusBar /> - {isAndroid ? <Image source={{ uri: 'launch_screen' }} style={styles.image} /> : null} + <StatusBar theme={theme} /> + {isAndroid + ? ( + <Image + source={{ uri: 'launch_screen' }} + style={styles.image} + resizeMode='contain' + /> + ) + : null + } </> -)); +))); diff --git a/app/views/AuthenticationWebView.js b/app/views/AuthenticationWebView.js index cbf7549b4..2a6b656c5 100644 --- a/app/views/AuthenticationWebView.js +++ b/app/views/AuthenticationWebView.js @@ -2,32 +2,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import { WebView } from 'react-native-webview'; import { connect } from 'react-redux'; -import { ActivityIndicator, StyleSheet } from 'react-native'; import RocketChat from '../lib/rocketchat'; import { isIOS } from '../utils/deviceInfo'; import { CloseModalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; +import ActivityIndicator from '../containers/ActivityIndicator'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; const userAgent = isIOS ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1' : 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36'; -const styles = StyleSheet.create({ - loading: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - alignItems: 'center', - justifyContent: 'center' - } -}); - class AuthenticationWebView extends React.PureComponent { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const authType = navigation.getParam('authType', 'oauth'); return { + ...themedHeader(screenProps.theme), headerLeft: <CloseModalButton navigation={navigation} />, title: authType === 'saml' || authType === 'cas' ? 'SSO' : 'OAuth' }; @@ -35,7 +26,8 @@ class AuthenticationWebView extends React.PureComponent { static propTypes = { navigation: PropTypes.object, - server: PropTypes.string + server: PropTypes.string, + theme: PropTypes.string } constructor(props) { @@ -101,14 +93,13 @@ class AuthenticationWebView extends React.PureComponent { } render() { - const { navigation } = this.props; const { loading } = this.state; + const { navigation, theme } = this.props; const uri = navigation.getParam('url'); return ( <> - <StatusBar /> + <StatusBar theme={theme} /> <WebView - useWebKit source={{ uri }} userAgent={userAgent} onNavigationStateChange={this.onNavigationStateChange} @@ -119,7 +110,7 @@ class AuthenticationWebView extends React.PureComponent { this.setState({ loading: false }); }} /> - { loading ? <ActivityIndicator size='large' style={styles.loading} /> : null } + { loading ? <ActivityIndicator size='large' theme={theme} absolute /> : null } </> ); } @@ -129,4 +120,4 @@ const mapStateToProps = state => ({ server: state.server.server }); -export default connect(mapStateToProps)(AuthenticationWebView); +export default connect(mapStateToProps)(withTheme(AuthenticationWebView)); diff --git a/app/views/AutoTranslateView/index.js b/app/views/AutoTranslateView/index.js index 0bb520a6b..b9beac37a 100644 --- a/app/views/AutoTranslateView/index.js +++ b/app/views/AutoTranslateView/index.js @@ -7,42 +7,56 @@ import { SafeAreaView, ScrollView } from 'react-navigation'; import RocketChat from '../../lib/rocketchat'; import I18n from '../../i18n'; -// import log from '../../utils/log'; import StatusBar from '../../containers/StatusBar'; import { CustomIcon } from '../../lib/Icons'; import sharedStyles from '../Styles'; import ListItem from '../../containers/ListItem'; import Separator from '../../containers/Separator'; -import { - SWITCH_TRACK_COLOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_SEPARATOR -} from '../../constants/colors'; +import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; const styles = StyleSheet.create({ contentContainerStyle: { - borderColor: COLOR_SEPARATOR, borderTopWidth: StyleSheet.hairlineWidth, - borderBottomWidth: StyleSheet.hairlineWidth, - backgroundColor: COLOR_WHITE, marginTop: 10, paddingBottom: 30 }, + flatListContainerStyle: { + borderBottomWidth: StyleSheet.hairlineWidth + }, sectionSeparator: { ...sharedStyles.separatorVertical, - backgroundColor: COLOR_BACKGROUND_CONTAINER, height: 10 } }); -const SectionSeparator = React.memo(() => <View style={styles.sectionSeparator} />); +const SectionSeparator = React.memo(({ theme }) => ( + <View + style={[ + styles.sectionSeparator, + { + backgroundColor: themes[theme].auxiliaryBackground, + borderColor: themes[theme].separatorColor + } + ]} + /> +)); -export default class AutoTranslateView extends React.Component { - static navigationOptions = () => ({ - title: I18n.t('Auto_Translate') +SectionSeparator.propTypes = { + theme: PropTypes.string +}; + +class AutoTranslateView extends React.Component { + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Auto_Translate'), + ...themedHeader(screenProps.theme) }) static propTypes = { - navigation: PropTypes.object + navigation: PropTypes.object, + theme: PropTypes.string } constructor(props) { @@ -107,9 +121,15 @@ export default class AutoTranslateView extends React.Component { } } - renderSeparator = () => <Separator /> + renderSeparator = () => { + const { theme } = this.props; + return <Separator theme={theme} />; + } - renderIcon = () => <CustomIcon name='check' size={20} style={sharedStyles.colorPrimary} /> + renderIcon = () => { + const { theme } = this.props; + return <CustomIcon name='check' size={20} style={{ color: themes[theme].tintColor }} />; + } renderSwitch = () => { const { enableAutoTranslate } = this.state; @@ -124,6 +144,7 @@ export default class AutoTranslateView extends React.Component { renderItem = ({ item }) => { const { selectedLanguage } = this.state; + const { theme } = this.props; const { language, name } = item; const isSelected = selectedLanguage === language; @@ -133,35 +154,51 @@ export default class AutoTranslateView extends React.Component { onPress={() => this.saveAutoTranslateLanguage(language)} testID={`auto-translate-view-${ language }`} right={isSelected ? this.renderIcon : null} + theme={theme} /> ); } render() { const { languages } = this.state; + const { theme } = this.props; return ( - <SafeAreaView style={sharedStyles.listSafeArea} testID='auto-translate-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView + style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} + forceInset={{ vertical: 'never' }} + testID='auto-translate-view' + > + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} - contentContainerStyle={styles.contentContainerStyle} + contentContainerStyle={[ + styles.contentContainerStyle, + { + backgroundColor: themes[theme].auxiliaryBackground, + borderColor: themes[theme].separatorColor + } + ]} testID='auto-translate-view-list' > <ListItem title={I18n.t('Enable_Auto_Translate')} testID='auto-translate-view-switch' right={() => this.renderSwitch()} + theme={theme} /> - <SectionSeparator /> + <SectionSeparator theme={theme} /> <FlatList data={languages} extraData={this.state} keyExtractor={item => item.language} renderItem={this.renderItem} ItemSeparatorComponent={this.renderSeparator} + contentContainerStyle={[styles.flatListContainerStyle, { borderColor: themes[theme].separatorColor }]} /> </ScrollView> </SafeAreaView> ); } } + +export default withTheme(AutoTranslateView); diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 46b989922..e05b9a819 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -2,11 +2,12 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { - View, Text, Switch, ScrollView, TextInput, StyleSheet, FlatList + View, Text, Switch, ScrollView, StyleSheet, FlatList } from 'react-native'; import { SafeAreaView } from 'react-navigation'; import equal from 'deep-equal'; +import TextInput from '../presentation/TextInput'; import Loading from '../containers/Loading'; import { createChannelRequest as createChannelRequestAction } from '../actions/createChannel'; import { removeUser as removeUserAction } from '../actions/selectedUsers'; @@ -18,16 +19,16 @@ import UserItem from '../presentation/UserItem'; import { showErrorAlert } from '../utils/info'; import { CustomHeaderButtons, Item } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; -import { COLOR_TEXT_DESCRIPTION, COLOR_WHITE, SWITCH_TRACK_COLOR } from '../constants/colors'; +import { SWITCH_TRACK_COLOR, themes } from '../constants/colors'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; const styles = StyleSheet.create({ container: { - backgroundColor: '#f7f8fa', flex: 1 }, list: { - width: '100%', - backgroundColor: COLOR_WHITE + width: '100%' }, separator: { marginLeft: 60 @@ -39,13 +40,10 @@ const styles = StyleSheet.create({ height: 54, paddingHorizontal: 18, fontSize: 17, - ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal, - backgroundColor: COLOR_WHITE + ...sharedStyles.textRegular }, swithContainer: { height: 54, - backgroundColor: COLOR_WHITE, alignItems: 'center', justifyContent: 'space-between', flexDirection: 'row', @@ -53,8 +51,7 @@ const styles = StyleSheet.create({ }, label: { fontSize: 17, - ...sharedStyles.textMedium, - ...sharedStyles.textColorNormal + ...sharedStyles.textMedium }, invitedHeader: { marginTop: 18, @@ -66,21 +63,20 @@ const styles = StyleSheet.create({ invitedTitle: { fontSize: 18, ...sharedStyles.textSemibold, - ...sharedStyles.textColorNormal, lineHeight: 41 }, invitedCount: { fontSize: 14, - ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription + ...sharedStyles.textRegular } }); class CreateChannelView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const submit = navigation.getParam('submit', () => {}); const showSubmit = navigation.getParam('showSubmit'); return { + ...themedHeader(screenProps.theme), title: I18n.t('Create_Channel'), headerRight: ( showSubmit @@ -107,7 +103,8 @@ class CreateChannelView extends React.Component { user: PropTypes.shape({ id: PropTypes.string, token: PropTypes.string - }) + }), + theme: PropTypes.string }; state = { @@ -127,8 +124,11 @@ class CreateChannelView extends React.Component { channelName, type, readOnly, broadcast } = this.state; const { - error, failure, isFetching, result, users + error, failure, isFetching, result, users, theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.channelName !== channelName) { return true; } @@ -213,18 +213,21 @@ class CreateChannelView extends React.Component { renderSwitch = ({ id, value, label, onValueChange, disabled = false - }) => ( - <View style={styles.swithContainer}> - <Text style={styles.label}>{I18n.t(label)}</Text> - <Switch - value={value} - onValueChange={onValueChange} - testID={`create-channel-${ id }`} - trackColor={SWITCH_TRACK_COLOR} - disabled={disabled} - /> - </View> - ) + }) => { + const { theme } = this.props; + return ( + <View style={[styles.swithContainer, { backgroundColor: themes[theme].backgroundColor }]}> + <Text style={[styles.label, { color: themes[theme].titleText }]}>{I18n.t(label)}</Text> + <Switch + value={value} + onValueChange={onValueChange} + testID={`create-channel-${ id }`} + trackColor={SWITCH_TRACK_COLOR} + disabled={disabled} + /> + </View> + ); + } renderType() { const { type } = this.state; @@ -264,10 +267,13 @@ class CreateChannelView extends React.Component { renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} /> - renderFormSeparator = () => <View style={[sharedStyles.separator, styles.formSeparator]} /> + renderFormSeparator = () => { + const { theme } = this.props; + return <View style={[sharedStyles.separator, styles.formSeparator, { backgroundColor: themes[theme].separatorColor }]} />; + } renderItem = ({ item }) => { - const { baseUrl, user } = this.props; + const { baseUrl, user, theme } = this.props; return ( <UserItem @@ -277,19 +283,27 @@ class CreateChannelView extends React.Component { testID={`create-channel-view-item-${ item.name }`} baseUrl={baseUrl} user={user} + theme={theme} /> ); } renderInvitedList = () => { - const { users } = this.props; + const { users, theme } = this.props; return ( <FlatList data={users} extraData={users} keyExtractor={item => item._id} - style={[styles.list, sharedStyles.separatorVertical]} + style={[ + styles.list, + sharedStyles.separatorVertical, + { + backgroundColor: themes[theme].focusedBackground, + borderColor: themes[theme].separatorColor + } + ]} renderItem={this.renderItem} ItemSeparatorComponent={this.renderSeparator} enableEmptySections @@ -300,30 +314,31 @@ class CreateChannelView extends React.Component { render() { const { channelName } = this.state; - const { users, isFetching } = this.props; + const { users, isFetching, theme } = this.props; const userCount = users.length; return ( <KeyboardView + style={{ backgroundColor: themes[theme].auxiliaryBackground }} contentContainerStyle={[sharedStyles.container, styles.container]} keyboardVerticalOffset={128} > - <StatusBar /> + <StatusBar theme={theme} /> <SafeAreaView testID='create-channel-view' style={styles.container} forceInset={{ vertical: 'never' }}> <ScrollView {...scrollPersistTaps}> - <View style={sharedStyles.separatorVertical}> + <View style={[sharedStyles.separatorVertical, { borderColor: themes[theme].separatorColor }]}> <TextInput autoFocus - style={styles.input} + style={[styles.input, { backgroundColor: themes[theme].backgroundColor }]} label={I18n.t('Channel_Name')} value={channelName} onChangeText={this.onChangeText} placeholder={I18n.t('Channel_Name')} - placeholderTextColor={COLOR_TEXT_DESCRIPTION} returnKeyType='done' testID='create-channel-name' autoCorrect={false} autoCapitalize='none' + theme={theme} underlineColorAndroid='transparent' /> {this.renderFormSeparator()} @@ -334,8 +349,8 @@ class CreateChannelView extends React.Component { {this.renderBroadcast()} </View> <View style={styles.invitedHeader}> - <Text style={styles.invitedTitle}>{I18n.t('Invite')}</Text> - <Text style={styles.invitedCount}>{userCount === 1 ? I18n.t('1_user') : I18n.t('N_users', { n: userCount })}</Text> + <Text style={[styles.invitedTitle, { color: themes[theme].titleText }]}>{I18n.t('Invite')}</Text> + <Text style={[styles.invitedCount, { color: themes[theme].auxiliaryText }]}>{userCount === 1 ? I18n.t('1_user') : I18n.t('N_users', { n: userCount })}</Text> </View> {this.renderInvitedList()} <Loading visible={isFetching} /> @@ -364,4 +379,4 @@ const mapDispatchToProps = dispatch => ({ removeUser: user => dispatch(removeUserAction(user)) }); -export default connect(mapStateToProps, mapDispatchToProps)(CreateChannelView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(CreateChannelView)); diff --git a/app/views/DirectoryView/Options.js b/app/views/DirectoryView/Options.js index 8a9425db3..3763d53a0 100644 --- a/app/views/DirectoryView/Options.js +++ b/app/views/DirectoryView/Options.js @@ -4,12 +4,12 @@ import { } from 'react-native'; import PropTypes from 'prop-types'; -import Touch from '../../utils/touch'; import styles from './styles'; +import Touch from '../../utils/touch'; import { CustomIcon } from '../../lib/Icons'; import Check from '../../containers/Check'; import I18n from '../../i18n'; -import { SWITCH_TRACK_COLOR } from '../../constants/colors'; +import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; const ANIMATION_DURATION = 200; const ANIMATION_PROPS = { @@ -25,7 +25,8 @@ export default class DirectoryOptions extends PureComponent { isFederationEnabled: PropTypes.bool, close: PropTypes.func, changeType: PropTypes.func, - toggleWorkspace: PropTypes.func + toggleWorkspace: PropTypes.func, + theme: PropTypes.string } constructor(props) { @@ -39,7 +40,7 @@ export default class DirectoryOptions extends PureComponent { { toValue: 1, ...ANIMATION_PROPS - }, + } ).start(); } @@ -50,12 +51,12 @@ export default class DirectoryOptions extends PureComponent { { toValue: 0, ...ANIMATION_PROPS - }, + } ).start(() => close()); } renderItem = (itemType) => { - const { changeType, type: propType } = this.props; + const { changeType, type: propType, theme } = this.props; let text = 'Users'; let icon = 'user'; if (itemType === 'channels') { @@ -64,11 +65,15 @@ export default class DirectoryOptions extends PureComponent { } return ( - <Touch style={styles.dropdownItemButton} onPress={() => changeType(itemType)}> + <Touch + onPress={() => changeType(itemType)} + style={styles.dropdownItemButton} + theme={theme} + > <View style={styles.dropdownItemContainer}> - <CustomIcon style={styles.dropdownItemIcon} size={22} name={icon} /> - <Text style={styles.dropdownItemText}>{I18n.t(text)}</Text> - {propType === itemType ? <Check /> : null} + <CustomIcon style={[styles.dropdownItemIcon, { color: themes[theme].bodyText }]} size={22} name={icon} /> + <Text style={[styles.dropdownItemText, { color: themes[theme].bodyText }]}>{I18n.t(text)}</Text> + {propType === itemType ? <Check theme={theme} /> : null} </View> </Touch> ); @@ -83,20 +88,19 @@ export default class DirectoryOptions extends PureComponent { inputRange: [0, 1], outputRange: [0, 0.3] }); - const { globalUsers, toggleWorkspace, isFederationEnabled } = this.props; + const { + globalUsers, toggleWorkspace, isFederationEnabled, theme + } = this.props; return ( <> <TouchableWithoutFeedback onPress={this.close}> - <Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]} /> + <Animated.View style={[styles.backdrop, { backgroundColor: themes[theme].backdropColor, opacity: backdropOpacity }]} /> </TouchableWithoutFeedback> - <Animated.View style={[styles.dropdownContainer, { transform: [{ translateY }] }]}> - <Touch - onPress={this.close} - style={styles.dropdownContainerHeader} - > - <View style={styles.dropdownItemContainer}> - <Text style={styles.dropdownToggleText}>{I18n.t('Search_by')}</Text> - <CustomIcon style={[styles.dropdownItemIcon, styles.inverted]} size={22} name='arrow-down' /> + <Animated.View style={[styles.dropdownContainer, { transform: [{ translateY }], backgroundColor: themes[theme].backgroundColor }]}> + <Touch onPress={this.close} theme={theme}> + <View style={[styles.dropdownContainerHeader, styles.dropdownItemContainer, { borderColor: themes[theme].separatorColor }]}> + <Text style={[styles.dropdownToggleText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Search_by')}</Text> + <CustomIcon style={[styles.dropdownItemIcon, styles.inverted, { color: themes[theme].auxiliaryTintColor }]} size={22} name='arrow-down' /> </View> </Touch> {this.renderItem('channels')} @@ -104,7 +108,7 @@ export default class DirectoryOptions extends PureComponent { {isFederationEnabled ? ( <> - <View style={styles.dropdownSeparator} /> + <View style={[styles.dropdownSeparator, { backgroundColor: themes[theme].separatorColor }]} /> <View style={[styles.dropdownItemContainer, styles.globalUsersContainer]}> <View style={styles.globalUsersTextContainer}> <Text style={styles.dropdownItemText}>{I18n.t('Search_global_users')}</Text> diff --git a/app/views/DirectoryView/index.js b/app/views/DirectoryView/index.js index 9f5018c86..0f91b2caa 100644 --- a/app/views/DirectoryView/index.js +++ b/app/views/DirectoryView/index.js @@ -6,24 +6,35 @@ import { import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; +import Touch from '../../utils/touch'; import RocketChat from '../../lib/rocketchat'; import DirectoryItem from '../../presentation/DirectoryItem'; import sharedStyles from '../Styles'; import I18n from '../../i18n'; -import Touch from '../../utils/touch'; import SearchBox from '../../containers/SearchBox'; import { CustomIcon } from '../../lib/Icons'; import StatusBar from '../../containers/StatusBar'; -import RCActivityIndicator from '../../containers/ActivityIndicator'; +import ActivityIndicator from '../../containers/ActivityIndicator'; +import { CloseModalButton } from '../../containers/HeaderButton'; import debounce from '../../utils/debounce'; import log from '../../utils/log'; import Options from './Options'; +import { withTheme } from '../../theme'; +import { themes } from '../../constants/colors'; import styles from './styles'; +import { themedHeader } from '../../utils/navigation'; class DirectoryView extends React.Component { - static navigationOptions = () => ({ - title: I18n.t('Directory') - }) + static navigationOptions = ({ navigation, screenProps }) => { + const options = { + ...themedHeader(screenProps.theme), + title: I18n.t('Directory') + }; + if (screenProps.split) { + options.headerLeft = <CloseModalButton navigation={navigation} testID='directory-view-close' />; + } + return options; + } static propTypes = { navigation: PropTypes.object, @@ -32,7 +43,8 @@ class DirectoryView extends React.Component { user: PropTypes.shape({ id: PropTypes.string, token: PropTypes.string - }) + }), + theme: PropTypes.string }; constructor(props) { @@ -56,16 +68,6 @@ class DirectoryView extends React.Component { this.setState({ text }); } - onPressItem = (item) => { - const { navigation } = this.props; - try { - const onPressItem = navigation.getParam('onPressItem', () => {}); - onPressItem(item); - } catch (error) { - console.log('DirectoryView -> onPressItem -> error', error); - } - } - // eslint-disable-next-line react/sort-comp load = debounce(async({ newSearch = false }) => { if (newSearch) { @@ -141,6 +143,7 @@ class DirectoryView extends React.Component { renderHeader = () => { const { type } = this.state; + const { theme } = this.props; return ( <> <SearchBox @@ -148,22 +151,30 @@ class DirectoryView extends React.Component { onSubmitEditing={this.search} testID='federation-view-search' /> - <Touch onPress={this.toggleDropdown} testID='federation-view-create-channel'> - <View style={[sharedStyles.separatorVertical, styles.toggleDropdownContainer]}> - <CustomIcon style={styles.toggleDropdownIcon} size={20} name={type === 'users' ? 'user' : 'hashtag'} /> - <Text style={styles.toggleDropdownText}>{type === 'users' ? I18n.t('Users') : I18n.t('Channels')}</Text> - <CustomIcon name='arrow-down' size={20} style={styles.toggleDropdownArrow} /> + <Touch + onPress={this.toggleDropdown} + style={styles.dropdownItemButton} + testID='federation-view-create-channel' + theme={theme} + > + <View style={[sharedStyles.separatorVertical, styles.toggleDropdownContainer, { borderColor: themes[theme].separatorColor }]}> + <CustomIcon style={[styles.toggleDropdownIcon, { color: themes[theme].tintColor }]} size={20} name={type === 'users' ? 'user' : 'hashtag'} /> + <Text style={[styles.toggleDropdownText, { color: themes[theme].tintColor }]}>{type === 'users' ? I18n.t('Users') : I18n.t('Channels')}</Text> + <CustomIcon name='arrow-down' size={20} style={[styles.toggleDropdownArrow, { color: themes[theme].auxiliaryTintColor }]} /> </View> </Touch> </> ); } - renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />; + renderSeparator = () => { + const { theme } = this.props; + return <View style={[sharedStyles.separator, styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; + } renderItem = ({ item, index }) => { const { data, type } = this.state; - const { baseUrl, user } = this.props; + const { baseUrl, user, theme } = this.props; let style; if (index === data.length - 1) { @@ -176,7 +187,8 @@ class DirectoryView extends React.Component { baseUrl, testID: `federation-view-item-${ item.name }`, style, - user + user, + theme }; if (type === 'users') { @@ -205,10 +217,10 @@ class DirectoryView extends React.Component { const { data, loading, showOptionsDropdown, type, globalUsers } = this.state; - const { isFederationEnabled } = this.props; + const { isFederationEnabled, theme } = this.props; return ( - <SafeAreaView style={styles.safeAreaView} testID='directory-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView style={[styles.safeAreaView, { backgroundColor: themes[theme].backgroundColor }]} testID='directory-view' forceInset={{ vertical: 'never' }}> + <StatusBar theme={theme} /> <FlatList data={data} style={styles.list} @@ -219,12 +231,13 @@ class DirectoryView extends React.Component { renderItem={this.renderItem} ItemSeparatorComponent={this.renderSeparator} keyboardShouldPersistTaps='always' - ListFooterComponent={loading ? <RCActivityIndicator /> : null} + ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null} onEndReached={() => this.load({})} /> {showOptionsDropdown ? ( <Options + theme={theme} type={type} globalUsers={globalUsers} close={this.toggleDropdown} @@ -248,4 +261,4 @@ const mapStateToProps = state => ({ isFederationEnabled: state.settings.FEDERATION_Enabled }); -export default connect(mapStateToProps)(DirectoryView); +export default connect(mapStateToProps)(withTheme(DirectoryView)); diff --git a/app/views/DirectoryView/styles.js b/app/views/DirectoryView/styles.js index 327c05cc2..bc2699157 100644 --- a/app/views/DirectoryView/styles.js +++ b/app/views/DirectoryView/styles.js @@ -1,13 +1,10 @@ import { StyleSheet } from 'react-native'; -import { COLOR_WHITE, COLOR_SEPARATOR, COLOR_PRIMARY } from '../../constants/colors'; -import { isIOS } from '../../utils/deviceInfo'; import sharedStyles from '../Styles'; export default StyleSheet.create({ safeAreaView: { - flex: 1, - backgroundColor: isIOS ? '#F7F8FA' : '#E1E5E8' + flex: 1 }, list: { flex: 1 @@ -19,46 +16,38 @@ export default StyleSheet.create({ marginLeft: 60 }, toggleDropdownContainer: { - height: 47, - backgroundColor: COLOR_WHITE, + height: 46, flexDirection: 'row', alignItems: 'center' }, toggleDropdownIcon: { - color: COLOR_PRIMARY, marginLeft: 20, marginRight: 17 }, toggleDropdownText: { flex: 1, - color: COLOR_PRIMARY, fontSize: 17, ...sharedStyles.textRegular }, toggleDropdownArrow: { - ...sharedStyles.textColorDescription, marginRight: 15 }, dropdownContainer: { - backgroundColor: COLOR_WHITE, width: '100%', position: 'absolute', top: 0 }, backdrop: { - ...StyleSheet.absoluteFill, - backgroundColor: '#000000' + ...StyleSheet.absoluteFill }, dropdownContainerHeader: { - height: 47, + height: 46, borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: COLOR_SEPARATOR, alignItems: 'center', - backgroundColor: isIOS ? COLOR_WHITE : '#54585E', flexDirection: 'row' }, dropdownItemButton: { - height: 57, + height: 46, justifyContent: 'center' }, dropdownItemContainer: { @@ -69,32 +58,27 @@ export default StyleSheet.create({ dropdownItemText: { fontSize: 18, flex: 1, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, dropdownItemDescription: { fontSize: 14, flex: 1, marginTop: 2, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, dropdownToggleText: { fontSize: 15, flex: 1, marginLeft: 15, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, dropdownItemIcon: { width: 22, height: 22, - marginHorizontal: 15, - ...sharedStyles.textColorDescription + marginHorizontal: 15 }, dropdownSeparator: { height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR, marginHorizontal: 15, flex: 1 }, diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js index b8e26c083..6eb960e33 100644 --- a/app/views/ForgotPasswordView.js +++ b/app/views/ForgotPasswordView.js @@ -13,17 +13,22 @@ import scrollPersistTaps from '../utils/scrollPersistTaps'; import I18n from '../i18n'; import RocketChat from '../lib/rocketchat'; import StatusBar from '../containers/StatusBar'; +import { withTheme } from '../theme'; +import { themes } from '../constants/colors'; +import { themedHeader } from '../utils/navigation'; -export default class ForgotPasswordView extends React.Component { - static navigationOptions = ({ navigation }) => { +class ForgotPasswordView extends React.Component { + static navigationOptions = ({ navigation, screenProps }) => { const title = navigation.getParam('title', 'Rocket.Chat'); return { - title + title, + ...themedHeader(screenProps.theme) }; } static propTypes = { - navigation: PropTypes.object + navigation: PropTypes.object, + theme: PropTypes.string } state = { @@ -34,6 +39,10 @@ export default class ForgotPasswordView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { email, invalidEmail, isFetching } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.email !== email) { return true; } @@ -76,16 +85,18 @@ export default class ForgotPasswordView extends React.Component { render() { const { invalidEmail, isFetching } = this.state; + const { theme } = this.props; return ( <KeyboardView + style={{ backgroundColor: themes[theme].backgroundColor }} contentContainerStyle={sharedStyles.container} keyboardVerticalOffset={128} > - <StatusBar /> + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} contentContainerStyle={sharedStyles.containerScrollView}> <SafeAreaView style={sharedStyles.container} testID='forgot-password-view' forceInset={{ vertical: 'never' }}> - <Text style={[sharedStyles.loginTitle, sharedStyles.textBold]}>{I18n.t('Forgot_password')}</Text> + <Text style={[sharedStyles.loginTitle, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Forgot_password')}</Text> <TextInput autoFocus placeholder={I18n.t('Email')} @@ -96,6 +107,7 @@ export default class ForgotPasswordView extends React.Component { onSubmitEditing={this.resetPassword} testID='forgot-password-view-email' containerStyle={sharedStyles.inputLastChild} + theme={theme} /> <Button title={I18n.t('Reset_password')} @@ -104,6 +116,7 @@ export default class ForgotPasswordView extends React.Component { testID='forgot-password-view-submit' loading={isFetching} disabled={invalidEmail} + theme={theme} /> </SafeAreaView> </ScrollView> @@ -111,3 +124,5 @@ export default class ForgotPasswordView extends React.Component { ); } } + +export default withTheme(ForgotPasswordView); diff --git a/app/views/JitsiMeetView.js b/app/views/JitsiMeetView.js index 495a796cd..7dec8ebfc 100644 --- a/app/views/JitsiMeetView.js +++ b/app/views/JitsiMeetView.js @@ -33,6 +33,13 @@ class JitsiMeetView extends React.Component { }, 1000); } + componentWillUnmount() { + if (this.jitsiTimeout) { + BackgroundTimer.clearInterval(this.jitsiTimeout); + } + JitsiMeet.endCall(); + } + // Jitsi Update Timeout needs to be called every 10 seconds to make sure // call is not ended and is available to web users. onConferenceJoined = () => { @@ -58,7 +65,7 @@ class JitsiMeetView extends React.Component { <RNJitsiMeetView onConferenceTerminated={this.onConferenceTerminated} onConferenceJoined={this.onConferenceJoined} - style={sharedStyles.root} + style={sharedStyles.container} /> ); } diff --git a/app/views/LanguageView/index.js b/app/views/LanguageView/index.js index d1dc35dbd..7bf6d5c38 100644 --- a/app/views/LanguageView/index.js +++ b/app/views/LanguageView/index.js @@ -15,6 +15,9 @@ import { CustomIcon } from '../../lib/Icons'; import sharedStyles from '../Styles'; import ListItem from '../../containers/ListItem'; import Separator from '../../containers/Separator'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; const LANGUAGES = [ { @@ -42,14 +45,16 @@ const LANGUAGES = [ ]; class LanguageView extends React.Component { - static navigationOptions = () => ({ - title: I18n.t('Change_Language') + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Change_Language'), + ...themedHeader(screenProps.theme) }) static propTypes = { userLanguage: PropTypes.string, navigation: PropTypes.object, - setUser: PropTypes.func + setUser: PropTypes.func, + theme: PropTypes.string } constructor(props) { @@ -62,7 +67,10 @@ class LanguageView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { language, saving } = this.state; - const { userLanguage } = this.props; + const { userLanguage, theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.language !== language) { return true; } @@ -114,13 +122,20 @@ class LanguageView extends React.Component { } } - renderSeparator = () => <Separator /> + renderSeparator = () => { + const { theme } = this.props; + return <Separator theme={theme} />; + } - renderIcon = () => <CustomIcon name='check' size={20} style={sharedStyles.colorPrimary} /> + renderIcon = () => { + const { theme } = this.props; + return <CustomIcon name='check' size={20} style={{ color: themes[theme].tintColor }} />; + } renderItem = ({ item }) => { const { value, label } = item; const { language } = this.state; + const { theme } = this.props; const isSelected = language === value; return ( @@ -129,19 +144,31 @@ class LanguageView extends React.Component { onPress={() => this.submit(value)} testID={`language-view-${ value }`} right={isSelected ? this.renderIcon : null} + theme={theme} /> ); } render() { const { saving } = this.state; + const { theme } = this.props; return ( - <SafeAreaView style={sharedStyles.listSafeArea} testID='language-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView + style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} + forceInset={{ vertical: 'never' }} + testID='language-view' + > + <StatusBar theme={theme} /> <FlatList data={LANGUAGES} keyExtractor={item => item.value} - contentContainerStyle={sharedStyles.listContentContainer} + contentContainerStyle={[ + sharedStyles.listContentContainer, + { + backgroundColor: themes[theme].auxiliaryBackground, + borderColor: themes[theme].separatorColor + } + ]} renderItem={this.renderItem} ItemSeparatorComponent={this.renderSeparator} /> @@ -159,4 +186,4 @@ const mapDispatchToProps = dispatch => ({ setUser: params => dispatch(setUserAction(params)) }); -export default connect(mapStateToProps, mapDispatchToProps)(LanguageView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LanguageView)); diff --git a/app/views/LegalView.js b/app/views/LegalView.js index 8986db3dd..3c16429bb 100644 --- a/app/views/LegalView.js +++ b/app/views/LegalView.js @@ -4,31 +4,29 @@ import { Text, ScrollView, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-navigation'; -import { RectButton } from 'react-native-gesture-handler'; import { connect } from 'react-redux'; +import Touch from '../utils/touch'; import sharedStyles from './Styles'; import scrollPersistTaps from '../utils/scrollPersistTaps'; import I18n from '../i18n'; import DisclosureIndicator from '../containers/DisclosureIndicator'; import StatusBar from '../containers/StatusBar'; -import { COLOR_SEPARATOR, COLOR_WHITE } from '../constants/colors'; +import { themes } from '../constants/colors'; import openLink from '../utils/openLink'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; const styles = StyleSheet.create({ container: { - backgroundColor: '#f7f8fa', flex: 1 }, scroll: { marginTop: 35, - backgroundColor: COLOR_WHITE, - borderColor: COLOR_SEPARATOR, borderTopWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth }, separator: { - backgroundColor: COLOR_SEPARATOR, height: StyleSheet.hairlineWidth, width: '100%', marginLeft: 20 @@ -36,7 +34,6 @@ const styles = StyleSheet.create({ item: { width: '100%', height: 48, - backgroundColor: COLOR_WHITE, paddingLeft: 20, paddingRight: 10, flexDirection: 'row', @@ -45,44 +42,73 @@ const styles = StyleSheet.create({ }, text: { ...sharedStyles.textMedium, - ...sharedStyles.textColorNormal, fontSize: 18 } }); -const Separator = () => <View style={styles.separator} />; +const Separator = ({ theme }) => <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; +Separator.propTypes = { + theme: PropTypes.string +}; class LegalView extends React.Component { - static navigationOptions = () => ({ - title: I18n.t('Legal') + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Legal'), + ...themedHeader(screenProps.theme) }) static propTypes = { - server: PropTypes.string + server: PropTypes.string, + theme: PropTypes.string } onPressItem = ({ route }) => { - const { server } = this.props; + const { server, theme } = this.props; if (!server) { return; } - openLink(`${ server }/${ route }`); + openLink(`${ server }/${ route }`, theme); } - renderItem = ({ text, route, testID }) => ( - <RectButton style={styles.item} onPress={() => this.onPressItem({ route })} testID={testID}> - <Text style={styles.text}>{I18n.t(text)}</Text> - <DisclosureIndicator /> - </RectButton> - ) + renderItem = ({ text, route, testID }) => { + const { theme } = this.props; + return ( + <Touch + style={[styles.item, { backgroundColor: themes[theme].backgroundColor }]} + onPress={() => this.onPressItem({ route })} + testID={testID} + theme={theme} + > + <Text style={[styles.text, { color: themes[theme].titleText }]}>{I18n.t(text)}</Text> + <DisclosureIndicator theme={theme} /> + </Touch> + ); + } render() { + const { theme } = this.props; return ( - <SafeAreaView style={styles.container} testID='legal-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> - <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.scroll}> + <SafeAreaView + style={[ + styles.container, + { backgroundColor: themes[theme].auxiliaryBackground } + ]} + forceInset={{ vertical: 'never' }} + testID='legal-view' + > + <StatusBar theme={theme} /> + <ScrollView + contentContainerStyle={[ + styles.scroll, + { + backgroundColor: themes[theme].backgroundColor, + borderColor: themes[theme].separatorColor + } + ]} + {...scrollPersistTaps} + > {this.renderItem({ text: 'Terms_of_Service', route: 'terms-of-service', testID: 'legal-terms-button' })} - <Separator /> + <Separator theme={theme} /> {this.renderItem({ text: 'Privacy_Policy', route: 'privacy-policy', testID: 'legal-privacy-button' })} </ScrollView> </SafeAreaView> @@ -94,4 +120,4 @@ const mapStateToProps = state => ({ server: state.server.server }); -export default connect(mapStateToProps)(LegalView); +export default connect(mapStateToProps)(withTheme(LegalView)); diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js index 0f55361f8..0fe8361e7 100644 --- a/app/views/LoginSignupView.js +++ b/app/views/LoginSignupView.js @@ -6,9 +6,10 @@ import { import { connect } from 'react-redux'; import { Base64 } from 'js-base64'; import { SafeAreaView } from 'react-navigation'; -import { RectButton, BorderlessButton } from 'react-native-gesture-handler'; +import { BorderlessButton } from 'react-native-gesture-handler'; import equal from 'deep-equal'; +import Touch from '../utils/touch'; import sharedStyles from './Styles'; import scrollPersistTaps from '../utils/scrollPersistTaps'; import random from '../utils/random'; @@ -16,7 +17,10 @@ import Button from '../containers/Button'; import I18n from '../i18n'; import { LegalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; -import { COLOR_SEPARATOR, COLOR_BORDER } from '../constants/colors'; +import { themes } from '../constants/colors'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; +import { isTablet } from '../utils/deviceInfo'; const styles = StyleSheet.create({ container: { @@ -32,7 +36,6 @@ const styles = StyleSheet.create({ serviceButtonContainer: { borderRadius: 2, borderWidth: 1, - borderColor: COLOR_BORDER, width: '100%', height: 48, flexDirection: 'row', @@ -49,7 +52,6 @@ const styles = StyleSheet.create({ }, serviceText: { ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal, fontSize: 16 }, serviceName: { @@ -71,8 +73,7 @@ const styles = StyleSheet.create({ }, separatorLine: { flex: 1, - height: 1, - backgroundColor: COLOR_SEPARATOR + height: 1 }, separatorLineLeft: { marginRight: 15 @@ -89,9 +90,10 @@ const SERVICE_HEIGHT = 58; const SERVICES_COLLAPSED_HEIGHT = 174; class LoginSignupView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const title = navigation.getParam('title', 'Rocket.Chat'); return { + ...themedHeader(screenProps.theme), title, headerRight: <LegalButton testID='welcome-view-more' navigation={navigation} /> }; @@ -104,7 +106,8 @@ class LoginSignupView extends React.Component { Site_Name: PropTypes.string, Gitlab_URL: PropTypes.string, CAS_enabled: PropTypes.bool, - CAS_login_url: PropTypes.string + CAS_login_url: PropTypes.string, + theme: PropTypes.string } constructor(props) { @@ -119,7 +122,9 @@ class LoginSignupView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { collapsed, servicesHeight } = this.state; - const { server, Site_Name, services } = this.props; + const { + server, Site_Name, services, theme + } = this.props; if (nextState.collapsed !== collapsed) { return true; } @@ -132,6 +137,9 @@ class LoginSignupView extends React.Component { if (nextProps.Site_Name !== Site_Name) { return true; } + if (nextProps.theme !== theme) { + return true; + } if (!equal(nextProps.services, services)) { return true; } @@ -249,7 +257,7 @@ class LoginSignupView extends React.Component { onPressCas = () => { const { server, CAS_login_url } = this.props; const ssoToken = random(17); - const url = `${ CAS_login_url }/?service=${ server }/_cas/${ ssoToken }`; + const url = `${ CAS_login_url }?service=${ server }/_cas/${ ssoToken }`; this.openOAuth({ url, ssoToken, authType: 'cas' }); } @@ -312,17 +320,17 @@ class LoginSignupView extends React.Component { renderServicesSeparator = () => { const { collapsed } = this.state; - const { services } = this.props; + const { services, theme } = this.props; const { length } = Object.values(services); if (length > 3) { return ( <View style={styles.servicesTogglerContainer}> - <View style={[styles.separatorLine, styles.separatorLineLeft]} /> + <View style={[styles.separatorLine, styles.separatorLineLeft, { backgroundColor: themes[theme].auxiliaryText }]} /> <BorderlessButton onPress={this.toggleServices}> <Image source={{ uri: 'options' }} style={[styles.servicesToggler, !collapsed && styles.inverted]} /> </BorderlessButton> - <View style={[styles.separatorLine, styles.separatorLineRight]} /> + <View style={[styles.separatorLine, styles.separatorLineRight, { backgroundColor: themes[theme].auxiliaryText }]} /> </View> ); } @@ -360,7 +368,7 @@ class LoginSignupView extends React.Component { break; } name = name.charAt(0).toUpperCase() + name.slice(1); - const { CAS_enabled } = this.props; + const { CAS_enabled, theme } = this.props; let buttonText; if (service.service === 'saml' || (service.service === 'cas' && CAS_enabled)) { buttonText = <Text style={styles.serviceName}>{name}</Text>; @@ -372,12 +380,17 @@ class LoginSignupView extends React.Component { ); } return ( - <RectButton key={service.name} onPress={onPress} style={styles.serviceButton}> - <View style={styles.serviceButtonContainer}> + <Touch + key={service.name} + onPress={onPress} + style={styles.serviceButton} + theme={theme} + > + <View style={[styles.serviceButtonContainer, { borderColor: themes[theme].borderColor }]}> {service.authType === 'oauth' ? <Image source={{ uri: icon }} style={styles.serviceIcon} /> : null} - <Text style={styles.serviceText}>{buttonText}</Text> + <Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text> </View> - </RectButton> + </Touch> ); } @@ -406,9 +419,19 @@ class LoginSignupView extends React.Component { } render() { + const { theme } = this.props; return ( - <ScrollView style={[sharedStyles.containerScrollView, sharedStyles.container, styles.container]} {...scrollPersistTaps}> - <StatusBar /> + <ScrollView + style={[ + sharedStyles.containerScrollView, + sharedStyles.container, + styles.container, + { backgroundColor: themes[theme].backgroundColor }, + isTablet && sharedStyles.tabletScreenContent + ]} + {...scrollPersistTaps} + > + <StatusBar theme={theme} /> <SafeAreaView testID='welcome-view' forceInset={{ vertical: 'never' }} style={styles.safeArea}> {this.renderServices()} {this.renderServicesSeparator()} @@ -416,12 +439,14 @@ class LoginSignupView extends React.Component { title={<Text>{I18n.t('Login_with')} <Text style={{ ...sharedStyles.textBold }}>{I18n.t('email')}</Text></Text>} type='primary' onPress={() => this.login()} + theme={theme} testID='welcome-view-login' /> <Button title={I18n.t('Create_account')} type='secondary' onPress={() => this.register()} + theme={theme} testID='welcome-view-register' /> </SafeAreaView> @@ -439,4 +464,4 @@ const mapStateToProps = state => ({ services: state.login.services }); -export default connect(mapStateToProps)(LoginSignupView); +export default connect(mapStateToProps)(withTheme(LoginSignupView)); diff --git a/app/views/LoginView.js b/app/views/LoginView.js index 87c9cf34f..490f7f852 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -17,8 +17,11 @@ import I18n from '../i18n'; import { loginRequest as loginRequestAction } from '../actions/login'; import { LegalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; -import { COLOR_PRIMARY } from '../constants/colors'; +import { themes } from '../constants/colors'; import { animateNextTransition } from '../utils/layoutAnimation'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; +import { isTablet } from '../utils/deviceInfo'; const styles = StyleSheet.create({ bottomContainer: { @@ -28,12 +31,10 @@ const styles = StyleSheet.create({ }, dontHaveAccount: { ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription, fontSize: 13 }, createAccount: { ...sharedStyles.textSemibold, - color: COLOR_PRIMARY, fontSize: 13 }, loginTitle: { @@ -43,11 +44,12 @@ const styles = StyleSheet.create({ }); class LoginView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const title = navigation.getParam('title', 'Rocket.Chat'); return { title, - headerRight: <LegalButton navigation={navigation} testID='login-view-more' /> + headerRight: <LegalButton navigation={navigation} testID='login-view-more' />, + ...themedHeader(screenProps.theme) }; } @@ -60,7 +62,12 @@ class LoginView extends React.Component { Accounts_PasswordPlaceholder: PropTypes.string, Accounts_PasswordReset: PropTypes.bool, isFetching: PropTypes.bool, - failure: PropTypes.bool + failure: PropTypes.bool, + theme: PropTypes.string + } + + static defaultProps = { + Accounts_PasswordReset: true } static defaultProps = { @@ -98,7 +105,7 @@ class LoginView extends React.Component { user, password, code, showTOTP } = this.state; const { - isFetching, failure, error, Site_Name, Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder + isFetching, failure, error, Site_Name, Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, theme } = this.props; if (nextState.user !== user) { return true; @@ -118,6 +125,9 @@ class LoginView extends React.Component { if (nextProps.failure !== failure) { return true; } + if (nextProps.theme !== theme) { + return true; + } if (nextProps.Site_Name !== Site_Name) { return true; } @@ -171,11 +181,27 @@ class LoginView extends React.Component { } renderTOTP = () => { - const { isFetching } = this.props; + const { isFetching, theme } = this.props; return ( - <SafeAreaView style={sharedStyles.container} testID='login-view' forceInset={{ vertical: 'never' }}> - <Text style={[sharedStyles.loginTitle, sharedStyles.textBold, styles.loginTitle]}>{I18n.t('Two_Factor_Authentication')}</Text> - <Text style={[sharedStyles.loginSubtitle, sharedStyles.textRegular]}>{I18n.t('Whats_your_2fa')}</Text> + <SafeAreaView + style={[ + sharedStyles.container, + isTablet && sharedStyles.tabletScreenContent, + { backgroundColor: themes[theme].backgroundColor } + ]} + testID='login-view' + forceInset={{ vertical: 'never' }} + > + <Text + style={[sharedStyles.loginTitle, sharedStyles.textBold, styles.loginTitle, { color: themes[theme].titleText }]} + > + {I18n.t('Two_Factor_Authentication')} + </Text> + <Text + style={[sharedStyles.loginSubtitle, sharedStyles.textRegular, { color: themes[theme].titleText }]} + > + {I18n.t('Whats_your_2fa')} + </Text> <TextInput inputRef={ref => this.codeInput = ref} autoFocus @@ -186,6 +212,7 @@ class LoginView extends React.Component { onSubmitEditing={this.submit} testID='login-view-totp' containerStyle={sharedStyles.inputLastChild} + theme={theme} /> <Button title={I18n.t('Confirm')} @@ -194,6 +221,7 @@ class LoginView extends React.Component { testID='login-view-submit' loading={isFetching} disabled={!this.valid()} + theme={theme} /> </SafeAreaView> ); @@ -201,11 +229,19 @@ class LoginView extends React.Component { renderUserForm = () => { const { - Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_PasswordReset, isFetching + Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_PasswordReset, isFetching, theme } = this.props; return ( - <SafeAreaView style={sharedStyles.container} testID='login-view' forceInset={{ vertical: 'never' }}> - <Text style={[sharedStyles.loginTitle, sharedStyles.textBold]}>{I18n.t('Login')}</Text> + <SafeAreaView + style={[ + sharedStyles.container, + isTablet && sharedStyles.tabletScreenContent, + { backgroundColor: themes[theme].backgroundColor } + ]} + testID='login-view' + forceInset={{ vertical: 'never' }} + > + <Text style={[sharedStyles.loginTitle, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Login')}</Text> <TextInput autoFocus placeholder={Accounts_EmailOrUsernamePlaceholder || I18n.t('Username_or_email')} @@ -215,6 +251,9 @@ class LoginView extends React.Component { onChangeText={value => this.setState({ user: value })} onSubmitEditing={() => { this.passwordInput.focus(); }} testID='login-view-email' + textContentType='username' + autoCompleteType='username' + theme={theme} /> <TextInput inputRef={(e) => { this.passwordInput = e; }} @@ -226,6 +265,9 @@ class LoginView extends React.Component { onChangeText={value => this.setState({ password: value })} testID='login-view-password' containerStyle={sharedStyles.inputLastChild} + textContentType='password' + autoCompleteType='password' + theme={theme} /> <Button title={I18n.t('Login')} @@ -234,6 +276,7 @@ class LoginView extends React.Component { testID='login-view-submit' loading={isFetching} disabled={!this.valid()} + theme={theme} /> {Accounts_PasswordReset && ( <Button @@ -241,12 +284,13 @@ class LoginView extends React.Component { type='secondary' onPress={this.forgotPassword} testID='login-view-forgot-password' + theme={theme} /> )} <View style={styles.bottomContainer}> - <Text style={styles.dontHaveAccount}>{I18n.t('Dont_Have_An_Account')}</Text> + <Text style={[styles.dontHaveAccount, { color: themes[theme].auxiliaryText }]}>{I18n.t('Dont_Have_An_Account')}</Text> <Text - style={styles.createAccount} + style={[styles.createAccount, { color: themes[theme].actionTintColor }]} onPress={this.register} testID='login-view-register' >{I18n.t('Create_account')} @@ -258,13 +302,15 @@ class LoginView extends React.Component { render() { const { showTOTP } = this.state; + const { theme } = this.props; return ( <KeyboardView + style={{ backgroundColor: themes[theme].backgroundColor }} contentContainerStyle={sharedStyles.container} keyboardVerticalOffset={128} key='login-view' > - <StatusBar /> + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} contentContainerStyle={sharedStyles.containerScrollView}> {!showTOTP ? this.renderUserForm() : null} {showTOTP ? this.renderTOTP() : null} @@ -288,4 +334,4 @@ const mapDispatchToProps = dispatch => ({ loginRequest: params => dispatch(loginRequestAction(params)) }); -export default connect(mapStateToProps, mapDispatchToProps)(LoginView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginView)); diff --git a/app/views/MarkdownTableView.js b/app/views/MarkdownTableView.js new file mode 100644 index 000000000..d62717358 --- /dev/null +++ b/app/views/MarkdownTableView.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import PropTypes from 'prop-types'; + +import I18n from '../i18n'; +import { isIOS } from '../utils/deviceInfo'; +import { themes } from '../constants/colors'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; + +class MarkdownTableView extends React.Component { + static navigationOptions = ({ screenProps }) => ({ + ...themedHeader(screenProps.theme), + title: I18n.t('Table') + }); + + static propTypes = { + navigation: PropTypes.object, + theme: PropTypes.string + } + + render() { + const { navigation, theme } = this.props; + const renderRows = navigation.getParam('renderRows'); + const tableWidth = navigation.getParam('tableWidth'); + + if (isIOS) { + return ( + <ScrollView style={{ backgroundColor: themes[theme].backgroundColor }} contentContainerStyle={{ width: tableWidth }}> + {renderRows()} + </ScrollView> + ); + } + + return ( + <ScrollView style={{ backgroundColor: themes[theme].backgroundColor }}> + <ScrollView horizontal> + {renderRows()} + </ScrollView> + </ScrollView> + ); + } +} + +export default withTheme(MarkdownTableView); diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.js index bea5d01bd..a6d25c236 100644 --- a/app/views/MessagesView/index.js +++ b/app/views/MessagesView/index.js @@ -8,26 +8,31 @@ import ActionSheet from 'react-native-action-sheet'; import styles from './styles'; import Message from '../../containers/message/Message'; -import RCActivityIndicator from '../../containers/ActivityIndicator'; +import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import RocketChat from '../../lib/rocketchat'; import StatusBar from '../../containers/StatusBar'; import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage'; import FileModal from '../../containers/FileModal'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; const ACTION_INDEX = 0; const CANCEL_INDEX = 1; class MessagesView extends React.Component { - static navigationOptions = ({ navigation }) => ({ - title: navigation.state.params.name + static navigationOptions = ({ navigation, screenProps }) => ({ + title: navigation.state.params.name, + ...themedHeader(screenProps.theme) }); static propTypes = { user: PropTypes.object, baseUrl: PropTypes.string, navigation: PropTypes.object, - customEmojis: PropTypes.object + customEmojis: PropTypes.object, + theme: PropTypes.string } constructor(props) { @@ -52,6 +57,10 @@ class MessagesView extends React.Component { const { loading, messages, photoModalVisible, fileLoading } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.loading !== loading) { return true; } @@ -70,7 +79,7 @@ class MessagesView extends React.Component { defineMessagesViewContent = (name) => { const { messages } = this.state; - const { user, baseUrl } = this.props; + const { user, baseUrl, theme } = this.props; const renderItemCommonProps = item => ({ baseUrl, @@ -106,6 +115,7 @@ class MessagesView extends React.Component { description: item.description, ...url }]} + theme={theme} /> ); } @@ -125,6 +135,7 @@ class MessagesView extends React.Component { <Message {...renderItemCommonProps(item)} msg={item.msg} + theme={theme} /> ) }, @@ -144,6 +155,7 @@ class MessagesView extends React.Component { {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} + theme={theme} /> ), actionTitle: I18n.t('Unstar'), @@ -160,6 +172,7 @@ class MessagesView extends React.Component { {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} + theme={theme} /> ), actionTitle: I18n.t('Unpin'), @@ -247,11 +260,20 @@ class MessagesView extends React.Component { this.setState({ fileLoading }); } - renderEmpty = () => ( - <View style={styles.listEmptyContainer} testID={this.content.testID}> - <Text style={styles.noDataFound}>{this.content.noDataMsg}</Text> - </View> - ) + renderEmpty = () => { + const { theme } = this.props; + return ( + <View + style={[ + styles.listEmptyContainer, + { backgroundColor: themes[theme].backgroundColor } + ]} + testID={this.content.testID} + > + <Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{this.content.noDataMsg}</Text> + </View> + ); + } renderItem = ({ item }) => this.content.renderItem(item) @@ -259,22 +281,29 @@ class MessagesView extends React.Component { const { messages, loading, selectedAttachment, photoModalVisible, fileLoading } = this.state; - const { user, baseUrl } = this.props; + const { user, baseUrl, theme } = this.props; if (!loading && messages.length === 0) { return this.renderEmpty(); } return ( - <SafeAreaView style={styles.list} testID={this.content.testID} forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView + style={[ + styles.list, + { backgroundColor: themes[theme].backgroundColor } + ]} + forceInset={{ vertical: 'never' }} + testID={this.content.testID} + > + <StatusBar theme={theme} /> <FlatList data={messages} renderItem={this.renderItem} - style={styles.list} + style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} keyExtractor={item => item._id} onEndReached={this.load} - ListFooterComponent={loading ? <RCActivityIndicator /> : null} + ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null} /> <FileModal attachment={selectedAttachment} @@ -300,4 +329,4 @@ const mapStateToProps = state => ({ customEmojis: state.customEmojis }); -export default connect(mapStateToProps)(MessagesView); +export default connect(mapStateToProps)(withTheme(MessagesView)); diff --git a/app/views/MessagesView/styles.js b/app/views/MessagesView/styles.js index 9bcaa5580..4ef557626 100644 --- a/app/views/MessagesView/styles.js +++ b/app/views/MessagesView/styles.js @@ -1,22 +1,18 @@ import { StyleSheet } from 'react-native'; import sharedStyles from '../Styles'; -import { COLOR_WHITE } from '../../constants/colors'; export default StyleSheet.create({ list: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, listEmptyContainer: { flex: 1, alignItems: 'center', - justifyContent: 'center', - backgroundColor: COLOR_WHITE + justifyContent: 'center' }, noDataFound: { fontSize: 14, - ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal + ...sharedStyles.textRegular } }); diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index 9f4bb8025..e16f3d8ca 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -9,24 +9,24 @@ import equal from 'deep-equal'; import { orderBy } from 'lodash'; import { Q } from '@nozbe/watermelondb'; +import Touch from '../utils/touch'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; import UserItem from '../presentation/UserItem'; import sharedStyles from './Styles'; import I18n from '../i18n'; import log from '../utils/log'; -import Touch from '../utils/touch'; -import { isIOS } from '../utils/deviceInfo'; import SearchBox from '../containers/SearchBox'; import { CustomIcon } from '../lib/Icons'; import { CloseModalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; -import { COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors'; +import { themes } from '../constants/colors'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; const styles = StyleSheet.create({ safeAreaView: { - flex: 1, - backgroundColor: isIOS ? '#F7F8FA' : '#E1E5E8' + flex: 1 }, separator: { marginLeft: 60 @@ -35,25 +35,23 @@ const styles = StyleSheet.create({ marginVertical: 25 }, createChannelContainer: { - height: 47, - backgroundColor: COLOR_WHITE, + height: 46, flexDirection: 'row', alignItems: 'center' }, createChannelIcon: { - color: COLOR_PRIMARY, marginLeft: 18, marginRight: 15 }, createChannelText: { - color: COLOR_PRIMARY, fontSize: 17, ...sharedStyles.textRegular } }); class NewMessageView extends React.Component { - static navigationOptions = ({ navigation }) => ({ + static navigationOptions = ({ navigation, screenProps }) => ({ + ...themedHeader(screenProps.theme), headerLeft: <CloseModalButton navigation={navigation} testID='new-message-view-close' />, title: I18n.t('New_Message') }) @@ -64,7 +62,8 @@ class NewMessageView extends React.Component { user: PropTypes.shape({ id: PropTypes.string, token: PropTypes.string - }) + }), + theme: PropTypes.string }; constructor(props) { @@ -78,6 +77,10 @@ class NewMessageView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { search, chats } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (!equal(nextState.search, search)) { return true; } @@ -138,27 +141,38 @@ class NewMessageView extends React.Component { navigation.navigate('SelectedUsersViewCreateChannel', { nextActionID: 'CREATE_CHANNEL', title: I18n.t('Select_Users') }); } - renderHeader = () => ( - <View> - <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='new-message-view-search' /> - <Touch onPress={this.createChannel} style={styles.createChannelButton} testID='new-message-view-create-channel'> - <View style={[sharedStyles.separatorVertical, styles.createChannelContainer]}> - <CustomIcon style={styles.createChannelIcon} size={24} name='plus' /> - <Text style={styles.createChannelText}>{I18n.t('Create_Channel')}</Text> - </View> - </Touch> - </View> - ) + renderHeader = () => { + const { theme } = this.props; + return ( + <View style={{ backgroundColor: themes[theme].auxiliaryBackground }}> + <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='new-message-view-search' /> + <Touch + onPress={this.createChannel} + style={[styles.createChannelButton, { backgroundColor: themes[theme].backgroundColor }]} + testID='new-message-view-create-channel' + theme={theme} + > + <View style={[sharedStyles.separatorVertical, styles.createChannelContainer, { borderColor: themes[theme].separatorColor }]}> + <CustomIcon style={[styles.createChannelIcon, { color: themes[theme].tintColor }]} size={24} name='plus' /> + <Text style={[styles.createChannelText, { color: themes[theme].tintColor }]}>{I18n.t('Create_Channel')}</Text> + </View> + </Touch> + </View> + ); + } - renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />; + renderSeparator = () => { + const { theme } = this.props; + return <View style={[sharedStyles.separator, styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; + } renderItem = ({ item, index }) => { const { search, chats } = this.state; - const { baseUrl, user } = this.props; + const { baseUrl, user, theme } = this.props; - let style = {}; + let style = { borderColor: themes[theme].separatorColor }; if (index === 0) { - style = { ...sharedStyles.separatorTop }; + style = { ...style, ...sharedStyles.separatorTop }; } if (search.length > 0 && index === search.length - 1) { style = { ...style, ...sharedStyles.separatorBottom }; @@ -175,12 +189,14 @@ class NewMessageView extends React.Component { testID={`new-message-view-item-${ item.name }`} style={style} user={user} + theme={theme} /> ); } renderList = () => { const { search, chats } = this.state; + const { theme } = this.props; return ( <FlatList data={search.length > 0 ? search : chats} @@ -189,17 +205,25 @@ class NewMessageView extends React.Component { ListHeaderComponent={this.renderHeader} renderItem={this.renderItem} ItemSeparatorComponent={this.renderSeparator} + contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} keyboardShouldPersistTaps='always' /> ); } - render = () => ( - <SafeAreaView style={styles.safeAreaView} testID='new-message-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> - {this.renderList()} - </SafeAreaView> - ); + render = () => { + const { theme } = this.props; + return ( + <SafeAreaView + style={[styles.safeAreaView, { backgroundColor: themes[theme].auxiliaryBackground }]} + forceInset={{ vertical: 'never' }} + testID='new-message-view' + > + <StatusBar theme={theme} /> + {this.renderList()} + </SafeAreaView> + ); + } } const mapStateToProps = state => ({ @@ -210,4 +234,4 @@ const mapStateToProps = state => ({ } }); -export default connect(mapStateToProps)(NewMessageView); +export default connect(mapStateToProps)(withTheme(NewMessageView)); diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 92759700c..a925d62ae 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -18,12 +18,13 @@ import TextInput from '../containers/TextInput'; import I18n from '../i18n'; import { verticalScale, moderateScale } from '../utils/scaling'; import KeyboardView from '../presentation/KeyboardView'; -import { isIOS, isNotch } from '../utils/deviceInfo'; +import { isIOS, isNotch, isTablet } from '../utils/deviceInfo'; import { CustomIcon } from '../lib/Icons'; import StatusBar from '../containers/StatusBar'; -import { COLOR_PRIMARY } from '../constants/colors'; +import { themes } from '../constants/colors'; import log from '../utils/log'; import { animateNextTransition } from '../utils/layoutAnimation'; +import { withTheme } from '../theme'; const styles = StyleSheet.create({ image: { @@ -34,7 +35,6 @@ const styles = StyleSheet.create({ }, title: { ...sharedStyles.textBold, - ...sharedStyles.textColorNormal, fontSize: moderateScale(22), letterSpacing: 0, alignSelf: 'center' @@ -56,13 +56,11 @@ const styles = StyleSheet.create({ }, chooseCertificateTitle: { fontSize: 15, - ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription + ...sharedStyles.textRegular }, chooseCertificate: { fontSize: 15, - ...sharedStyles.textSemibold, - ...sharedStyles.textColorHeaderBack + ...sharedStyles.textSemibold } }); @@ -76,6 +74,7 @@ class NewServerView extends React.Component { static propTypes = { navigation: PropTypes.object, server: PropTypes.string, + theme: PropTypes.string, connecting: PropTypes.bool.isRequired, connectServer: PropTypes.func.isRequired } @@ -109,7 +108,7 @@ class NewServerView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { text, certificate } = this.state; - const { connecting } = this.props; + const { connecting, theme } = this.props; if (nextState.text !== text) { return true; } @@ -119,6 +118,9 @@ class NewServerView extends React.Component { if (nextProps.connecting !== connecting) { return true; } + if (nextProps.theme !== theme) { + return true; + } return false; } @@ -165,7 +167,7 @@ class NewServerView extends React.Component { onPress: password => this.saveCertificate({ path, name, password }) } ], - 'secure-text', + 'secure-text' ); } catch (e) { if (!DocumentPicker.isCancel(e)) { @@ -175,7 +177,7 @@ class NewServerView extends React.Component { } completeUrl = (url) => { - url = url && url.trim(); + url = url && url.replace(/\s/g, ''); if (/^(\w|[0-9-_]){3,}$/.test(url) && /^(htt(ps?)?)|(loca((l)?|(lh)?|(lho)?|(lhos)?|(lhost:?\d*)?)$)/.test(url) === false) { @@ -190,7 +192,26 @@ class NewServerView extends React.Component { } } - return url.replace(/\/+$/, ''); + return url.replace(/\/+$/, '').replace(/\\/g, '/'); + } + + uriToPath = uri => uri.replace('file://', ''); + + saveCertificate = (certificate) => { + animateNextTransition(); + this.setState({ certificate }); + } + + handleDelete = () => this.setState({ certificate: null }); // We not need delete file from DocumentPicker because it is a temp file + + showActionSheet = () => { + ActionSheet.showActionSheetWithOptions({ + options: this.options, + cancelButtonIndex: this.CANCEL_INDEX, + destructiveButtonIndex: this.DELETE_INDEX + }, (actionIndex) => { + if (actionIndex === this.DELETE_INDEX) { this.handleDelete(); } + }); } uriToPath = uri => uri.replace('file://', ''); @@ -213,7 +234,7 @@ class NewServerView extends React.Component { } renderBack = () => { - const { navigation } = this.props; + const { navigation, theme } = this.props; let top = 15; if (isIOS) { @@ -228,7 +249,7 @@ class NewServerView extends React.Component { <CustomIcon name='back' size={30} - color={COLOR_PRIMARY} + color={themes[theme].tintColor} /> </TouchableOpacity> ); @@ -236,50 +257,75 @@ class NewServerView extends React.Component { renderCertificatePicker = () => { const { certificate } = this.state; + const { theme } = this.props; return ( <View style={styles.certificatePicker}> - <Text style={styles.chooseCertificateTitle}>{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}</Text> - <TouchableOpacity onPress={certificate ? this.showActionSheet : this.chooseCertificate} testID='new-server-choose-certificate'> - <Text style={styles.chooseCertificate}>{certificate ? certificate.name : I18n.t('Apply_Your_Certificate')}</Text> + <Text + style={[ + styles.chooseCertificateTitle, + { color: themes[theme].auxiliaryText } + ]} + > + {certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')} + </Text> + <TouchableOpacity + onPress={certificate ? this.showActionSheet : this.chooseCertificate} + testID='new-server-choose-certificate' + > + <Text + style={[ + styles.chooseCertificate, + { color: themes[theme].tintColor } + ]} + > + {certificate ? certificate.name : I18n.t('Apply_Your_Certificate')} + </Text> </TouchableOpacity> </View> ); } render() { - const { connecting } = this.props; + const { connecting, theme } = this.props; const { text, autoFocus } = this.state; return ( <KeyboardView + style={{ backgroundColor: themes[theme].backgroundColor }} contentContainerStyle={sharedStyles.container} keyboardVerticalOffset={128} key='login-view' > - <StatusBar light /> + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} contentContainerStyle={sharedStyles.containerScrollView}> <SafeAreaView style={sharedStyles.container} testID='new-server-view'> <Image style={styles.image} source={{ uri: 'new_server' }} /> - <Text style={styles.title}>{I18n.t('Sign_in_your_server')}</Text> - <TextInput - autoFocus={autoFocus} - containerStyle={styles.inputContainer} - placeholder={defaultServer} - value={text} - returnKeyType='send' - onChangeText={this.onChangeText} - testID='new-server-view-input' - onSubmitEditing={this.submit} - clearButtonMode='while-editing' - /> - <Button - title={I18n.t('Connect')} - type='primary' - onPress={this.submit} - disabled={!text} - loading={connecting} - testID='new-server-view-button' - /> - { isIOS ? this.renderCertificatePicker() : null } + <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Sign_in_your_server')}</Text> + <View style={isTablet && sharedStyles.tabletScreenContent}> + <TextInput + autoFocus={autoFocus} + containerStyle={styles.inputContainer} + placeholder={defaultServer} + value={text} + returnKeyType='send' + onChangeText={this.onChangeText} + testID='new-server-view-input' + onSubmitEditing={this.submit} + clearButtonMode='while-editing' + keyboardType='url' + textContentType='URL' + theme={theme} + /> + <Button + title={I18n.t('Connect')} + type='primary' + onPress={this.submit} + disabled={!text} + loading={connecting} + testID='new-server-view-button' + theme={theme} + /> + { isIOS ? this.renderCertificatePicker() : null } + </View> </SafeAreaView> </ScrollView> {this.renderBack()} @@ -296,4 +342,4 @@ const mapDispatchToProps = dispatch => ({ connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)) }); -export default connect(mapStateToProps, mapDispatchToProps)(NewServerView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewServerView)); diff --git a/app/views/NotificationPreferencesView/index.js b/app/views/NotificationPreferencesView/index.js index 423d2a7fb..456b39993 100644 --- a/app/views/NotificationPreferencesView/index.js +++ b/app/views/NotificationPreferencesView/index.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import RNPickerSelect from 'react-native-picker-select'; import { SafeAreaView } from 'react-navigation'; -import { SWITCH_TRACK_COLOR } from '../../constants/colors'; +import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import StatusBar from '../../containers/StatusBar'; import ListItem from '../../containers/ListItem'; import Separator from '../../containers/Separator'; @@ -16,19 +16,58 @@ import styles from './styles'; import sharedStyles from '../Styles'; import RocketChat from '../../lib/rocketchat'; import log from '../../utils/log'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; -const SectionTitle = React.memo(({ title }) => <Text style={styles.sectionTitle}>{title}</Text>); +const SectionTitle = React.memo(({ title, theme }) => ( + <Text + style={[ + styles.sectionTitle, + { + backgroundColor: themes[theme].auxiliaryBackground, + color: themes[theme].infoText + } + ]} + > + {title} + </Text> +)); -const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />); +const SectionSeparator = React.memo(({ theme }) => ( + <View + style={[ + styles.sectionSeparatorBorder, + { backgroundColor: themes[theme].auxiliaryBackground } + ]} + /> +)); -const Info = React.memo(({ info }) => <Text style={styles.infoText}>{info}</Text>); +const Info = React.memo(({ info, theme }) => ( + <Text + style={[ + styles.infoText, + { + color: themes[theme].infoText, + backgroundColor: themes[theme].auxiliaryBackground + } + ]} + > + {info} + </Text> +)); SectionTitle.propTypes = { - title: PropTypes.string + title: PropTypes.string, + theme: PropTypes.string +}; + +SectionSeparator.propTypes = { + theme: PropTypes.string }; Info.propTypes = { - info: PropTypes.string + info: PropTypes.string, + theme: PropTypes.string }; const OPTIONS = { @@ -100,14 +139,16 @@ const OPTIONS = { }] }; -export default class NotificationPreferencesView extends React.Component { - static navigationOptions = () => ({ - title: I18n.t('Notification_Preferences') +class NotificationPreferencesView extends React.Component { + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Notification_Preferences'), + ...themedHeader(screenProps.theme) }) static propTypes = { - navigation: PropTypes.object - } + navigation: PropTypes.object, + theme: PropTypes.string + }; constructor(props) { super(props); @@ -141,9 +182,6 @@ export default class NotificationPreferencesView extends React.Component { } onValueChangeSwitch = async(key, value) => { - const { room: newRoom } = this.state; - newRoom[key] = value; - this.setState({ room: newRoom }); const params = { [key]: value ? '1' : '0' }; @@ -155,9 +193,6 @@ export default class NotificationPreferencesView extends React.Component { } onValueChangePicker = async(key, value) => { - const { room: newRoom } = this.state; - newRoom[key] = value; - this.setState({ room: newRoom }); const params = { [key]: value.toString() }; @@ -170,12 +205,13 @@ export default class NotificationPreferencesView extends React.Component { renderPicker = (key) => { const { room } = this.state; + const { theme } = this.props; return ( <RNPickerSelect testID={key} style={{ viewContainer: styles.viewContainer }} value={room[key]} - textInputProps={{ style: styles.pickerText }} + textInputProps={{ style: { ...styles.pickerText, color: themes[theme].actionTintColor } }} useNativeAndroidPickerStyle={false} placeholder={{}} onValueChange={value => this.onValueChangePicker(key, value)} @@ -198,105 +234,118 @@ export default class NotificationPreferencesView extends React.Component { render() { const { room } = this.state; + const { theme } = this.props; return ( - <SafeAreaView style={sharedStyles.listSafeArea} testID='notification-preference-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView style={sharedStyles.container} testID='notification-preference-view' forceInset={{ vertical: 'never' }}> + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} + style={{ backgroundColor: themes[theme].auxiliaryBackground }} contentContainerStyle={styles.contentContainer} showsVerticalScrollIndicator={false} testID='notification-preference-view-list' > - <Separator /> + <Separator theme={theme} /> <ListItem title={I18n.t('Receive_Notification')} testID='notification-preference-view-receive-notification' right={() => this.renderSwitch('disableNotifications')} + theme={theme} /> - <Separator /> - <Info info={I18n.t('Receive_notifications_from', { name: room.name })} /> - <SectionSeparator /> + <Separator theme={theme} /> + <Info info={I18n.t('Receive_notifications_from', { name: room.name })} theme={theme} /> + <SectionSeparator theme={theme} /> - <Separator /> + <Separator theme={theme} /> <ListItem title={I18n.t('Receive_Group_Mentions')} testID='notification-preference-view-group-mentions' right={() => this.renderSwitch('muteGroupMentions')} + theme={theme} /> - <Separator /> - <Info info={I18n.t('Receive_Group_Mentions_Info')} /> + <Separator theme={theme} /> + <Info info={I18n.t('Receive_Group_Mentions_Info')} theme={theme} /> - <SectionSeparator /> - <Separator /> + <SectionSeparator theme={theme} /> + <Separator theme={theme} /> <ListItem title={I18n.t('Show_Unread_Counter')} testID='notification-preference-view-unread-count' right={() => this.renderSwitch('hideUnreadStatus')} + theme={theme} /> - <Separator /> - <Info info={I18n.t('Show_Unread_Counter_Info')} /> + <Separator theme={theme} /> + <Info info={I18n.t('Show_Unread_Counter_Info')} theme={theme} /> - <SectionSeparator /> - <SectionTitle title={I18n.t('IN_APP_AND_DESKTOP')} /> - <Separator /> + <SectionSeparator theme={theme} /> + <SectionTitle title={I18n.t('IN_APP_AND_DESKTOP')} theme={theme} /> + <Separator theme={theme} /> <ListItem title={I18n.t('Alert')} testID='notification-preference-view-alert' right={() => this.renderPicker('desktopNotifications')} + theme={theme} /> - <Separator /> - <Info info={I18n.t('In_App_and_Desktop_Alert_info')} /> + <Separator theme={theme} /> + <Info info={I18n.t('In_App_and_Desktop_Alert_info')} theme={theme} /> - <SectionSeparator /> - <SectionTitle title={I18n.t('PUSH_NOTIFICATIONS')} /> - <Separator /> + <SectionSeparator theme={theme} /> + <SectionTitle title={I18n.t('PUSH_NOTIFICATIONS')} theme={theme} /> + <Separator theme={theme} /> <ListItem title={I18n.t('Alert')} testID='notification-preference-view-push-notification' right={() => this.renderPicker('mobilePushNotifications')} + theme={theme} /> - <Separator /> - <Info info={I18n.t('Push_Notifications_Alert_Info')} /> + <Separator theme={theme} /> + <Info info={I18n.t('Push_Notifications_Alert_Info')} theme={theme} /> - <SectionSeparator /> - <SectionTitle title={I18n.t('DESKTOP_OPTIONS')} /> - <Separator /> + <SectionSeparator theme={theme} /> + <SectionTitle title={I18n.t('DESKTOP_OPTIONS')} theme={theme} /> + <Separator theme={theme} /> <ListItem title={I18n.t('Audio')} testID='notification-preference-view-audio' right={() => this.renderPicker('audioNotifications')} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> <ListItem title={I18n.t('Sound')} testID='notification-preference-view-sound' right={() => this.renderPicker('audioNotificationValue')} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> <ListItem title={I18n.t('Notification_Duration')} testID='notification-preference-view-notification-duration' right={() => this.renderPicker('desktopNotificationDuration')} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> - <SectionSeparator /> - <SectionTitle title={I18n.t('EMAIL')} /> - <Separator /> + <SectionSeparator theme={theme} /> + <SectionTitle title={I18n.t('EMAIL')} theme={theme} /> + <Separator theme={theme} /> <ListItem title={I18n.t('Alert')} testID='notification-preference-view-email-alert' right={() => this.renderPicker('emailNotifications')} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> - <View style={styles.marginBottom} /> + <View style={[styles.marginBottom, { backgroundColor: themes[theme].auxiliaryBackground }]} /> </ScrollView> </SafeAreaView> ); } } + +export default withTheme(NotificationPreferencesView); diff --git a/app/views/NotificationPreferencesView/styles.js b/app/views/NotificationPreferencesView/styles.js index d01d019e6..61b39cb6d 100644 --- a/app/views/NotificationPreferencesView/styles.js +++ b/app/views/NotificationPreferencesView/styles.js @@ -1,43 +1,34 @@ import { StyleSheet } from 'react-native'; -import { COLOR_BACKGROUND_CONTAINER, COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ sectionSeparatorBorder: { - backgroundColor: COLOR_BACKGROUND_CONTAINER, height: 10 }, marginBottom: { - height: 30, - backgroundColor: COLOR_BACKGROUND_CONTAINER + height: 30 }, contentContainer: { - backgroundColor: COLOR_WHITE, marginVertical: 10 }, infoText: { ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal, fontSize: 13, paddingHorizontal: 15, - paddingVertical: 10, - backgroundColor: COLOR_BACKGROUND_CONTAINER + paddingVertical: 10 }, sectionTitle: { ...sharedStyles.separatorBottom, paddingHorizontal: 15, - backgroundColor: COLOR_BACKGROUND_CONTAINER, paddingVertical: 10, - fontSize: 14, - ...sharedStyles.textColorNormal + fontSize: 14 }, viewContainer: { justifyContent: 'center' }, pickerText: { ...sharedStyles.textRegular, - fontSize: 16, - color: COLOR_PRIMARY + fontSize: 16 } }); diff --git a/app/views/OnboardingView/Button.js b/app/views/OnboardingView/Button.js index c890bc0f0..4fb8ca02d 100644 --- a/app/views/OnboardingView/Button.js +++ b/app/views/OnboardingView/Button.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { View, Text, TouchableWithoutFeedback } from 'react-native'; import styles from './styles'; +import { themes } from '../../constants/colors'; import DisclosureIndicator from '../../containers/DisclosureIndicator'; export default class Button extends React.PureComponent { @@ -10,6 +11,7 @@ export default class Button extends React.PureComponent { title: PropTypes.string, subtitle: PropTypes.string, type: PropTypes.string, + theme: PropTypes.string, icon: PropTypes.node.isRequired, testID: PropTypes.string.isRequired, onPress: PropTypes.func @@ -27,10 +29,15 @@ export default class Button extends React.PureComponent { render() { const { - title, subtitle, type, onPress, icon, testID + title, subtitle, type, onPress, icon, testID, theme } = this.props; const { active } = this.state; const activeStyle = active && styles.buttonActive; + const isPrimary = (type === 'primary'); + const buttonContainerStyle = { + backgroundColor: isPrimary ? themes[theme].actionTintColor : themes[theme].focusedBackground, + borderColor: isPrimary ? themes[theme].actionTintColor : themes[theme].borderColor + }; return ( <TouchableWithoutFeedback onPress={onPress} @@ -38,15 +45,15 @@ export default class Button extends React.PureComponent { onPressOut={() => this.setState({ active: false })} testID={testID} > - <View style={[styles.buttonContainer, styles[`button_container_${ type }`]]}> + <View style={[styles.buttonContainer, buttonContainerStyle]}> <View style={styles.buttonIconContainer}> {icon} </View> <View style={styles.buttonCenter}> - <Text style={[styles.buttonTitle, styles[`button_text_${ type }`], activeStyle]}>{title}</Text> - {subtitle ? <Text style={[styles.buttonSubtitle, activeStyle]}>{subtitle}</Text> : null} + <Text style={[styles.buttonTitle, { color: isPrimary ? themes[theme].buttonText : themes[theme].tintColor }, activeStyle]}>{title}</Text> + {subtitle ? <Text style={[styles.buttonSubtitle, activeStyle, { color: themes[theme].auxiliaryText }]}>{subtitle}</Text> : null} </View> - {type === 'secondary' ? <DisclosureIndicator /> : null} + {type === 'secondary' ? <DisclosureIndicator theme={theme} /> : null} </View> </TouchableWithoutFeedback> ); diff --git a/app/views/OnboardingView/index.js b/app/views/OnboardingView/index.js index c54a6eea6..c2fa36674 100644 --- a/app/views/OnboardingView/index.js +++ b/app/views/OnboardingView/index.js @@ -13,11 +13,13 @@ import I18n from '../../i18n'; import openLink from '../../utils/openLink'; import Button from './Button'; import styles from './styles'; -import { isIOS, isNotch } from '../../utils/deviceInfo'; +import { isIOS, isNotch, isTablet } from '../../utils/deviceInfo'; import EventEmitter from '../../utils/events'; import { CustomIcon } from '../../lib/Icons'; import StatusBar from '../../containers/StatusBar'; -import { COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import sharedStyles from '../Styles'; class OnboardingView extends React.Component { static navigationOptions = () => ({ @@ -31,14 +33,17 @@ class OnboardingView extends React.Component { currentServer: PropTypes.string, initAdd: PropTypes.func, finishAdd: PropTypes.func, - appStart: PropTypes.func + appStart: PropTypes.func, + theme: PropTypes.string } constructor(props) { super(props); BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); this.previousServer = props.navigation.getParam('previousServer'); - Orientation.lockToPortrait(); + if (!isTablet) { + Orientation.lockToPortrait(); + } } componentDidMount() { @@ -49,7 +54,11 @@ class OnboardingView extends React.Component { EventEmitter.addEventListener('NewServer', this.handleNewServerEvent); } - shouldComponentUpdate() { + shouldComponentUpdate(nextProps) { + const { theme } = this.props; + if (theme !== nextProps.theme) { + return true; + } return false; } @@ -97,10 +106,12 @@ class OnboardingView extends React.Component { } createWorkspace = () => { - openLink('https://cloud.rocket.chat/trial'); + const { theme } = this.props; + openLink('https://cloud.rocket.chat/trial', theme); } renderClose = () => { + const { theme } = this.props; if (this.previousServer) { let top = 15; if (isIOS) { @@ -115,7 +126,7 @@ class OnboardingView extends React.Component { <CustomIcon name='cross' size={30} - color={COLOR_PRIMARY} + color={themes[theme].actionTintColor} /> </TouchableOpacity> ); @@ -124,19 +135,27 @@ class OnboardingView extends React.Component { } render() { + const { theme } = this.props; return ( - <SafeAreaView style={styles.container} testID='onboarding-view'> - <StatusBar light /> + <SafeAreaView + style={[ + styles.container, + { backgroundColor: themes[theme].backgroundColor } + ]} + testID='onboarding-view' + > + <StatusBar theme={theme} /> <Image style={styles.onboarding} source={{ uri: 'onboarding' }} fadeDuration={0} /> - <Text style={styles.title}>{I18n.t('Welcome_to_RocketChat')}</Text> - <Text style={styles.subtitle}>{I18n.t('Open_Source_Communication')}</Text> - <View style={styles.buttonsContainer}> + <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Welcome_to_RocketChat')}</Text> + <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]}>{I18n.t('Open_Source_Communication')}</Text> + <View style={[styles.buttonsContainer, isTablet && sharedStyles.tabletScreenContent]}> <Button type='secondary' title={I18n.t('Connect_to_a_server')} - icon={<CustomIcon name='permalink' size={30} color={COLOR_PRIMARY} />} + icon={<CustomIcon name='permalink' size={30} color={themes[theme].actionTintColor} />} onPress={this.connectServer} testID='connect-server-button' + theme={theme} /> <Button type='secondary' @@ -145,13 +164,15 @@ class OnboardingView extends React.Component { icon={<Image source={{ uri: 'logo_onboarding' }} style={{ width: 32, height: 27 }} fadeDuration={0} />} onPress={this.joinCommunity} testID='join-community-button' + theme={theme} /> <Button type='primary' title={I18n.t('Create_a_new_workspace')} - icon={<CustomIcon name='plus' size={30} color={COLOR_WHITE} />} + icon={<CustomIcon name='plus' size={30} color={themes[theme].buttonText} />} onPress={this.createWorkspace} testID='create-workspace-button' + theme={theme} /> </View> {this.renderClose()} @@ -172,4 +193,4 @@ const mapDispatchToProps = dispatch => ({ appStart: root => dispatch(appStartAction(root)) }); -export default connect(mapStateToProps, mapDispatchToProps)(OnboardingView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(OnboardingView)); diff --git a/app/views/OnboardingView/styles.js b/app/views/OnboardingView/styles.js index 65cfac5c0..2de8f4dde 100644 --- a/app/views/OnboardingView/styles.js +++ b/app/views/OnboardingView/styles.js @@ -1,25 +1,14 @@ import { StyleSheet } from 'react-native'; import { verticalScale, moderateScale } from '../../utils/scaling'; +import { isTablet } from '../../utils/deviceInfo'; import sharedStyles from '../Styles'; -import { COLOR_PRIMARY, COLOR_BORDER, COLOR_WHITE } from '../../constants/colors'; - -const colors = { - backgroundPrimary: COLOR_PRIMARY, - backgroundSecondary: 'white', - - textColorPrimary: 'white', - textColorSecondary: COLOR_PRIMARY, - - borderColorPrimary: COLOR_PRIMARY, - borderColorSecondary: COLOR_BORDER -}; export default StyleSheet.create({ container: { flex: 1, flexDirection: 'column', - backgroundColor: COLOR_WHITE + justifyContent: isTablet ? 'center' : 'flex-start' }, onboarding: { alignSelf: 'center', @@ -32,7 +21,6 @@ export default StyleSheet.create({ }, title: { ...sharedStyles.textBold, - ...sharedStyles.textColorNormal, letterSpacing: 0, fontSize: moderateScale(24), alignSelf: 'center', @@ -68,7 +56,6 @@ export default StyleSheet.create({ }, buttonSubtitle: { ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription, fontSize: 15 }, buttonIconContainer: { @@ -84,20 +71,6 @@ export default StyleSheet.create({ buttonActive: { opacity: 0.5 }, - button_container_primary: { - backgroundColor: colors.backgroundPrimary, - borderColor: colors.borderColorPrimary - }, - button_container_secondary: { - backgroundColor: colors.backgroundSecondary, - borderColor: colors.borderColorSecondary - }, - button_text_primary: { - color: colors.textColorPrimary - }, - button_text_secondary: { - color: colors.textColorSecondary - }, closeModal: { position: 'absolute', left: 15 diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index aab3c76d3..1f89d017f 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -7,8 +7,10 @@ import SHA256 from 'js-sha256'; import ImagePicker from 'react-native-image-crop-picker'; import RNPickerSelect from 'react-native-picker-select'; import { SafeAreaView } from 'react-navigation'; +import { HeaderBackButton } from 'react-navigation-stack'; import equal from 'deep-equal'; +import Touch from '../../utils/touch'; import KeyboardView from '../../presentation/KeyboardView'; import sharedStyles from '../Styles'; import styles from './styles'; @@ -22,16 +24,25 @@ import log from '../../utils/log'; import I18n from '../../i18n'; import Button from '../../containers/Button'; import Avatar from '../../containers/Avatar'; -import Touch from '../../utils/touch'; import { setUser as setUserAction } from '../../actions/login'; import { CustomIcon } from '../../lib/Icons'; import { DrawerButton } from '../../containers/HeaderButton'; import StatusBar from '../../containers/StatusBar'; -import { COLOR_TEXT } from '../../constants/colors'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; class ProfileView extends React.Component { - static navigationOptions = ({ navigation }) => ({ - headerLeft: <DrawerButton navigation={navigation} />, + static navigationOptions = ({ navigation, screenProps }) => ({ + ...themedHeader(screenProps.theme), + headerLeft: screenProps.split ? ( + <HeaderBackButton + onPress={() => navigation.navigate('SettingsView')} + tintColor={themes[screenProps.theme].headerTintColor} + /> + ) : ( + <DrawerButton navigation={navigation} /> + ), title: I18n.t('Profile') }) @@ -39,7 +50,8 @@ class ProfileView extends React.Component { baseUrl: PropTypes.string, user: PropTypes.object, Accounts_CustomFields: PropTypes.string, - setUser: PropTypes.func + setUser: PropTypes.func, + theme: PropTypes.string } state = { @@ -249,26 +261,25 @@ class ProfileView extends React.Component { renderAvatarButton = ({ key, child, onPress, disabled = false - }) => ( - <Touch - key={key} - testID={key} - onPress={onPress} - underlayColor='rgba(255, 255, 255, 0.5)' - activeOpacity={0.3} - disabled={disabled} - > - <View - style={[styles.avatarButton, { opacity: disabled ? 0.5 : 1 }]} + }) => { + const { theme } = this.props; + return ( + <Touch + key={key} + testID={key} + onPress={onPress} + style={[styles.avatarButton, { opacity: disabled ? 0.5 : 1 }, { backgroundColor: themes[theme].borderColor }]} + enabled={!disabled} + theme={theme} > {child} - </View> - </Touch> - ) + </Touch> + ); + } renderAvatarButtons = () => { const { avatarUrl, avatarSuggestions } = this.state; - const { user, baseUrl } = this.props; + const { user, baseUrl, theme } = this.props; return ( <View style={styles.avatarButtons}> @@ -278,12 +289,12 @@ class ProfileView extends React.Component { key: 'profile-view-reset-avatar' })} {this.renderAvatarButton({ - child: <CustomIcon name='upload' size={30} color={COLOR_TEXT} />, + child: <CustomIcon name='upload' size={30} color={themes[theme].bodyText} />, onPress: () => this.pickImage(), key: 'profile-view-upload-avatar' })} {this.renderAvatarButton({ - child: <CustomIcon name='permalink' size={30} color={COLOR_TEXT} />, + child: <CustomIcon name='permalink' size={30} color={themes[theme].bodyText} />, onPress: () => this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' }), disabled: !avatarUrl, key: 'profile-view-avatar-url-button' @@ -304,7 +315,7 @@ class ProfileView extends React.Component { renderCustomFields = () => { const { customFields } = this.state; - const { Accounts_CustomFields } = this.props; + const { Accounts_CustomFields, theme } = this.props; if (!Accounts_CustomFields) { return null; @@ -331,6 +342,7 @@ class ProfileView extends React.Component { placeholder={key} value={customFields[key]} testID='settings-view-language' + theme={theme} /> </RNPickerSelect> ); @@ -354,6 +366,7 @@ class ProfileView extends React.Component { } this.avatarUrl.focus(); }} + theme={theme} /> ); }); @@ -366,20 +379,23 @@ class ProfileView extends React.Component { const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving, showPasswordAlert } = this.state; - const { baseUrl, user } = this.props; + const { + baseUrl, user, theme, Accounts_CustomFields + } = this.props; return ( <KeyboardView + style={{ backgroundColor: themes[theme].auxiliaryBackground }} contentContainerStyle={sharedStyles.container} keyboardVerticalOffset={128} > - <StatusBar /> - <ScrollView - contentContainerStyle={sharedStyles.containerScrollView} - testID='profile-view-list' - {...scrollPersistTaps} - > - <SafeAreaView style={sharedStyles.container} testID='profile-view' forceInset={{ vertical: 'never' }}> + <StatusBar theme={theme} /> + <SafeAreaView style={sharedStyles.container} testID='profile-view' forceInset={{ vertical: 'never' }}> + <ScrollView + contentContainerStyle={sharedStyles.containerScrollView} + testID='profile-view-list' + {...scrollPersistTaps} + > <View style={styles.avatarContainer} testID='profile-view-avatar'> <Avatar text={username} @@ -398,6 +414,7 @@ class ProfileView extends React.Component { onChangeText={value => this.setState({ name: value })} onSubmitEditing={() => { this.username.focus(); }} testID='profile-view-name' + theme={theme} /> <RCTextInput inputRef={(e) => { this.username = e; }} @@ -407,6 +424,7 @@ class ProfileView extends React.Component { onChangeText={value => this.setState({ username: value })} onSubmitEditing={() => { this.email.focus(); }} testID='profile-view-username' + theme={theme} /> <RCTextInput inputRef={(e) => { this.email = e; }} @@ -416,6 +434,7 @@ class ProfileView extends React.Component { onChangeText={value => this.setState({ email: value })} onSubmitEditing={() => { this.newPassword.focus(); }} testID='profile-view-email' + theme={theme} /> <RCTextInput inputRef={(e) => { this.newPassword = e; }} @@ -424,13 +443,14 @@ class ProfileView extends React.Component { value={newPassword} onChangeText={value => this.setState({ newPassword: value })} onSubmitEditing={() => { - if (Object.keys(customFields).length) { + if (Accounts_CustomFields && Object.keys(customFields).length) { return this[Object.keys(customFields)[0]].focus(); } this.avatarUrl.focus(); }} secureTextEntry testID='profile-view-new-password' + theme={theme} /> {this.renderCustomFields()} <RCTextInput @@ -441,6 +461,7 @@ class ProfileView extends React.Component { onChangeText={value => this.setState({ avatarUrl: value })} onSubmitEditing={this.submit} testID='profile-view-avatar-url' + theme={theme} /> {this.renderAvatarButtons()} <Button @@ -450,6 +471,7 @@ class ProfileView extends React.Component { disabled={!this.formIsChanged()} testID='profile-view-submit' loading={saving} + theme={theme} /> <Dialog.Container visible={showPasswordAlert}> <Dialog.Title> @@ -467,8 +489,8 @@ class ProfileView extends React.Component { <Dialog.Button label={I18n.t('Cancel')} onPress={this.closePasswordAlert} /> <Dialog.Button label={I18n.t('Save')} onPress={this.submit} /> </Dialog.Container> - </SafeAreaView> - </ScrollView> + </ScrollView> + </SafeAreaView> </KeyboardView> ); } @@ -491,4 +513,4 @@ const mapDispatchToProps = dispatch => ({ setUser: params => dispatch(setUserAction(params)) }); -export default connect(mapStateToProps, mapDispatchToProps)(ProfileView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(ProfileView)); diff --git a/app/views/ReadReceiptView/index.js b/app/views/ReadReceiptView/index.js index 7d01574dd..d5d90fcd1 100644 --- a/app/views/ReadReceiptView/index.js +++ b/app/views/ReadReceiptView/index.js @@ -8,22 +8,27 @@ import { connect } from 'react-redux'; import Avatar from '../../containers/Avatar'; import styles from './styles'; -import RCActivityIndicator from '../../containers/ActivityIndicator'; +import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import RocketChat from '../../lib/rocketchat'; import StatusBar from '../../containers/StatusBar'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; +import { themes } from '../../constants/colors'; class ReadReceiptView extends React.Component { - static navigationOptions = { - title: I18n.t('Read_Receipt') - } + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Read_Receipt'), + ...themedHeader(screenProps.theme) + }) static propTypes = { navigation: PropTypes.object, Message_TimeFormat: PropTypes.string, baseUrl: PropTypes.string, userId: PropTypes.string, - token: PropTypes.string + token: PropTypes.string, + theme: PropTypes.string } constructor(props) { @@ -41,6 +46,10 @@ class ReadReceiptView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { loading, receipts } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.loading !== loading) { return true; } @@ -72,19 +81,22 @@ class ReadReceiptView extends React.Component { } } - renderEmpty = () => ( - <View style={styles.listEmptyContainer} testID='read-receipt-view'> - <Text>{I18n.t('No_Read_Receipts')}</Text> - </View> - ) + renderEmpty = () => { + const { theme } = this.props; + return ( + <View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].chatComponentBackground }]} testID='read-receipt-view'> + <Text style={{ color: themes[theme].titleText }}>{I18n.t('No_Read_Receipts')}</Text> + </View> + ); + } renderItem = ({ item }) => { const { - Message_TimeFormat, userId, baseUrl, token + Message_TimeFormat, userId, baseUrl, token, theme } = this.props; const time = moment(item.ts).format(Message_TimeFormat); return ( - <View style={styles.itemContainer}> + <View style={[styles.itemContainer, { backgroundColor: themes[theme].backgroundColor }]}> <Avatar text={item.user.username} size={40} @@ -94,14 +106,14 @@ class ReadReceiptView extends React.Component { /> <View style={styles.infoContainer}> <View style={styles.item}> - <Text style={styles.name}> + <Text style={[styles.name, { color: themes[theme].titleText }]}> {item.user.name} </Text> - <Text> + <Text style={{ color: themes[theme].auxiliaryText }}> {time} </Text> </View> - <Text> + <Text style={{ color: themes[theme].auxiliaryText }}> {`@${ item.user.username }`} </Text> </View> @@ -109,27 +121,41 @@ class ReadReceiptView extends React.Component { ); } - renderSeparator = () => <View style={styles.separator} />; + renderSeparator = () => { + const { theme } = this.props; + return <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; + } render() { const { receipts, loading } = this.state; + const { theme } = this.props; if (!loading && receipts.length === 0) { return this.renderEmpty(); } return ( - <SafeAreaView style={styles.container} testID='read-receipt-view' forceInset={{ bottom: 'always' }}> - <StatusBar /> + <SafeAreaView + style={[styles.container, { backgroundColor: themes[theme].chatComponentBackground }]} + forceInset={{ bottom: 'always' }} + testID='read-receipt-view' + > + <StatusBar theme={theme} /> <View> {loading - ? <RCActivityIndicator /> + ? <ActivityIndicator theme={theme} /> : ( <FlatList data={receipts} renderItem={this.renderItem} ItemSeparatorComponent={this.renderSeparator} - style={styles.list} + style={[ + styles.list, + { + backgroundColor: themes[theme].chatComponentBackground, + borderColor: themes[theme].separatorColor + } + ]} keyExtractor={item => item._id} /> )} @@ -146,4 +172,4 @@ const mapStateToProps = state => ({ token: state.login.user && state.login.user.token }); -export default connect(mapStateToProps)(ReadReceiptView); +export default connect(mapStateToProps)(withTheme(ReadReceiptView)); diff --git a/app/views/ReadReceiptView/styles.js b/app/views/ReadReceiptView/styles.js index 731fe8f1d..a0013531c 100644 --- a/app/views/ReadReceiptView/styles.js +++ b/app/views/ReadReceiptView/styles.js @@ -1,13 +1,11 @@ import { StyleSheet } from 'react-native'; -import { COLOR_SEPARATOR, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER } from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ listEmptyContainer: { flex: 1, alignItems: 'center', - justifyContent: 'center', - backgroundColor: COLOR_BACKGROUND_CONTAINER + justifyContent: 'center' }, item: { flex: 1, @@ -15,20 +13,12 @@ export default StyleSheet.create({ justifyContent: 'space-between' }, separator: { - height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR + height: StyleSheet.hairlineWidth }, name: { ...sharedStyles.textRegular, - ...sharedStyles.textColorTitle, fontSize: 17 }, - username: { - flex: 1, - ...sharedStyles.textRegular, - ...sharedStyles.textColorDescription, - fontSize: 14 - }, infoContainer: { flex: 1, marginLeft: 10 @@ -36,12 +26,10 @@ export default StyleSheet.create({ itemContainer: { flex: 1, flexDirection: 'row', - padding: 10, - backgroundColor: COLOR_WHITE + padding: 10 }, container: { - flex: 1, - backgroundColor: COLOR_BACKGROUND_CONTAINER + flex: 1 }, list: { ...sharedStyles.separatorVertical, diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js index f1b34498f..12e79b633 100644 --- a/app/views/RegisterView.js +++ b/app/views/RegisterView.js @@ -20,13 +20,18 @@ import isValidEmail from '../utils/isValidEmail'; import { LegalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; import log from '../utils/log'; +import { withTheme } from '../theme'; +import { themes } from '../constants/colors'; +import { themedHeader } from '../utils/navigation'; +import { isTablet } from '../utils/deviceInfo'; const shouldUpdateState = ['name', 'email', 'password', 'username', 'saving']; class RegisterView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const title = navigation.getParam('title', 'Rocket.Chat'); return { + ...themedHeader(screenProps.theme), title, headerRight: <LegalButton testID='register-view-more' navigation={navigation} /> }; @@ -36,7 +41,8 @@ class RegisterView extends React.Component { navigation: PropTypes.object, loginRequest: PropTypes.func, Site_Name: PropTypes.string, - Accounts_CustomFields: PropTypes.string + Accounts_CustomFields: PropTypes.string, + theme: PropTypes.string } constructor(props) { @@ -67,6 +73,10 @@ class RegisterView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { customFields } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (!equal(nextState.customFields, customFields)) { return true; } @@ -124,7 +134,7 @@ class RegisterView extends React.Component { renderCustomFields = () => { const { customFields } = this.state; - const { Accounts_CustomFields } = this.props; + const { Accounts_CustomFields, theme } = this.props; if (!Accounts_CustomFields) { return null; } @@ -149,6 +159,7 @@ class RegisterView extends React.Component { value={customFields[key]} iconLeft='flag' testID='register-view-custom-picker' + theme={theme} /> </RNPickerSelect> ); @@ -172,6 +183,7 @@ class RegisterView extends React.Component { } this.avatarUrl.focus(); }} + theme={theme} /> ); }); @@ -182,12 +194,16 @@ class RegisterView extends React.Component { render() { const { saving } = this.state; + const { theme } = this.props; return ( - <KeyboardView contentContainerStyle={sharedStyles.container}> - <StatusBar /> + <KeyboardView + style={{ backgroundColor: themes[theme].backgroundColor }} + contentContainerStyle={sharedStyles.container} + > + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} contentContainerStyle={sharedStyles.containerScrollView}> - <SafeAreaView style={sharedStyles.container} testID='register-view' forceInset={{ vertical: 'never' }}> - <Text style={[sharedStyles.loginTitle, sharedStyles.textBold]}>{I18n.t('Sign_Up')}</Text> + <SafeAreaView style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]} testID='register-view' forceInset={{ vertical: 'never' }}> + <Text style={[sharedStyles.loginTitle, sharedStyles.textBold, { color: themes[theme].titleText }]}>{I18n.t('Sign_Up')}</Text> <TextInput autoFocus placeholder={I18n.t('Name')} @@ -196,6 +212,7 @@ class RegisterView extends React.Component { onChangeText={name => this.setState({ name })} onSubmitEditing={() => { this.usernameInput.focus(); }} testID='register-view-name' + theme={theme} /> <TextInput inputRef={(e) => { this.usernameInput = e; }} @@ -205,6 +222,7 @@ class RegisterView extends React.Component { onChangeText={username => this.setState({ username })} onSubmitEditing={() => { this.emailInput.focus(); }} testID='register-view-username' + theme={theme} /> <TextInput inputRef={(e) => { this.emailInput = e; }} @@ -215,6 +233,7 @@ class RegisterView extends React.Component { onChangeText={email => this.setState({ email })} onSubmitEditing={() => { this.passwordInput.focus(); }} testID='register-view-email' + theme={theme} /> <TextInput inputRef={(e) => { this.passwordInput = e; }} @@ -226,6 +245,7 @@ class RegisterView extends React.Component { onSubmitEditing={this.submit} testID='register-view-password' containerStyle={sharedStyles.inputLastChild} + theme={theme} /> {this.renderCustomFields()} @@ -237,6 +257,7 @@ class RegisterView extends React.Component { testID='register-view-submit' disabled={!this.valid()} loading={saving} + theme={theme} /> </SafeAreaView> </ScrollView> @@ -253,4 +274,4 @@ const mapDispatchToProps = dispatch => ({ loginRequest: params => dispatch(loginRequestAction(params)) }); -export default connect(mapStateToProps, mapDispatchToProps)(RegisterView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RegisterView)); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index e40ff68a4..9a0a89301 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -6,12 +6,12 @@ import { import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; +import Touch from '../../utils/touch'; import { leaveRoom as leaveRoomAction } from '../../actions/room'; import styles from './styles'; import sharedStyles from '../Styles'; import Avatar from '../../containers/Avatar'; import Status from '../../containers/Status'; -import Touch from '../../utils/touch'; import RocketChat from '../../lib/rocketchat'; import log from '../../utils/log'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; @@ -20,13 +20,21 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps'; import { CustomIcon } from '../../lib/Icons'; import DisclosureIndicator from '../../containers/DisclosureIndicator'; import StatusBar from '../../containers/StatusBar'; -import { COLOR_WHITE } from '../../constants/colors'; - -const renderSeparator = () => <View style={styles.separator} />; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; +import { CloseModalButton } from '../../containers/HeaderButton'; class RoomActionsView extends React.Component { - static navigationOptions = { - title: I18n.t('Actions') + static navigationOptions = ({ navigation, screenProps }) => { + const options = { + ...themedHeader(screenProps.theme), + title: I18n.t('Actions') + }; + if (screenProps.split) { + options.headerLeft = <CloseModalButton navigation={navigation} testID='room-actions-view-close' />; + } + return options; } static propTypes = { @@ -37,7 +45,8 @@ class RoomActionsView extends React.Component { token: PropTypes.string }), leaveRoom: PropTypes.func, - jitsiEnabled: PropTypes.bool + jitsiEnabled: PropTypes.bool, + theme: PropTypes.string } constructor(props) { @@ -325,6 +334,11 @@ class RoomActionsView extends React.Component { return sections; } + renderSeparator = () => { + const { theme } = this.props; + return <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; + } + updateRoomMember = async() => { const { room } = this.state; const { rid } = room; @@ -385,7 +399,7 @@ class RoomActionsView extends React.Component { renderRoomInfo = ({ item }) => { const { room, member } = this.state; const { name, t, topic } = room; - const { baseUrl, user } = this.props; + const { baseUrl, user, theme } = this.props; return ( this.renderTouchableItem([ @@ -403,70 +417,77 @@ class RoomActionsView extends React.Component { </Avatar>, <View key='name' style={styles.roomTitleContainer}> {room.t === 'd' - ? <Text style={styles.roomTitle}>{room.fname}</Text> + ? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text> : ( <View style={styles.roomTitleRow}> - <RoomTypeIcon type={room.prid ? 'discussion' : room.t} /> - <Text style={styles.roomTitle}>{room.prid ? room.fname : room.name}</Text> + <RoomTypeIcon type={room.prid ? 'discussion' : room.t} theme={theme} /> + <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.prid ? room.fname : room.name}</Text> </View> ) } - <Text style={styles.roomDescription} ellipsizeMode='tail' numberOfLines={1}>{t === 'd' ? `@${ name }` : topic}</Text> + <Text style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} ellipsizeMode='tail' numberOfLines={1}>{t === 'd' ? `@${ name }` : topic}</Text> </View>, - <DisclosureIndicator key='disclosure-indicator' /> + <DisclosureIndicator theme={theme} key='disclosure-indicator' /> ], item) ); } - renderTouchableItem = (subview, item) => ( - <Touch - onPress={() => this.onPressTouchable(item)} - underlayColor={COLOR_WHITE} - activeOpacity={0.5} - accessibilityLabel={item.name} - accessibilityTraits='button' - testID={item.testID} - > - <View style={[styles.sectionItem, item.disabled && styles.sectionItemDisabled]}> - {subview} - </View> - </Touch> - ) + renderTouchableItem = (subview, item) => { + const { theme } = this.props; + return ( + <Touch + onPress={() => this.onPressTouchable(item)} + style={{ backgroundColor: themes[theme].backgroundColor }} + accessibilityLabel={item.name} + accessibilityTraits='button' + testID={item.testID} + theme={theme} + > + <View style={[styles.sectionItem, item.disabled && styles.sectionItemDisabled]}> + {subview} + </View> + </Touch> + ); + } renderItem = ({ item }) => { + const { theme } = this.props; + const colorDanger = { color: themes[theme].dangerColor }; const subview = item.type === 'danger' ? [ - <CustomIcon key='icon' name={item.icon} size={24} style={[styles.sectionItemIcon, styles.textColorDanger]} />, - <Text key='name' style={[styles.sectionItemName, styles.textColorDanger]}>{ item.name }</Text> + <CustomIcon key='icon' name={item.icon} size={24} style={[styles.sectionItemIcon, colorDanger]} />, + <Text key='name' style={[styles.sectionItemName, colorDanger]}>{ item.name }</Text> ] : [ - <CustomIcon key='left-icon' name={item.icon} size={24} style={styles.sectionItemIcon} />, - <Text key='name' style={styles.sectionItemName}>{ item.name }</Text>, - item.description ? <Text key='description' style={styles.sectionItemDescription}>{ item.description }</Text> : null, - <DisclosureIndicator key='disclosure-indicator' /> + <CustomIcon key='left-icon' name={item.icon} size={24} style={[styles.sectionItemIcon, { color: themes[theme].bodyText }]} />, + <Text key='name' style={[styles.sectionItemName, { color: themes[theme].bodyText }]}>{ item.name }</Text>, + item.description ? <Text key='description' style={[styles.sectionItemDescription, { color: themes[theme].auxiliaryText }]}>{ item.description }</Text> : null, + <DisclosureIndicator theme={theme} key='disclosure-indicator' /> ]; return this.renderTouchableItem(subview, item); } renderSectionSeparator = (data) => { + const { theme } = this.props; if (data.trailingItem) { - return <View style={[styles.sectionSeparator, data.leadingSection && styles.sectionSeparatorBorder]} />; + return <View style={[styles.sectionSeparator, data.leadingSection && styles.sectionSeparatorBorder, { backgroundColor: themes[theme].auxiliaryBackground, borderColor: themes[theme].separatorColor }]} />; } if (!data.trailingSection) { - return <View style={styles.sectionSeparatorBorder} />; + return <View style={[styles.sectionSeparatorBorder, { backgroundColor: themes[theme].auxiliaryBackground, borderColor: themes[theme].separatorColor }]} />; } return null; } render() { + const { theme } = this.props; return ( <SafeAreaView style={styles.container} testID='room-actions-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <StatusBar theme={theme} /> <SectionList - contentContainerStyle={styles.contentContainer} - style={styles.container} + contentContainerStyle={[styles.contentContainer, { backgroundColor: themes[theme].auxiliaryBackground }]} + style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} stickySectionHeadersEnabled={false} sections={this.sections} SectionSeparatorComponent={this.renderSectionSeparator} - ItemSeparatorComponent={renderSeparator} + ItemSeparatorComponent={this.renderSeparator} keyExtractor={item => item.name} testID='room-actions-list' {...scrollPersistTaps} @@ -489,4 +510,4 @@ const mapDispatchToProps = dispatch => ({ leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)) }); -export default connect(mapStateToProps, mapDispatchToProps)(RoomActionsView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RoomActionsView)); diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js index ee0939aba..1461cba37 100644 --- a/app/views/RoomActionsView/styles.js +++ b/app/views/RoomActionsView/styles.js @@ -1,7 +1,4 @@ import { StyleSheet } from 'react-native'; -import { - COLOR_SEPARATOR, COLOR_BORDER, COLOR_DANGER, COLOR_WHITE -} from '../../constants/colors'; import sharedStyles from '../Styles'; @@ -10,12 +7,10 @@ export default StyleSheet.create({ paddingBottom: 30 }, container: { - flex: 1, - backgroundColor: '#F6F7F9' + flex: 1 }, sectionItem: { - backgroundColor: COLOR_WHITE, - paddingVertical: 16, + paddingVertical: 11, flexDirection: 'row', alignItems: 'center' }, @@ -24,34 +19,26 @@ export default StyleSheet.create({ }, sectionItemIcon: { width: 56, - textAlign: 'center', - ...sharedStyles.textColorNormal + textAlign: 'center' }, sectionItemName: { flex: 1, fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, sectionItemDescription: { fontSize: 14, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, separator: { - height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR + height: StyleSheet.hairlineWidth }, sectionSeparator: { - height: 10, - backgroundColor: '#F6F7F9' + borderBottomWidth: StyleSheet.hairlineWidth, + height: 36 }, sectionSeparatorBorder: { - borderColor: COLOR_BORDER, - borderTopWidth: 1 - }, - textColorDanger: { - color: COLOR_DANGER + borderTopWidth: StyleSheet.hairlineWidth }, avatar: { marginHorizontal: 16 @@ -61,12 +48,11 @@ export default StyleSheet.create({ }, roomTitle: { fontSize: 16, - ...sharedStyles.textColorNormal, + paddingRight: 16, ...sharedStyles.textMedium }, roomDescription: { fontSize: 13, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, roomTitleRow: { diff --git a/app/views/RoomInfoEditView/SwitchContainer.js b/app/views/RoomInfoEditView/SwitchContainer.js index 6f3266098..5b877552d 100644 --- a/app/views/RoomInfoEditView/SwitchContainer.js +++ b/app/views/RoomInfoEditView/SwitchContainer.js @@ -3,8 +3,7 @@ import { View, Text, Switch } from 'react-native'; import PropTypes from 'prop-types'; import styles from './styles'; -import sharedStyles from '../Styles'; -import { SWITCH_TRACK_COLOR } from '../../constants/colors'; +import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; export default class SwitchContainer extends React.PureComponent { static propTypes = { @@ -15,19 +14,20 @@ export default class SwitchContainer extends React.PureComponent { rightLabelPrimary: PropTypes.string, rightLabelSecondary: PropTypes.string, onValueChange: PropTypes.func, + theme: PropTypes.string, testID: PropTypes.string } render() { const { - value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary, testID + value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary, theme, testID } = this.props; return ( [ <View key='switch-container' style={styles.switchContainer}> - <View style={[styles.switchLabelContainer, sharedStyles.alignItemsFlexEnd]}> - <Text style={styles.switchLabelPrimary}>{leftLabelPrimary}</Text> - <Text style={[styles.switchLabelSecondary, sharedStyles.textAlignRight]}>{leftLabelSecondary}</Text> + <View style={styles.switchLabelContainer}> + <Text style={[styles.switchLabelPrimary, { color: themes[theme].titleText }]}>{leftLabelPrimary}</Text> + <Text style={[styles.switchLabelSecondary, { color: themes[theme].titleText }]}>{leftLabelSecondary}</Text> </View> <Switch style={styles.switch} @@ -38,11 +38,11 @@ export default class SwitchContainer extends React.PureComponent { testID={testID} /> <View style={styles.switchLabelContainer}> - <Text style={styles.switchLabelPrimary}>{rightLabelPrimary}</Text> - <Text style={styles.switchLabelSecondary}>{rightLabelSecondary}</Text> + <Text style={[styles.switchLabelPrimary, { color: themes[theme].titleText }]}>{rightLabelPrimary}</Text> + <Text style={[styles.switchLabelSecondary, { color: themes[theme].titleText }]}>{rightLabelSecondary}</Text> </View> </View>, - <View key='switch-divider' style={styles.divider} /> + <View key='switch-divider' style={[styles.divider, { borderColor: themes[theme].separatorColor }]} /> ] ); } diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js index 8bb3da77b..3d6a61e8c 100644 --- a/app/views/RoomInfoEditView/index.js +++ b/app/views/RoomInfoEditView/index.js @@ -24,6 +24,9 @@ import random from '../../utils/random'; import log from '../../utils/log'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; +import { themedHeader } from '../../utils/navigation'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; const PERMISSION_SET_READONLY = 'set-readonly'; const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly'; @@ -41,13 +44,15 @@ const PERMISSIONS_ARRAY = [ ]; class RoomInfoEditView extends React.Component { - static navigationOptions = { - title: I18n.t('Room_Info_Edit') - } + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Room_Info_Edit'), + ...themedHeader(screenProps.theme) + }) static propTypes = { navigation: PropTypes.object, - eraseRoom: PropTypes.func + eraseRoom: PropTypes.func, + theme: PropTypes.string }; constructor(props) { @@ -145,11 +150,12 @@ class RoomInfoEditView extends React.Component { const { room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode } = this.state; + const { joinCodeRequired } = room; return !(room.name === name && room.description === description && room.topic === topic && room.announcement === announcement - && this.randomValue === joinCode + && (joinCodeRequired ? this.randomValue : '') === joinCode && room.t === 'p' === t && room.ro === ro && room.reactWhenReadOnly === reactWhenReadOnly @@ -296,12 +302,15 @@ class RoomInfoEditView extends React.Component { const { name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived } = this.state; + const { theme } = this.props; + const { dangerColor } = themes[theme]; return ( <KeyboardView + style={{ backgroundColor: themes[theme].backgroundColor }} contentContainerStyle={sharedStyles.container} keyboardVerticalOffset={128} > - <StatusBar /> + <StatusBar theme={theme} /> <ScrollView contentContainerStyle={sharedStyles.containerScrollView} testID='room-info-edit-view-list' @@ -315,6 +324,7 @@ class RoomInfoEditView extends React.Component { onChangeText={value => this.setState({ name: value })} onSubmitEditing={() => { this.description.focus(); }} error={nameError} + theme={theme} testID='room-info-edit-view-name' /> <RCTextInput @@ -323,6 +333,7 @@ class RoomInfoEditView extends React.Component { value={description} onChangeText={value => this.setState({ description: value })} onSubmitEditing={() => { this.topic.focus(); }} + theme={theme} testID='room-info-edit-view-description' /> <RCTextInput @@ -331,6 +342,7 @@ class RoomInfoEditView extends React.Component { value={topic} onChangeText={value => this.setState({ topic: value })} onSubmitEditing={() => { this.announcement.focus(); }} + theme={theme} testID='room-info-edit-view-topic' /> <RCTextInput @@ -339,6 +351,7 @@ class RoomInfoEditView extends React.Component { value={announcement} onChangeText={value => this.setState({ announcement: value })} onSubmitEditing={() => { this.joinCode.focus(); }} + theme={theme} testID='room-info-edit-view-announcement' /> <RCTextInput @@ -348,6 +361,7 @@ class RoomInfoEditView extends React.Component { onChangeText={value => this.setState({ joinCode: value })} onSubmitEditing={this.submit} secureTextEntry + theme={theme} testID='room-info-edit-view-password' /> <SwitchContainer @@ -357,6 +371,7 @@ class RoomInfoEditView extends React.Component { rightLabelPrimary={I18n.t('Private')} rightLabelSecondary={I18n.t('Just_invited_people_can_access_this_channel')} onValueChange={value => this.setState({ t: value })} + theme={theme} testID='room-info-edit-view-t' /> <SwitchContainer @@ -367,6 +382,7 @@ class RoomInfoEditView extends React.Component { rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')} onValueChange={value => this.setState({ ro: value })} disabled={!permissions[PERMISSION_SET_READONLY] || room.broadcast} + theme={theme} testID='room-info-edit-view-ro' /> {ro && !room.broadcast @@ -379,6 +395,7 @@ class RoomInfoEditView extends React.Component { rightLabelSecondary={I18n.t('Reactions_are_enabled')} onValueChange={value => this.setState({ reactWhenReadOnly: value })} disabled={!permissions[PERMISSION_SET_REACT_WHEN_READONLY]} + theme={theme} testID='room-info-edit-view-react-when-ro' /> ) @@ -387,55 +404,89 @@ class RoomInfoEditView extends React.Component { {room.broadcast ? [ <Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>, - <View style={styles.divider} /> + <View style={[styles.divider, { borderColor: themes[theme].separatorColor }]} /> ] : null } <TouchableOpacity - style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]} + style={[ + styles.buttonContainer, + { backgroundColor: themes[theme].buttonBackground }, + !this.formIsChanged() && styles.buttonContainerDisabled + ]} onPress={this.submit} disabled={!this.formIsChanged()} testID='room-info-edit-view-submit' > - <Text style={sharedStyles.button} accessibilityTraits='button'>{I18n.t('SAVE')}</Text> + <Text style={[styles.button, { color: themes[theme].buttonText }]} accessibilityTraits='button'>{I18n.t('SAVE')}</Text> </TouchableOpacity> <View style={{ flexDirection: 'row' }}> <TouchableOpacity - style={[sharedStyles.buttonContainer_inverted, styles.buttonInverted, { flex: 1 }]} + style={[ + styles.buttonContainer_inverted, + styles.buttonInverted, + { flex: 1, borderColor: themes[theme].auxiliaryText } + ]} onPress={this.reset} testID='room-info-edit-view-reset' > - <Text style={sharedStyles.button_inverted} accessibilityTraits='button'>{I18n.t('RESET')}</Text> + <Text + style={[ + styles.button, + styles.button_inverted, + { color: themes[theme].bodyText } + ]} + accessibilityTraits='button' + > + {I18n.t('RESET')} + </Text> </TouchableOpacity> <TouchableOpacity style={[ - sharedStyles.buttonContainer_inverted, - styles.buttonDanger, + styles.buttonInverted, + styles.buttonContainer_inverted, !this.hasArchivePermission() && sharedStyles.opacity5, - { flex: 1, marginLeft: 10 } + { flex: 1, marginLeft: 10, borderColor: dangerColor } ]} onPress={this.toggleArchive} disabled={!this.hasArchivePermission()} testID='room-info-edit-view-archive' > - <Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'> + <Text + style={[ + styles.button, + styles.button_inverted, + { color: dangerColor } + ]} + accessibilityTraits='button' + > { archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') } </Text> </TouchableOpacity> </View> - <View style={styles.divider} /> + <View style={[styles.divider, { borderColor: themes[theme].separatorColor }]} /> <TouchableOpacity style={[ - sharedStyles.buttonContainer_inverted, - sharedStyles.buttonContainerLastChild, + styles.buttonContainer_inverted, + styles.buttonContainerLastChild, styles.buttonDanger, + { borderColor: dangerColor }, !this.hasDeletePermission() && sharedStyles.opacity5 ]} onPress={this.delete} disabled={!this.hasDeletePermission()} testID='room-info-edit-view-delete' > - <Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>{I18n.t('DELETE')}</Text> + <Text + style={[ + styles.button, + styles.button_inverted, + { color: dangerColor } + ]} + accessibilityTraits='button' + > + {I18n.t('DELETE')} + </Text> </TouchableOpacity> <Loading visible={saving} /> </SafeAreaView> @@ -449,4 +500,4 @@ const mapDispatchToProps = dispatch => ({ eraseRoom: (rid, t) => dispatch(eraseRoomAction(rid, t)) }); -export default connect(null, mapDispatchToProps)(RoomInfoEditView); +export default connect(null, mapDispatchToProps)(withTheme(RoomInfoEditView)); diff --git a/app/views/RoomInfoEditView/styles.js b/app/views/RoomInfoEditView/styles.js index cbe6f50cb..b63ae9987 100644 --- a/app/views/RoomInfoEditView/styles.js +++ b/app/views/RoomInfoEditView/styles.js @@ -1,24 +1,37 @@ import { StyleSheet } from 'react-native'; -import { COLOR_DANGER, COLOR_SEPARATOR } from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ + button: { + ...sharedStyles.textAlignCenter, + ...sharedStyles.textBold + }, buttonInverted: { - borderColor: 'rgba(0,0,0,.15)', borderWidth: 2, borderRadius: 2 }, buttonContainerDisabled: { - backgroundColor: 'rgba(65, 72, 82, 0.7)' + opacity: 0.7 }, - buttonDanger: { - borderColor: COLOR_DANGER, - borderWidth: 2, + buttonContainer_inverted: { + paddingVertical: 15, + marginBottom: 0 + }, + button_inverted: { + flexGrow: 1 + }, + buttonContainerLastChild: { + marginBottom: 40 + }, + buttonContainer: { + paddingVertical: 15, + marginBottom: 20, borderRadius: 2 }, - colorDanger: { - color: COLOR_DANGER + buttonDanger: { + borderWidth: 2, + borderRadius: 2 }, switchContainer: { flexDirection: 'row', @@ -26,31 +39,29 @@ export default StyleSheet.create({ }, switchLabelContainer: { flex: 1, - paddingHorizontal: 10 + paddingHorizontal: 10, + alignItems: 'flex-end' }, switchLabelPrimary: { fontSize: 16, paddingBottom: 6, - ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal + ...sharedStyles.textRegular }, switchLabelSecondary: { fontSize: 12, ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal + textAlign: 'right' }, switch: { alignSelf: 'center' }, divider: { height: StyleSheet.hairlineWidth, - borderColor: COLOR_SEPARATOR, borderBottomWidth: StyleSheet.hairlineWidth, marginVertical: 20 }, broadcast: { - textAlign: 'center', - ...sharedStyles.textSemibold, - ...sharedStyles.textColorNormal + ...sharedStyles.textAlignCenter, + ...sharedStyles.textSemibold } }); diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index e7dcd7ea0..e2da34a96 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -16,26 +16,30 @@ import I18n from '../../i18n'; import { CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import StatusBar from '../../containers/StatusBar'; import log from '../../utils/log'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; const PERMISSION_EDIT_ROOM = 'edit-room'; const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase()); -const getRoomTitle = (room, type, name) => (type === 'd' - ? <Text testID='room-info-view-name' style={styles.roomTitle}>{name}</Text> +const getRoomTitle = (room, type, name, theme) => (type === 'd' + ? <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]}>{name}</Text> : ( <View style={styles.roomTitleRow}> - <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' /> - <Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.prid ? room.fname : room.name}</Text> + <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' theme={theme} /> + <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{room.prid ? room.fname : room.name}</Text> </View> ) ); class RoomInfoView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const showEdit = navigation.getParam('showEdit'); const rid = navigation.getParam('rid'); return { title: I18n.t('Room_Info'), + ...themedHeader(screenProps.theme), headerRight: showEdit ? ( <CustomHeaderButtons> @@ -53,7 +57,8 @@ class RoomInfoView extends React.Component { token: PropTypes.string }), baseUrl: PropTypes.string, - Message_TimeFormat: PropTypes.string + Message_TimeFormat: PropTypes.string, + theme: PropTypes.string } constructor(props) { @@ -138,21 +143,25 @@ class RoomInfoView extends React.Component { isDirect = () => this.t === 'd' - renderItem = (key, room) => ( - <View style={styles.item}> - <Text style={styles.itemLabel}>{I18n.t(camelize(key))}</Text> - <Text - style={[styles.itemContent, !room[key] && styles.itemContent__empty]} - testID={`room-info-view-${ key }`} - >{ room[key] ? room[key] : I18n.t(`No_${ key }_provided`) } - </Text> - </View> - ); + renderItem = (key, room) => { + const { theme } = this.props; + return ( + <View style={styles.item}> + <Text style={[styles.itemLabel, { color: themes[theme].titleText }]}>{I18n.t(camelize(key))}</Text> + <Text + style={[styles.itemContent, !room[key] && styles.itemContent__empty, { color: themes[theme].auxiliaryText }]} + testID={`room-info-view-${ key }`} + >{ room[key] ? room[key] : I18n.t(`No_${ key }_provided`) } + </Text> + </View> + ); + } renderRole = (description) => { + const { theme } = this.props; if (description) { return ( - <View style={styles.roleBadge} key={description}> + <View style={[styles.roleBadge, { backgroundColor: themes[theme].focusedBackground }]} key={description}> <Text style={styles.role}>{ description }</Text> </View> ); @@ -177,7 +186,7 @@ class RoomInfoView extends React.Component { renderTimezone = () => { const { roomUser } = this.state; - const { Message_TimeFormat } = this.props; + const { Message_TimeFormat, theme } = this.props; if (roomUser) { const { utcOffset } = roomUser; @@ -187,8 +196,8 @@ class RoomInfoView extends React.Component { } return ( <View style={styles.item}> - <Text style={styles.itemLabel}>{I18n.t('Timezone')}</Text> - <Text style={styles.itemContent}>{moment().utcOffset(utcOffset).format(Message_TimeFormat)} (UTC { utcOffset })</Text> + <Text style={[styles.itemLabel, { color: themes[theme].titleText }]}>{I18n.t('Timezone')}</Text> + <Text style={[styles.itemContent, { color: themes[theme].auxiliaryText }]}>{moment().utcOffset(utcOffset).format(Message_TimeFormat)} (UTC { utcOffset })</Text> </View> ); } @@ -275,16 +284,21 @@ class RoomInfoView extends React.Component { render() { const { room, roomUser } = this.state; + const { theme } = this.props; if (!room) { return <View />; } return ( - <ScrollView style={styles.scroll}> - <StatusBar /> - <SafeAreaView style={styles.container} testID='room-info-view' forceInset={{ vertical: 'never' }}> + <ScrollView style={[styles.scroll, { backgroundColor: themes[theme].backgroundColor }]}> + <StatusBar theme={theme} /> + <SafeAreaView + style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} + forceInset={{ vertical: 'never' }} + testID='room-info-view' + > <View style={styles.avatarContainer}> {this.renderAvatar(room, roomUser)} - <View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name) }</View> + <View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name, theme) }</View> </View> {this.isDirect() ? this.renderDirect() : this.renderChannel()} </SafeAreaView> @@ -302,4 +316,4 @@ const mapStateToProps = state => ({ Message_TimeFormat: state.settings.Message_TimeFormat }); -export default connect(mapStateToProps)(RoomInfoView); +export default connect(mapStateToProps)(withTheme(RoomInfoView)); diff --git a/app/views/RoomInfoView/styles.js b/app/views/RoomInfoView/styles.js index faeaf9101..00901410a 100644 --- a/app/views/RoomInfoView/styles.js +++ b/app/views/RoomInfoView/styles.js @@ -1,17 +1,14 @@ import { StyleSheet } from 'react-native'; import sharedStyles from '../Styles'; -import { COLOR_BACKGROUND_CONTAINER, COLOR_WHITE } from '../../constants/colors'; export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, scroll: { flex: 1, flexDirection: 'column', - backgroundColor: COLOR_WHITE, padding: 10 }, item: { @@ -33,7 +30,6 @@ export default StyleSheet.create({ }, roomTitle: { fontSize: 18, - ...sharedStyles.textColorNormal, ...sharedStyles.textMedium }, roomTitleRow: { @@ -48,12 +44,10 @@ export default StyleSheet.create({ itemLabel: { marginBottom: 10, fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textMedium }, itemContent: { fontSize: 14, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, itemContent__empty: { @@ -65,14 +59,12 @@ export default StyleSheet.create({ }, roleBadge: { padding: 6, - backgroundColor: COLOR_BACKGROUND_CONTAINER, borderRadius: 2, marginRight: 6, marginBottom: 6 }, role: { fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular } }); diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index 25476297b..30be1624d 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, ActivityIndicator } from 'react-native'; +import { FlatList, View } from 'react-native'; import ActionSheet from 'react-native-action-sheet'; import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; @@ -20,16 +20,21 @@ import SearchBox from '../../containers/SearchBox'; import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import { CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import StatusBar from '../../containers/StatusBar'; +import ActivityIndicator from '../../containers/ActivityIndicator'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; +import { themes } from '../../constants/colors'; const PAGE_SIZE = 25; class RoomMembersView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const toggleStatus = navigation.getParam('toggleStatus', () => {}); const allUsers = navigation.getParam('allUsers'); const toggleText = allUsers ? I18n.t('Online') : I18n.t('All'); return { title: I18n.t('Members'), + ...themedHeader(screenProps.theme), headerRight: ( <CustomHeaderButtons> <Item title={toggleText} onPress={toggleStatus} testID='room-members-view-toggle-status' /> @@ -47,7 +52,8 @@ class RoomMembersView extends React.Component { user: PropTypes.shape({ id: PropTypes.string, token: PropTypes.string - }) + }), + theme: PropTypes.string } constructor(props) { @@ -226,10 +232,13 @@ class RoomMembersView extends React.Component { <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='room-members-view-search' /> ) - renderSeparator = () => <View style={styles.separator} />; + renderSeparator = () => { + const { theme } = this.props; + return <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; + } renderItem = ({ item }) => { - const { baseUrl, user } = this.props; + const { baseUrl, user, theme } = this.props; return ( <UserItem @@ -240,6 +249,7 @@ class RoomMembersView extends React.Component { baseUrl={baseUrl} testID={`room-members-view-item-${ item.username }`} user={user} + theme={theme} /> ); } @@ -248,22 +258,20 @@ class RoomMembersView extends React.Component { const { filtering, members, membersFiltered, isLoading } = this.state; - // if (isLoading) { - // return <ActivityIndicator style={styles.loading} />; - // } + const { theme } = this.props; return ( <SafeAreaView style={styles.list} testID='room-members-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <StatusBar theme={theme} /> <FlatList data={filtering ? membersFiltered : members} renderItem={this.renderItem} - style={styles.list} + style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} keyExtractor={item => item._id} ItemSeparatorComponent={this.renderSeparator} ListHeaderComponent={this.renderSearchBar} ListFooterComponent={() => { if (isLoading) { - return <ActivityIndicator style={styles.loading} />; + return <ActivityIndicator theme={theme} />; } return null; }} @@ -286,4 +294,4 @@ const mapStateToProps = state => ({ } }); -export default connect(mapStateToProps)(RoomMembersView); +export default connect(mapStateToProps)(withTheme(RoomMembersView)); diff --git a/app/views/RoomMembersView/styles.js b/app/views/RoomMembersView/styles.js index 75ae5e17d..d183ebb09 100644 --- a/app/views/RoomMembersView/styles.js +++ b/app/views/RoomMembersView/styles.js @@ -1,10 +1,8 @@ import { StyleSheet } from 'react-native'; -import { COLOR_SEPARATOR, COLOR_WHITE } from '../../constants/colors'; export default StyleSheet.create({ list: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, item: { flexDirection: 'row', @@ -17,10 +15,6 @@ export default StyleSheet.create({ }, separator: { height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR, marginLeft: 60 - }, - loading: { - flex: 1 } }); diff --git a/app/views/RoomView/EmptyRoom.js b/app/views/RoomView/EmptyRoom.js index 7251e29d7..255940528 100644 --- a/app/views/RoomView/EmptyRoom.js +++ b/app/views/RoomView/EmptyRoom.js @@ -10,15 +10,24 @@ const styles = StyleSheet.create({ } }); -const EmptyRoom = React.memo(({ length, mounted }) => { - if (length === 0 && mounted) { - return <ImageBackground source={{ uri: 'message_empty' }} style={styles.image} />; +const EmptyRoom = React.memo(({ + length, mounted, theme, rid +}) => { + if ((length === 0 && mounted) || !rid) { + return ( + <ImageBackground + source={{ uri: `message_empty_${ theme }` }} + style={styles.image} + /> + ); } return null; }); EmptyRoom.propTypes = { length: PropTypes.number.isRequired, - mounted: PropTypes.bool + mounted: PropTypes.bool, + theme: PropTypes.string, + rid: PropTypes.string }; export default EmptyRoom; diff --git a/app/views/RoomView/Header/Header.js b/app/views/RoomView/Header/Header.js index 802a2a391..4a915280f 100644 --- a/app/views/RoomView/Header/Header.js +++ b/app/views/RoomView/Header/Header.js @@ -1,16 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - View, Text, StyleSheet, ScrollView + View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import { shortnameToUnicode } from 'emoji-toolkit'; import removeMarkdown from 'remove-markdown'; import I18n from '../../../i18n'; import sharedStyles from '../../Styles'; -import { isIOS, isAndroid } from '../../../utils/deviceInfo'; +import { isAndroid, isTablet } from '../../../utils/deviceInfo'; import Icon from './Icon'; -import { COLOR_TEXT_DESCRIPTION, HEADER_TITLE, COLOR_WHITE } from '../../../constants/colors'; +import { themes } from '../../../constants/colors'; + +const androidMarginLeft = isTablet ? 0 : 10; const TITLE_SIZE = 16; const styles = StyleSheet.create({ @@ -18,7 +20,7 @@ const styles = StyleSheet.create({ flex: 1, height: '100%', marginRight: isAndroid ? 15 : 5, - marginLeft: isAndroid ? 10 : 0 + marginLeft: isAndroid ? androidMarginLeft : -12 }, titleContainer: { flex: 6, @@ -29,7 +31,6 @@ const styles = StyleSheet.create({ }, title: { ...sharedStyles.textSemibold, - color: HEADER_TITLE, fontSize: TITLE_SIZE }, scroll: { @@ -37,7 +38,6 @@ const styles = StyleSheet.create({ }, typing: { ...sharedStyles.textRegular, - color: isIOS ? COLOR_TEXT_DESCRIPTION : COLOR_WHITE, fontSize: 12, flex: 4 }, @@ -46,7 +46,7 @@ const styles = StyleSheet.create({ } }); -const Typing = React.memo(({ usersTyping }) => { +const Typing = React.memo(({ usersTyping, theme }) => { let usersText; if (!usersTyping.length) { return null; @@ -56,7 +56,7 @@ const Typing = React.memo(({ usersTyping }) => { usersText = usersTyping.join(', '); } return ( - <Text style={styles.typing} numberOfLines={1}> + <Text style={[styles.typing, { color: themes[theme].headerTitleColor }]} numberOfLines={1}> <Text style={styles.typingUsers}>{usersText} </Text> { usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }... </Text> @@ -64,18 +64,19 @@ const Typing = React.memo(({ usersTyping }) => { }); Typing.propTypes = { - usersTyping: PropTypes.array + usersTyping: PropTypes.array, + theme: PropTypes.string }; const HeaderTitle = React.memo(({ - title, scale, connecting + title, scale, connecting, theme }) => { if (connecting) { title = I18n.t('Connecting'); } return ( <Text - style={[styles.title, { fontSize: TITLE_SIZE * scale }]} + style={[styles.title, { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor }]} numberOfLines={1} testID={`room-view-title-${ title }`} >{title} @@ -86,16 +87,17 @@ const HeaderTitle = React.memo(({ HeaderTitle.propTypes = { title: PropTypes.string, scale: PropTypes.number, - connecting: PropTypes.bool + connecting: PropTypes.bool, + theme: PropTypes.string }; const Header = React.memo(({ - title, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting + title, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, theme }) => { const portrait = height > width; let scale = 1; - if (!portrait) { + if (!portrait && !tmid) { if (usersTyping.length > 0) { scale = 0.8; } @@ -107,8 +109,14 @@ const Header = React.memo(({ } } + const onPress = () => { + if (!tmid) { + goRoomActionsView(); + } + }; + return ( - <View style={[styles.container, { width: width - widthOffset }]}> + <TouchableOpacity onPress={onPress} style={[styles.container, { width: width - widthOffset }]}> <View style={[styles.titleContainer, tmid && styles.threadContainer]}> <ScrollView showsHorizontalScrollIndicator={false} @@ -116,19 +124,17 @@ const Header = React.memo(({ bounces={false} contentContainerStyle={styles.scroll} > - <Icon type={prid ? 'discussion' : type} status={status} /> + <Icon type={prid ? 'discussion' : type} status={status} theme={theme} /> <HeaderTitle - prid={prid} - type={type} - status={status} title={title} scale={scale} connecting={connecting} + theme={theme} /> </ScrollView> </View> - {type === 'thread' ? null : <Typing usersTyping={usersTyping} />} - </View> + {type === 'thread' ? null : <Typing usersTyping={usersTyping} theme={theme} />} + </TouchableOpacity> ); }); @@ -140,9 +146,11 @@ Header.propTypes = { prid: PropTypes.string, tmid: PropTypes.string, status: PropTypes.string, + theme: PropTypes.string, usersTyping: PropTypes.array, widthOffset: PropTypes.number, - connecting: PropTypes.bool + connecting: PropTypes.bool, + goRoomActionsView: PropTypes.func }; Header.defaultProps = { diff --git a/app/views/RoomView/Header/Icon.js b/app/views/RoomView/Header/Icon.js index 945f6a246..7247d88fe 100644 --- a/app/views/RoomView/Header/Icon.js +++ b/app/views/RoomView/Header/Icon.js @@ -2,10 +2,10 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; -import { STATUS_COLORS, COLOR_TEXT_DESCRIPTION, COLOR_WHITE } from '../../../constants/colors'; +import { STATUS_COLORS, themes } from '../../../constants/colors'; import { CustomIcon } from '../../../lib/Icons'; import Status from '../../../containers/Status/Status'; -import { isIOS } from '../../../utils/deviceInfo'; +import { isAndroid } from '../../../utils/deviceInfo'; const ICON_SIZE = 18; @@ -13,8 +13,7 @@ const styles = StyleSheet.create({ type: { width: ICON_SIZE, height: ICON_SIZE, - marginRight: 8, - color: isIOS ? COLOR_TEXT_DESCRIPTION : COLOR_WHITE + marginRight: 8 }, status: { marginLeft: 4, @@ -22,11 +21,18 @@ const styles = StyleSheet.create({ } }); -const Icon = React.memo(({ type, status }) => { +const Icon = React.memo(({ type, status, theme }) => { if (type === 'd') { return <Status size={10} style={styles.status} status={status} />; } + let colorStyle = {}; + if (type === 'd') { + colorStyle = { color: STATUS_COLORS[status] }; + } else { + colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText }; + } + let icon; if (type === 'discussion') { icon = 'chat'; @@ -47,7 +53,7 @@ const Icon = React.memo(({ type, status }) => { width: ICON_SIZE * 1, height: ICON_SIZE * 1 }, - type === 'd' && { color: STATUS_COLORS[status] } + colorStyle ]} /> ); @@ -55,6 +61,7 @@ const Icon = React.memo(({ type, status }) => { Icon.propTypes = { type: PropTypes.string, - status: PropTypes.string + status: PropTypes.string, + theme: PropTypes.string }; export default Icon; diff --git a/app/views/RoomView/Header/RightButtons.js b/app/views/RoomView/Header/RightButtons.js index aca52a726..87a4d6d15 100644 --- a/app/views/RoomView/Header/RightButtons.js +++ b/app/views/RoomView/Header/RightButtons.js @@ -1,24 +1,10 @@ import React from 'react'; -import { StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { CustomHeaderButtons, Item } from '../../../containers/HeaderButton'; import database from '../../../lib/database'; -const styles = StyleSheet.create({ - more: { - marginHorizontal: 0, - marginLeft: 0, - marginRight: 5 - }, - thread: { - marginHorizontal: 0, - marginLeft: 0, - marginRight: 15 - } -}); - class RightButtonsContainer extends React.PureComponent { static propTypes = { userId: PropTypes.string, @@ -27,8 +13,7 @@ class RightButtonsContainer extends React.PureComponent { t: PropTypes.string, tmid: PropTypes.string, navigation: PropTypes.object, - toggleFollowThread: PropTypes.func, - room: PropTypes.object + toggleFollowThread: PropTypes.func }; constructor(props) { @@ -39,15 +24,15 @@ class RightButtonsContainer extends React.PureComponent { } async componentDidMount() { - const { tmid, userId } = this.props; + const { tmid } = this.props; if (tmid) { const db = database.active; - const threadObservable = await db.collections.get('messages').findAndObserve(tmid); - this.threadSubscription = threadObservable.subscribe((thread) => { - this.setState({ - isFollowingThread: thread.replies && !!thread.replies.find(t => t === userId) - }); - }); + try { + const threadRecord = await db.collections.get('messages').find(tmid); + this.observeThead(threadRecord); + } catch (e) { + console.log('Can\'t find message to observe.'); + } } } @@ -57,10 +42,16 @@ class RightButtonsContainer extends React.PureComponent { } } - updateThread = () => { + observeThead = (threadRecord) => { + const threadObservable = threadRecord.observe(); + this.threadSubscription = threadObservable + .subscribe(thread => this.updateThread(thread)); + } + + updateThread = (thread) => { const { userId } = this.props; this.setState({ - isFollowingThread: this.thread.replies && !!this.thread.replies.find(t => t === userId) + isFollowingThread: thread.replies && !!thread.replies.find(t => t === userId) }); } @@ -69,13 +60,6 @@ class RightButtonsContainer extends React.PureComponent { navigation.navigate('ThreadMessagesView', { rid, t }); } - goRoomActionsView = () => { - const { - rid, t, navigation, room - } = this.props; - navigation.navigate('RoomActionsView', { rid, t, room }); - } - toggleFollowThread = () => { const { isFollowingThread } = this.state; const { toggleFollowThread } = this.props; @@ -110,16 +94,8 @@ class RightButtonsContainer extends React.PureComponent { iconName='thread' onPress={this.goThreadsView} testID='room-view-header-threads' - buttonStyle={styles.thread} /> ) : null} - <Item - title='more' - iconName='menu' - onPress={this.goRoomActionsView} - testID='room-view-header-actions' - buttonStyle={styles.more} - /> </CustomHeaderButtons> ); } diff --git a/app/views/RoomView/Header/RoomHeaderLeft.js b/app/views/RoomView/Header/RoomHeaderLeft.js new file mode 100644 index 000000000..85b8db314 --- /dev/null +++ b/app/views/RoomView/Header/RoomHeaderLeft.js @@ -0,0 +1,62 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet } from 'react-native'; +import { HeaderBackButton } from 'react-navigation-stack'; + +import { isIOS } from '../../../utils/deviceInfo'; +import { themes } from '../../../constants/colors'; +import Avatar from '../../../containers/Avatar'; + +const styles = StyleSheet.create({ + avatar: { + borderRadius: 10, + marginHorizontal: 16 + } +}); + +const RoomHeaderLeft = ({ + tmid, unreadsCount, navigation, baseUrl, userId, token, title, t, theme, goRoomActionsView, split +}) => { + if (!split || tmid) { + return ( + <HeaderBackButton + title={unreadsCount > 999 ? '+999' : unreadsCount || ' '} + backTitleVisible={isIOS} + onPress={() => navigation.goBack()} + tintColor={themes[theme].headerTintColor} + /> + ); + } + if (baseUrl && userId && token) { + return ( + <Avatar + text={title} + size={30} + type={t} + baseUrl={baseUrl} + style={styles.avatar} + userId={userId} + token={token} + theme={theme} + onPress={goRoomActionsView} + /> + ); + } + return null; +}; + +RoomHeaderLeft.propTypes = { + tmid: PropTypes.string, + unreadsCount: PropTypes.number, + navigation: PropTypes.object, + baseUrl: PropTypes.string, + userId: PropTypes.string, + token: PropTypes.string, + title: PropTypes.string, + t: PropTypes.string, + theme: PropTypes.string, + goRoomActionsView: PropTypes.func, + split: PropTypes.bool +}; + +export default RoomHeaderLeft; diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index 311eafb84..4911be2d3 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -6,6 +6,8 @@ import equal from 'deep-equal'; import Header from './Header'; import RightButtons from './RightButtons'; +import { withTheme } from '../../../theme'; +import RoomHeaderLeft from './RoomHeaderLeft'; class RoomHeaderView extends Component { static propTypes = { @@ -17,14 +19,18 @@ class RoomHeaderView extends Component { window: PropTypes.object, status: PropTypes.string, connecting: PropTypes.bool, - widthOffset: PropTypes.number + theme: PropTypes.string, + widthOffset: PropTypes.number, + goRoomActionsView: PropTypes.func }; shouldComponentUpdate(nextProps) { - const { usersTyping } = this.props; const { - type, title, status, window, connecting + type, title, status, window, connecting, goRoomActionsView, usersTyping, theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextProps.type !== type) { return true; } @@ -46,12 +52,15 @@ class RoomHeaderView extends Component { if (!equal(nextProps.usersTyping, usersTyping)) { return true; } + if (nextProps.goRoomActionsView !== goRoomActionsView) { + return true; + } return false; } render() { const { - window, title, type, prid, tmid, widthOffset, status = 'offline', connecting, usersTyping + window, title, type, prid, tmid, widthOffset, status = 'offline', connecting, usersTyping, goRoomActionsView, theme } = this.props; return ( @@ -63,8 +72,10 @@ class RoomHeaderView extends Component { status={status} width={window.width} height={window.height} + theme={theme} usersTyping={usersTyping} widthOffset={widthOffset} + goRoomActionsView={goRoomActionsView} connecting={connecting} /> ); @@ -89,6 +100,6 @@ const mapStateToProps = (state, ownProps) => { }; }; -export default responsive(connect(mapStateToProps)(RoomHeaderView)); +export default responsive(connect(mapStateToProps)(withTheme(RoomHeaderView))); -export { RightButtons }; +export { RightButtons, RoomHeaderLeft }; diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List.js index 4bae8096a..8c4e36099 100644 --- a/app/views/RoomView/List.js +++ b/app/views/RoomView/List.js @@ -1,12 +1,8 @@ import React from 'react'; -import { - ActivityIndicator, FlatList, InteractionManager -} from 'react-native'; +import { FlatList, InteractionManager } from 'react-native'; import PropTypes from 'prop-types'; -import debounce from 'lodash/debounce'; import orderBy from 'lodash/orderBy'; import { Q } from '@nozbe/watermelondb'; -import isEqual from 'lodash/isEqual'; import styles from './styles'; import database from '../../lib/database'; @@ -16,8 +12,10 @@ import log from '../../utils/log'; import EmptyRoom from './EmptyRoom'; import { isIOS } from '../../utils/deviceInfo'; import { animateNextTransition } from '../../utils/layoutAnimation'; +import ActivityIndicator from '../../containers/ActivityIndicator'; +import debounce from '../../utils/debounce'; -export class List extends React.Component { +class List extends React.Component { static propTypes = { onEndReached: PropTypes.func, renderFooter: PropTypes.func, @@ -25,7 +23,9 @@ export class List extends React.Component { rid: PropTypes.string, t: PropTypes.string, tmid: PropTypes.string, - animated: PropTypes.bool + animated: PropTypes.bool, + theme: PropTypes.string, + listRef: PropTypes.func }; constructor(props) { @@ -63,34 +63,32 @@ export class List extends React.Component { } this.messagesObservable = db.collections .get('thread_messages') - .query( - Q.where('rid', tmid) - ) - .observeWithColumns(['_updated_at']); - } else { + .query(Q.where('rid', tmid)) + .observe(); + } else if (rid) { this.messagesObservable = db.collections .get('messages') - .query( - Q.where('rid', rid) - ) - .observeWithColumns(['_updated_at']); + .query(Q.where('rid', rid)) + .observe(); } - this.messagesSubscription = this.messagesObservable - .subscribe((data) => { - this.interaction = InteractionManager.runAfterInteractions(() => { - if (tmid) { - data = [this.thread, ...data]; - } - const messages = orderBy(data, ['ts'], ['desc']); - if (this.mounted) { - animateNextTransition(); - this.setState({ messages }); - } else { - this.state.messages = messages; - } + if (rid) { + this.unsubscribeMessages(); + this.messagesSubscription = this.messagesObservable + .subscribe((data) => { + this.interaction = InteractionManager.runAfterInteractions(() => { + if (tmid) { + data = [this.thread, ...data]; + } + const messages = orderBy(data, ['ts'], ['desc']); + if (this.mounted) { + this.setState({ messages }, () => this.update()); + } else { + this.state.messages = messages; + } + }); }); - }); + } } // this.state.loading works for this.onEndReached and RoomView.init @@ -104,23 +102,22 @@ export class List extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { messages, loading, end } = this.state; + const { loading, end } = this.state; + const { theme } = this.props; + if (theme !== nextProps.theme) { + return true; + } if (loading !== nextState.loading) { return true; } if (end !== nextState.end) { return true; } - if (!isEqual(messages, nextState.messages)) { - return true; - } return false; } componentWillUnmount() { - if (this.messagesSubscription && this.messagesSubscription.unsubscribe) { - this.messagesSubscription.unsubscribe(); - } + this.unsubscribeMessages(); if (this.interaction && this.interaction.cancel) { this.interaction.cancel(); } @@ -156,10 +153,31 @@ export class List extends React.Component { } }, 300) + // eslint-disable-next-line react/sort-comp + update = () => { + animateNextTransition(); + this.forceUpdate(); + }; + + unsubscribeMessages = () => { + if (this.messagesSubscription && this.messagesSubscription.unsubscribe) { + this.messagesSubscription.unsubscribe(); + } + } + + getLastMessage = () => { + const { messages } = this.state; + if (messages.length > 0) { + return messages[0]; + } + return null; + } + renderFooter = () => { const { loading } = this.state; - if (loading) { - return <ActivityIndicator style={styles.loading} />; + const { rid, theme } = this.props; + if (loading && rid) { + return <ActivityIndicator theme={theme} />; } return null; } @@ -172,13 +190,15 @@ export class List extends React.Component { render() { console.count(`${ this.constructor.name }.render calls`); + const { rid, listRef } = this.props; const { messages } = this.state; + const { theme } = this.props; return ( <> - <EmptyRoom length={messages.length} mounted={this.mounted} /> + <EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} theme={theme} /> <FlatList testID='room-view-messages' - ref={ref => this.list = ref} + ref={listRef} keyExtractor={item => item.id} data={messages} extraData={this.state} @@ -187,7 +207,7 @@ export class List extends React.Component { style={styles.list} inverted removeClippedSubviews={isIOS} - // initialNumToRender={7} + initialNumToRender={7} onEndReached={this.onEndReached} // onEndReachedThreshold={5} // maxToRenderPerBatch={5} @@ -199,3 +219,5 @@ export class List extends React.Component { ); } } + +export default List; diff --git a/app/views/RoomView/ReactionPicker.js b/app/views/RoomView/ReactionPicker.js index ca138cc43..1c158ab9a 100644 --- a/app/views/RoomView/ReactionPicker.js +++ b/app/views/RoomView/ReactionPicker.js @@ -8,9 +8,10 @@ import { responsive } from 'react-native-responsive-ui'; import EmojiPicker from '../../containers/EmojiPicker'; import styles from './styles'; import { isAndroid } from '../../utils/deviceInfo'; +import { withSplit } from '../../split'; const margin = isAndroid ? 40 : 20; -const tabEmojiStyle = { fontSize: 15 }; +const maxSize = 400; class ReactionPicker extends React.Component { static propTypes = { @@ -19,15 +20,16 @@ class ReactionPicker extends React.Component { message: PropTypes.object, show: PropTypes.bool, reactionClose: PropTypes.func, - onEmojiSelected: PropTypes.func + onEmojiSelected: PropTypes.func, + split: PropTypes.bool }; shouldComponentUpdate(nextProps) { - const { show, window } = this.props; - return nextProps.show !== show || window.width !== nextProps.window.width; + const { show, window, split } = this.props; + return nextProps.show !== show || window.width !== nextProps.window.width || nextProps.split !== split; } - onEmojiSelected(emoji, shortname) { + onEmojiSelected = (emoji, shortname) => { // standard emojis: `emoji` is unicode and `shortname` is :joy: // custom emojis: only `emoji` is returned with shortname type (:joy:) // to set reactions, we need shortname type @@ -37,9 +39,16 @@ class ReactionPicker extends React.Component { render() { const { - window: { width, height }, show, baseUrl, reactionClose + window: { width, height }, show, baseUrl, reactionClose, split } = this.props; + let widthStyle = width - margin; + let heightStyle = Math.min(width, height) - (margin * 2); + if (split) { + widthStyle = maxSize; + heightStyle = maxSize; + } + return (show ? ( <Modal @@ -51,13 +60,18 @@ class ReactionPicker extends React.Component { animationOut='fadeOut' > <View - style={[styles.reactionPickerContainer, { width: width - margin, height: Math.min(width, height) - (margin * 2) }]} + style={[ + styles.reactionPickerContainer, + { + width: widthStyle, + height: heightStyle + } + ]} testID='reaction-picker' > <EmojiPicker - tabEmojiStyle={tabEmojiStyle} - width={Math.min(width, height) - margin} - onEmojiSelected={(emoji, shortname) => this.onEmojiSelected(emoji, shortname)} + // tabEmojiStyle={tabEmojiStyle} + onEmojiSelected={this.onEmojiSelected} baseUrl={baseUrl} /> </View> @@ -72,4 +86,4 @@ const mapStateToProps = state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' }); -export default responsive(connect(mapStateToProps)(ReactionPicker)); +export default responsive(connect(mapStateToProps)(withSplit(ReactionPicker))); diff --git a/app/views/RoomView/Separator.js b/app/views/RoomView/Separator.js index ea772b11f..8e2e7a16b 100644 --- a/app/views/RoomView/Separator.js +++ b/app/views/RoomView/Separator.js @@ -5,7 +5,7 @@ import moment from 'moment'; import I18n from '../../i18n'; import sharedStyles from '../Styles'; -import { COLOR_DANGER, COLOR_TEXT_DESCRIPTION } from '../../constants/colors'; +import { themes } from '../../constants/colors'; const styles = StyleSheet.create({ container: { @@ -16,20 +16,12 @@ const styles = StyleSheet.create({ marginHorizontal: 14 }, line: { - backgroundColor: COLOR_TEXT_DESCRIPTION, height: 1, flex: 1 }, text: { fontSize: 14, - ...sharedStyles.textMedium, - ...sharedStyles.textColorDescription - }, - unreadLine: { - backgroundColor: COLOR_DANGER - }, - unreadText: { - color: COLOR_DANGER + ...sharedStyles.textMedium }, marginLeft: { marginLeft: 14 @@ -42,36 +34,39 @@ const styles = StyleSheet.create({ } }); -const DateSeparator = React.memo(({ ts, unread }) => { +const DateSeparator = React.memo(({ ts, unread, theme }) => { const date = ts ? moment(ts).format('MMM DD, YYYY') : null; + const unreadLine = { backgroundColor: themes[theme].dangerColor }; + const unreadText = { color: themes[theme].dangerColor }; if (ts && unread) { return ( <View style={styles.container}> - <Text style={[styles.text, styles.unreadText]}>{I18n.t('unread_messages')}</Text> - <View style={[styles.line, styles.unreadLine, styles.marginHorizontal]} /> - <Text style={[styles.text, styles.unreadText]}>{date}</Text> + <Text style={[styles.text, unreadText]}>{I18n.t('unread_messages')}</Text> + <View style={[styles.line, unreadLine, styles.marginHorizontal]} /> + <Text style={[styles.text, unreadText]}>{date}</Text> </View> ); } if (ts) { return ( <View style={styles.container}> - <View style={styles.line} /> - <Text style={[styles.text, styles.marginLeft]}>{date}</Text> + <View style={[styles.line, { backgroundColor: themes[theme].borderColor }]} /> + <Text style={[styles.text, { color: themes[theme].auxiliaryText }, styles.marginLeft]}>{date}</Text> </View> ); } return ( <View style={styles.container}> - <Text style={[styles.text, styles.unreadText, styles.marginRight]}>{I18n.t('unread_messages')}</Text> - <View style={[styles.line, styles.unreadLine]} /> + <Text style={[styles.text, unreadText, styles.marginRight]}>{I18n.t('unread_messages')}</Text> + <View style={[styles.line, unreadLine]} /> </View> ); }); DateSeparator.propTypes = { ts: PropTypes.instanceOf(Date), - unread: PropTypes.bool + unread: PropTypes.bool, + theme: PropTypes.string }; export default DateSeparator; diff --git a/app/views/RoomView/UploadProgress.js b/app/views/RoomView/UploadProgress.js index aa62f1b62..ae0ca9dc9 100644 --- a/app/views/RoomView/UploadProgress.js +++ b/app/views/RoomView/UploadProgress.js @@ -11,10 +11,9 @@ import RocketChat from '../../lib/rocketchat'; import log from '../../utils/log'; import I18n from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; -import { - COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_TEXT_DESCRIPTION, COLOR_DANGER -} from '../../constants/colors'; +import { themes } from '../../constants/colors'; import sharedStyles from '../Styles'; +import { withTheme } from '../../theme'; const styles = StyleSheet.create({ container: { @@ -24,10 +23,8 @@ const styles = StyleSheet.create({ maxHeight: 246 }, item: { - backgroundColor: COLOR_BACKGROUND_CONTAINER, height: 54, borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: COLOR_SEPARATOR, justifyContent: 'center', paddingHorizontal: 20 }, @@ -43,17 +40,14 @@ const styles = StyleSheet.create({ descriptionText: { fontSize: 16, lineHeight: 20, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, progress: { position: 'absolute', bottom: 0, - backgroundColor: COLOR_PRIMARY, height: 3 }, tryAgainButtonText: { - color: COLOR_PRIMARY, fontSize: 16, lineHeight: 20, ...sharedStyles.textMedium @@ -64,6 +58,7 @@ class UploadProgress extends Component { static propTypes = { window: PropTypes.object, rid: PropTypes.string, + theme: PropTypes.string, user: PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, @@ -94,6 +89,7 @@ class UploadProgress extends Component { init = () => { const { rid } = this.props; + if (!rid) { return; } const db = database.active; this.uploadsObservable = db.collections @@ -171,42 +167,56 @@ class UploadProgress extends Component { } renderItemContent = (item) => { - const { window } = this.props; + const { window, theme } = this.props; if (!item.error) { return ( [ <View key='row' style={styles.row}> - <CustomIcon name='file-generic' size={20} color={COLOR_TEXT_DESCRIPTION} /> - <Text style={[styles.descriptionContainer, styles.descriptionText]} ellipsizeMode='tail' numberOfLines={1}> + <CustomIcon name='file-generic' size={20} color={themes[theme].auxiliaryText} /> + <Text style={[styles.descriptionContainer, styles.descriptionText, { color: themes[theme].auxiliaryText }]} ellipsizeMode='tail' numberOfLines={1}> {I18n.t('Uploading')} {item.name} </Text> - <CustomIcon name='cross' size={20} color={COLOR_TEXT_DESCRIPTION} onPress={() => this.cancelUpload(item)} /> + <CustomIcon name='cross' size={20} color={themes[theme].auxiliaryText} onPress={() => this.cancelUpload(item)} /> </View>, - <View key='progress' style={[styles.progress, { width: (window.width * item.progress) / 100 }]} /> + <View key='progress' style={[styles.progress, { width: (window.width * item.progress) / 100, backgroundColor: themes[theme].tintColor }]} /> ] ); } return ( <View style={styles.row}> - <CustomIcon name='warning' size={20} color={COLOR_DANGER} /> + <CustomIcon name='warning' size={20} color={themes[theme].dangerColor} /> <View style={styles.descriptionContainer}> - <Text style={styles.descriptionText}>{I18n.t('Error_uploading')} {item.name}</Text> + <Text style={[styles.descriptionText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Error_uploading')} {item.name}</Text> <TouchableOpacity onPress={() => this.tryAgain(item)}> - <Text style={styles.tryAgainButtonText}>{I18n.t('Try_again')}</Text> + <Text style={[styles.tryAgainButtonText, { color: themes[theme].tintColor }]}>{I18n.t('Try_again')}</Text> </TouchableOpacity> </View> - <CustomIcon name='cross' size={20} color={COLOR_TEXT_DESCRIPTION} onPress={() => this.deleteUpload(item)} /> + <CustomIcon name='cross' size={20} color={themes[theme].auxiliaryText} onPress={() => this.deleteUpload(item)} /> </View> ); } // TODO: transform into stateless and update based on its own observable changes - renderItem = (item, index) => ( - <View key={item.path} style={[styles.item, index !== 0 ? { marginTop: 10 } : {}]}> - {this.renderItemContent(item)} - </View> - ); + renderItem = (item, index) => { + const { theme } = this.props; + + return ( + <View + key={item.path} + style={[ + styles.item, + index !== 0 ? { marginTop: 10 } : {}, + { + backgroundColor: themes[theme].chatComponentBackground, + borderColor: themes[theme].borderColor + } + ]} + > + {this.renderItemContent(item)} + </View> + ); + } render() { const { uploads } = this.state; @@ -218,4 +228,4 @@ class UploadProgress extends Component { } } -export default responsive(UploadProgress); +export default responsive(withTheme(UploadProgress)); diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 31e9f40ac..bb549a909 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -2,19 +2,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Text, View, InteractionManager } from 'react-native'; import { connect } from 'react-redux'; -import { RectButton } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-navigation'; -import { HeaderBackButton } from 'react-navigation-stack'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import moment from 'moment'; import * as Haptics from 'expo-haptics'; import { Q } from '@nozbe/watermelondb'; import isEqual from 'lodash/isEqual'; +import Touch from '../../utils/touch'; import { replyBroadcast as replyBroadcastAction } from '../../actions/messages'; -import { List } from './List'; +import List from './List'; import database from '../../lib/database'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; @@ -27,17 +26,27 @@ import styles from './styles'; import log from '../../utils/log'; import EventEmitter from '../../utils/events'; import I18n from '../../i18n'; -import RoomHeaderView, { RightButtons } from './Header'; +import RoomHeaderView, { RightButtons, RoomHeaderLeft } from './Header'; import StatusBar from '../../containers/StatusBar'; import Separator from './Separator'; -import { COLOR_WHITE, HEADER_BACK } from '../../constants/colors'; +import { themes } from '../../constants/colors'; import debounce from '../../utils/debounce'; import FileModal from '../../containers/FileModal'; import ReactionsModal from '../../containers/ReactionsModal'; import { LISTENER } from '../../containers/Toast'; import { isReadOnly, isBlocked } from '../../utils/room'; -import { isIOS } from '../../utils/deviceInfo'; +import { isIOS, isTablet } from '../../utils/deviceInfo'; import { showErrorAlert } from '../../utils/info'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; +import { + KEY_COMMAND, + handleCommandScroll, + handleCommandRoomActions, + handleCommandSearchMessages, + handleCommandReplyLatest +} from '../../commands'; +import ModalNavigation from '../../lib/ModalNavigation'; const stateAttrsUpdate = [ 'joined', @@ -55,16 +64,26 @@ const stateAttrsUpdate = [ const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout']; class RoomView extends React.Component { - static navigationOptions = ({ navigation }) => { - const rid = navigation.getParam('rid'); + static navigationOptions = ({ navigation, screenProps }) => { + const rid = navigation.getParam('rid', null); const prid = navigation.getParam('prid'); const title = navigation.getParam('name'); const t = navigation.getParam('t'); const tmid = navigation.getParam('tmid'); - const room = navigation.getParam('room'); + const baseUrl = navigation.getParam('baseUrl'); + const userId = navigation.getParam('userId'); + const token = navigation.getParam('token'); + const avatar = navigation.getParam('avatar'); const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {}); + const goRoomActionsView = navigation.getParam('goRoomActionsView', () => {}); const unreadsCount = navigation.getParam('unreadsCount', null); + if (!rid) { + return { + ...themedHeader(screenProps.theme) + }; + } return { + ...themedHeader(screenProps.theme), headerTitle: ( <RoomHeaderView rid={rid} @@ -73,24 +92,31 @@ class RoomView extends React.Component { title={title} type={t} widthOffset={tmid ? 95 : 130} + goRoomActionsView={goRoomActionsView} /> ), headerRight: ( <RightButtons rid={rid} tmid={tmid} - room={room} t={t} navigation={navigation} toggleFollowThread={toggleFollowThread} /> ), headerLeft: ( - <HeaderBackButton - title={unreadsCount > 999 ? '+999' : unreadsCount || ' '} - backTitleVisible={isIOS} - onPress={() => navigation.goBack()} - tintColor={HEADER_BACK} + <RoomHeaderLeft + tmid={tmid} + unreadsCount={unreadsCount} + navigation={navigation} + baseUrl={baseUrl} + userId={userId} + token={token} + title={avatar} + theme={screenProps.theme} + t={t} + goRoomActionsView={goRoomActionsView} + split={screenProps.split} /> ) }; @@ -111,7 +137,9 @@ class RoomView extends React.Component { Message_Read_Receipt_Enabled: PropTypes.bool, baseUrl: PropTypes.string, customEmojis: PropTypes.object, + screenProps: PropTypes.object, useMarkdown: PropTypes.bool, + theme: PropTypes.string, replyBroadcast: PropTypes.func }; @@ -124,9 +152,13 @@ class RoomView extends React.Component { this.tmid = props.navigation.getParam('tmid', null); const room = props.navigation.getParam('room'); const selectedMessage = props.navigation.getParam('message'); + const name = props.navigation.getParam('name'); + const fname = props.navigation.getParam('fname'); this.state = { joined: true, - room: room || { rid: this.rid, t: this.t }, + room: room || { + rid: this.rid, t: this.t, name, fname + }, roomUpdate: {}, lastOpen: null, photoModalVisible: false, @@ -145,13 +177,14 @@ class RoomView extends React.Component { if (room && room.observe) { this.observeRoom(room); - } else { + } else if (this.rid) { this.findAndObserveRoom(this.rid); } this.beginAnimating = false; this.didFocusListener = props.navigation.addListener('didFocus', () => this.beginAnimating = true); this.messagebox = React.createRef(); + this.list = React.createRef(); this.willBlurListener = props.navigation.addListener('willBlur', () => this.mounted = false); this.mounted = false; console.timeEnd(`${ this.constructor.name } init`); @@ -159,32 +192,48 @@ class RoomView extends React.Component { componentDidMount() { this.mounted = true; + this.offset = 0; this.didMountInteraction = InteractionManager.runAfterInteractions(() => { const { room } = this.state; - const { navigation, isAuthenticated } = this.props; - if (room.id && !this.tmid) { - navigation.setParams({ name: this.getRoomTitle(room), t: room.t }); + const { + navigation, isAuthenticated, user, baseUrl + } = this.props; + if ((room.id || room.rid) && !this.tmid) { + navigation.setParams({ + name: this.getRoomTitle(room), + avatar: room.name, + t: room.t, + token: user.token, + userId: user.id, + goRoomActionsView: this.goRoomActionsView, + baseUrl + }); } if (this.tmid) { - navigation.setParams({ toggleFollowThread: this.toggleFollowThread }); + navigation.setParams({ toggleFollowThread: this.toggleFollowThread, goRoomActionsView: this.goRoomActionsView }); } - if (isAuthenticated) { + if (isAuthenticated && this.rid) { this.init(); - } else { + } else if (this.rid) { EventEmitter.addEventListener('connected', this.handleConnected); } - if (isIOS) { + if (isIOS && this.rid) { this.updateUnreadCount(); } }); - + if (isTablet) { + EventEmitter.addEventListener(KEY_COMMAND, this.handleCommands); + } console.timeEnd(`${ this.constructor.name } mount`); } shouldComponentUpdate(nextProps, nextState) { const { state } = this; const { roomUpdate } = state; - const { appState } = this.props; + const { appState, theme } = this.props; + if (theme !== nextProps.theme) { + return true; + } if (appState !== nextProps.appState) { return true; } @@ -198,9 +247,13 @@ class RoomView extends React.Component { componentDidUpdate(prevProps) { const { appState } = this.props; - if (appState === 'foreground' && appState !== prevProps.appState) { + if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => { this.init(); + // Fire List.init() just to keep observables working + if (this.list && this.list.current) { + this.list.current.init(); + } }); } if (appState === 'background' && appState !== prevProps.appState) { @@ -260,9 +313,19 @@ class RoomView extends React.Component { this.queryUnreads.unsubscribe(); } EventEmitter.removeListener('connected', this.handleConnected); + if (isTablet) { + EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); + } console.countReset(`${ this.constructor.name }.render calls`); } + // eslint-disable-next-line react/sort-comp + goRoomActionsView = () => { + const { room } = this.state; + const { navigation } = this.props; + navigation.navigate('RoomActionsView', { rid: this.rid, t: this.t, room }); + } + // eslint-disable-next-line react/sort-comp init = () => { try { @@ -312,7 +375,7 @@ class RoomView extends React.Component { if (this.t !== 'd') { console.log('Room not found'); this.internalSetState({ joined: false }); - } else { + } else if (this.rid) { // We navigate to RoomView before the DM is inserted to the local db // So we retry just to make sure we have the right content this.retryFindCount = this.retryFindCount + 1 || 1; @@ -496,6 +559,9 @@ class RoomView extends React.Component { sendMessage = (message, tmid) => { const { user } = this.props; RocketChat.sendMessage(this.rid, message, this.tmid || tmid, user).then(() => { + if (this.list && this.list.current) { + this.list.current.update(); + } this.setLastOpen(null); }); }; @@ -599,11 +665,17 @@ class RoomView extends React.Component { } navToRoomInfo = (navParam) => { - const { navigation, user } = this.props; + const { room } = this.state; + const { navigation, user, screenProps } = this.props; if (navParam.rid === user.id) { return; } - navigation.navigate('RoomInfoView', navParam); + if (screenProps && screenProps.split) { + navigation.navigate('RoomActionsView', { rid: this.rid, t: this.t, room }); + ModalNavigation.navigate('RoomInfoView', navParam); + } else { + navigation.navigate('RoomInfoView', navParam); + } } callJitsi = () => { @@ -616,6 +688,29 @@ class RoomView extends React.Component { } }; + handleCommands = ({ event }) => { + if (this.rid) { + const { room } = this.state; + const { navigation } = this.props; + const { input } = event; + if (handleCommandScroll(event)) { + const offset = input === 'UIKeyInputUpArrow' ? 100 : -100; + this.offset += offset; + this.flatList.scrollToOffset({ offset: this.offset }); + } else if (handleCommandRoomActions(event)) { + navigation.navigate('RoomActionsView', { rid: this.rid, t: this.t, room }); + } else if (handleCommandSearchMessages(event)) { + navigation.navigate('RoomActionsView', { rid: this.rid, t: this.t, room }); + ModalNavigation.navigate('SearchMessagesView', { rid: this.rid }); + } else if (handleCommandReplyLatest(event)) { + if (this.list && this.list.current) { + const message = this.list.current.getLastMessage(); + this.onReplyInit(message, false); + } + } + } + } + get isReadOnly() { const { room } = this.state; const { user } = this.props; @@ -625,7 +720,7 @@ class RoomView extends React.Component { renderItem = (item, previousItem) => { const { room, lastOpen, canAutoTranslate } = this.state; const { - user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown, Message_Read_Receipt_Enabled + user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown, Message_Read_Receipt_Enabled, theme } = this.props; let dateSeparator = null; let showUnreadSeparator = false; @@ -682,6 +777,7 @@ class RoomView extends React.Component { <Separator ts={dateSeparator} unread={showUnreadSeparator} + theme={theme} /> </> ); @@ -694,34 +790,36 @@ class RoomView extends React.Component { const { joined, room, selectedMessage, editing, replying, replyWithMention } = this.state; - const { navigation } = this.props; + const { navigation, theme } = this.props; + if (!this.rid) { + return null; + } if (!joined && !this.tmid) { return ( <View style={styles.joinRoomContainer} key='room-view-join' testID='room-view-join'> - <Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text> - <RectButton + <Text style={[styles.previewMode, { color: themes[theme].titleText }]}>{I18n.t('You_are_in_preview_mode')}</Text> + <Touch onPress={this.joinRoom} - style={styles.joinRoomButton} - activeOpacity={0.5} - underlayColor={COLOR_WHITE} + style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]} + theme={theme} > - <Text style={styles.joinRoomText} testID='room-view-join-button'>{I18n.t('Join')}</Text> - </RectButton> + <Text style={[styles.joinRoomText, { color: themes[theme].buttonText }]} testID='room-view-join-button'>{I18n.t('Join')}</Text> + </Touch> </View> ); } if (this.isReadOnly) { return ( <View style={styles.readOnly}> - <Text style={styles.previewMode}>{I18n.t('This_room_is_read_only')}</Text> + <Text style={[styles.previewMode, { color: themes[theme].titleText }]}>{I18n.t('This_room_is_read_only')}</Text> </View> ); } if (isBlocked(room)) { return ( <View style={styles.readOnly}> - <Text style={styles.previewMode}>{I18n.t('This_room_is_blocked')}</Text> + <Text style={[styles.previewMode, { color: themes[theme].titleText }]}>{I18n.t('This_room_is_blocked')}</Text> </View> ); } @@ -732,7 +830,7 @@ class RoomView extends React.Component { rid={this.rid} tmid={this.tmid} roomType={room.t} - isFocused={navigation.isFocused()} + isFocused={navigation.isFocused} message={selectedMessage} editing={editing} editRequest={this.onEditRequest} @@ -775,6 +873,7 @@ class RoomView extends React.Component { } {showErrorActions ? ( <MessageErrorActions + tmid={this.tmid} message={selectedMessage} actionsHide={this.onErrorActionsHide} /> @@ -783,21 +882,33 @@ class RoomView extends React.Component { ); } + setListRef = ref => this.flatList = ref; + render() { console.count(`${ this.constructor.name }.render calls`); const { room, photoModalVisible, reactionsModalVisible, selectedAttachment, selectedMessage, loading, reacting } = this.state; - const { user, baseUrl } = this.props; + const { user, baseUrl, theme } = this.props; const { rid, t } = room; return ( - <SafeAreaView style={styles.container} testID='room-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView + style={[ + styles.container, + { backgroundColor: themes[theme].backgroundColor } + ]} + testID='room-view' + forceInset={{ vertical: 'never' }} + > + <StatusBar theme={theme} /> <List + ref={this.list} + listRef={this.setListRef} rid={rid} t={t} tmid={this.tmid} + theme={theme} room={room} renderRow={this.renderItem} loading={loading} @@ -853,4 +964,4 @@ const mapDispatchToProps = dispatch => ({ replyBroadcast: message => dispatch(replyBroadcastAction(message)) }); -export default connect(mapStateToProps, mapDispatchToProps)(RoomView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RoomView)); diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js index 11fcfd670..9f011413e 100644 --- a/app/views/RoomView/styles.js +++ b/app/views/RoomView/styles.js @@ -1,14 +1,10 @@ import { StyleSheet } from 'react-native'; -import { - COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION -} from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, safeAreaView: { flex: 1 @@ -19,26 +15,15 @@ export default StyleSheet.create({ contentContainer: { paddingTop: 10 }, - separator: { - height: 1, - backgroundColor: COLOR_SEPARATOR - }, - loading: { - flex: 1, - padding: 15, - color: COLOR_TEXT_DESCRIPTION - }, readOnly: { justifyContent: 'flex-end', alignItems: 'center', marginVertical: 15 }, reactionPickerContainer: { - // width: width - 20, - // height: width - 20, - backgroundColor: '#F7F7F7', borderRadius: 4, - flexDirection: 'column' + flexDirection: 'column', + overflow: 'hidden' }, joinRoomContainer: { justifyContent: 'flex-end', @@ -52,17 +37,14 @@ export default StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - backgroundColor: COLOR_PRIMARY, borderRadius: 2 }, joinRoomText: { - color: COLOR_WHITE, fontSize: 14, ...sharedStyles.textMedium }, previewMode: { fontSize: 16, - ...sharedStyles.textMedium, - ...sharedStyles.textColorNormal + ...sharedStyles.textMedium } }); diff --git a/app/views/RoomsListView/Header/Header.android.js b/app/views/RoomsListView/Header/Header.android.js index f331e81a6..b40a9442d 100644 --- a/app/views/RoomsListView/Header/Header.android.js +++ b/app/views/RoomsListView/Header/Header.android.js @@ -3,11 +3,11 @@ import { Text, View, TouchableOpacity, Image, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; -import { TextInput } from 'react-native-gesture-handler'; +import TextInput from '../../../presentation/TextInput'; import I18n from '../../../i18n'; import sharedStyles from '../../Styles'; -import { COLOR_WHITE } from '../../../constants/colors'; +import { themes } from '../../../constants/colors'; const styles = StyleSheet.create({ container: { @@ -20,7 +20,6 @@ const styles = StyleSheet.create({ }, server: { fontSize: 20, - color: COLOR_WHITE, ...sharedStyles.textRegular }, serverSmall: { @@ -28,7 +27,6 @@ const styles = StyleSheet.create({ }, updating: { fontSize: 14, - color: COLOR_WHITE, ...sharedStyles.textRegular }, disclosure: { @@ -43,17 +41,19 @@ const styles = StyleSheet.create({ }); const Header = React.memo(({ - connecting, isFetching, serverName, showServerDropdown, showSearchHeader, onSearchChangeText, onPress + connecting, isFetching, serverName, showServerDropdown, showSearchHeader, theme, onSearchChangeText, onPress }) => { + const titleColorStyle = { color: themes[theme].headerTitleColor }; + const isLight = theme === 'light'; if (showSearchHeader) { return ( <View style={styles.container}> <TextInput autoFocus - style={styles.server} + style={[styles.server, isLight && titleColorStyle]} placeholder='Search' - placeholderTextColor='rgba(255, 255, 255, 0.5)' onChangeText={onSearchChangeText} + theme={theme} /> </View> ); @@ -65,11 +65,18 @@ const Header = React.memo(({ testID='rooms-list-header-server-dropdown-button' disabled={connecting || isFetching} > - {connecting ? <Text style={styles.updating}>{I18n.t('Connecting')}</Text> : null} - {isFetching ? <Text style={styles.updating}>{I18n.t('Updating')}</Text> : null} + {connecting ? <Text style={[styles.updating, titleColorStyle]}>{I18n.t('Connecting')}</Text> : null} + {isFetching ? <Text style={[styles.updating, titleColorStyle]}>{I18n.t('Updating')}</Text> : null} <View style={styles.button}> - <Text style={[styles.server, isFetching && styles.serverSmall]}>{serverName}</Text> - <Image style={[styles.disclosure, showServerDropdown && styles.upsideDown]} source={{ uri: 'disclosure_indicator_server' }} /> + <Text style={[styles.server, isFetching && styles.serverSmall, titleColorStyle]}>{serverName}</Text> + <Image + style={[ + styles.disclosure, + showServerDropdown && styles.upsideDown, + { tintColor: themes[theme].headerTitleColor } + ]} + source={{ uri: 'disclosure_indicator_server' }} + /> </View> </TouchableOpacity> </View> @@ -83,7 +90,8 @@ Header.propTypes = { onSearchChangeText: PropTypes.func.isRequired, connecting: PropTypes.bool, isFetching: PropTypes.bool, - serverName: PropTypes.string + serverName: PropTypes.string, + theme: PropTypes.string }; Header.defaultProps = { diff --git a/app/views/RoomsListView/Header/Header.ios.js b/app/views/RoomsListView/Header/Header.ios.js index 09adacfec..31f887cf5 100644 --- a/app/views/RoomsListView/Header/Header.ios.js +++ b/app/views/RoomsListView/Header/Header.ios.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import I18n from '../../../i18n'; import sharedStyles from '../../Styles'; -import { COLOR_PRIMARY } from '../../../constants/colors'; +import { themes } from '../../../constants/colors'; const styles = StyleSheet.create({ container: { @@ -19,12 +19,10 @@ const styles = StyleSheet.create({ }, title: { fontSize: 14, - ...sharedStyles.textColorTitle, ...sharedStyles.textRegular }, server: { fontSize: 12, - color: COLOR_PRIMARY, ...sharedStyles.textRegular }, disclosure: { @@ -39,7 +37,7 @@ const styles = StyleSheet.create({ } }); -const HeaderTitle = React.memo(({ connecting, isFetching }) => { +const HeaderTitle = React.memo(({ connecting, isFetching, theme }) => { let title = I18n.t('Messages'); if (connecting) { title = I18n.t('Connecting'); @@ -47,11 +45,11 @@ const HeaderTitle = React.memo(({ connecting, isFetching }) => { if (isFetching) { title = I18n.t('Updating'); } - return <Text style={styles.title}>{title}</Text>; + return <Text style={[styles.title, { color: themes[theme].headerTitleColor }]}>{title}</Text>; }); const Header = React.memo(({ - connecting, isFetching, serverName, showServerDropdown, onPress + connecting, isFetching, serverName, showServerDropdown, onPress, theme }) => ( <View style={styles.container}> <TouchableOpacity @@ -60,9 +58,9 @@ const Header = React.memo(({ style={styles.container} disabled={connecting || isFetching} > - <HeaderTitle connecting={connecting} isFetching={isFetching} /> + <HeaderTitle connecting={connecting} isFetching={isFetching} theme={theme} /> <View style={styles.button}> - <Text style={styles.server}>{serverName}</Text> + <Text style={[styles.server, { color: themes[theme].headerTintColor }]}>{serverName}</Text> <Image style={[styles.disclosure, showServerDropdown && styles.upsideDown]} source={{ uri: 'disclosure_indicator_server' }} /> </View> </TouchableOpacity> @@ -73,6 +71,7 @@ Header.propTypes = { connecting: PropTypes.bool, isFetching: PropTypes.bool, serverName: PropTypes.string, + theme: PropTypes.string, showServerDropdown: PropTypes.bool.isRequired, onPress: PropTypes.func.isRequired }; @@ -83,7 +82,8 @@ Header.defaultProps = { HeaderTitle.propTypes = { connecting: PropTypes.bool, - isFetching: PropTypes.bool + isFetching: PropTypes.bool, + theme: PropTypes.string }; export default Header; diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js index db169eca7..294cf9eb6 100644 --- a/app/views/RoomsListView/Header/index.js +++ b/app/views/RoomsListView/Header/index.js @@ -6,6 +6,10 @@ import { toggleServerDropdown, closeServerDropdown, closeSortDropdown, setSearch as setSearchAction } from '../../../actions/rooms'; import Header from './Header'; +import { withTheme } from '../../../theme'; +import EventEmitter from '../../../utils/events'; +import { KEY_COMMAND, handleCommandOpenServerDropdown } from '../../../commands'; +import { isTablet } from '../../../utils/deviceInfo'; class RoomsListHeaderView extends PureComponent { static propTypes = { @@ -15,12 +19,31 @@ class RoomsListHeaderView extends PureComponent { serverName: PropTypes.string, connecting: PropTypes.bool, isFetching: PropTypes.bool, + theme: PropTypes.string, open: PropTypes.func, close: PropTypes.func, closeSort: PropTypes.func, setSearch: PropTypes.func } + componentDidMount() { + if (isTablet) { + EventEmitter.addEventListener(KEY_COMMAND, this.handleCommands); + } + } + + componentWillUnmount() { + if (isTablet) { + EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); + } + } + + handleCommands = ({ event }) => { + if (handleCommandOpenServerDropdown(event)) { + this.onPress(); + } + } + onSearchChangeText = (text) => { const { setSearch } = this.props; setSearch(text.trim()); @@ -44,11 +67,12 @@ class RoomsListHeaderView extends PureComponent { render() { const { - serverName, showServerDropdown, showSearchHeader, connecting, isFetching + serverName, showServerDropdown, showSearchHeader, connecting, isFetching, theme } = this.props; return ( <Header + theme={theme} serverName={serverName} showServerDropdown={showServerDropdown} showSearchHeader={showSearchHeader} @@ -77,4 +101,4 @@ const mapDispatchtoProps = dispatch => ({ setSearch: searchText => dispatch(setSearchAction(searchText)) }); -export default connect(mapStateToProps, mapDispatchtoProps)(RoomsListHeaderView); +export default connect(mapStateToProps, mapDispatchtoProps)(withTheme(RoomsListHeaderView)); diff --git a/app/views/RoomsListView/ListHeader/Directory.js b/app/views/RoomsListView/ListHeader/Directory.js index 0e83ec175..477f1a6cb 100644 --- a/app/views/RoomsListView/ListHeader/Directory.js +++ b/app/views/RoomsListView/ListHeader/Directory.js @@ -1,30 +1,40 @@ import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; +import Touch from '../../../utils/touch'; import { CustomIcon } from '../../../lib/Icons'; import I18n from '../../../i18n'; -import Touch from '../../../utils/touch'; import styles from '../styles'; import DisclosureIndicator from '../../../containers/DisclosureIndicator'; +import { themes } from '../../../constants/colors'; +import { withTheme } from '../../../theme'; - -const Directory = React.memo(({ goDirectory }) => ( - <Touch - key='rooms-list-view-sort' - onPress={goDirectory} - style={styles.dropdownContainerHeader} - > - <View style={styles.sortItemContainer}> - <CustomIcon style={styles.directoryIcon} size={22} name='discover' /> - <Text style={styles.directoryText}>{I18n.t('Directory')}</Text> - <DisclosureIndicator /> - </View> - </Touch> -)); +const Directory = React.memo(({ goDirectory, theme }) => { + const color = { color: themes[theme].headerSecondaryText }; + return ( + <Touch + onPress={goDirectory} + theme={theme} + style={{ backgroundColor: themes[theme].headerSecondaryBackground }} + > + <View + style={[ + styles.dropdownContainerHeader, + { borderBottomWidth: StyleSheet.hairlineWidth, borderColor: themes[theme].separatorColor } + ]} + > + <CustomIcon style={[styles.directoryIcon, color]} size={22} name='discover' /> + <Text style={[styles.directoryText, color]}>{I18n.t('Directory')}</Text> + <DisclosureIndicator theme={theme} /> + </View> + </Touch> + ); +}); Directory.propTypes = { - goDirectory: PropTypes.func + goDirectory: PropTypes.func, + theme: PropTypes.string }; -export default Directory; +export default withTheme(Directory); diff --git a/app/views/RoomsListView/ListHeader/SearchBar.js b/app/views/RoomsListView/ListHeader/SearchBar.js index 31e0372d3..d18393793 100644 --- a/app/views/RoomsListView/ListHeader/SearchBar.js +++ b/app/views/RoomsListView/ListHeader/SearchBar.js @@ -3,16 +3,26 @@ import PropTypes from 'prop-types'; import SearchBox from '../../../containers/SearchBox'; import { isIOS } from '../../../utils/deviceInfo'; +import { withTheme } from '../../../theme'; -const SearchBar = React.memo(({ onChangeSearchText }) => { +const SearchBar = React.memo(({ theme, onChangeSearchText, inputRef }) => { if (isIOS) { - return <SearchBox onChangeText={onChangeSearchText} testID='rooms-list-view-search' key='rooms-list-view-search' />; + return ( + <SearchBox + onChangeText={onChangeSearchText} + testID='rooms-list-view-search' + inputRef={inputRef} + theme={theme} + /> + ); } return null; }); SearchBar.propTypes = { + theme: PropTypes.string, + inputRef: PropTypes.func, onChangeSearchText: PropTypes.func }; -export default SearchBar; +export default withTheme(SearchBar); diff --git a/app/views/RoomsListView/ListHeader/Sort.js b/app/views/RoomsListView/ListHeader/Sort.js index 087724289..f5ec8820c 100644 --- a/app/views/RoomsListView/ListHeader/Sort.js +++ b/app/views/RoomsListView/ListHeader/Sort.js @@ -1,26 +1,35 @@ import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; +import Touch from '../../../utils/touch'; import { CustomIcon } from '../../../lib/Icons'; import I18n from '../../../i18n'; -import Touch from '../../../utils/touch'; import styles from '../styles'; +import { themes } from '../../../constants/colors'; +import { withTheme } from '../../../theme'; -const Sort = React.memo(({ searchLength, sortBy, toggleSort }) => { +const Sort = React.memo(({ + searchLength, sortBy, toggleSort, theme +}) => { if (searchLength > 0) { return null; } return ( <Touch - key='rooms-list-view-sort' onPress={toggleSort} - style={styles.dropdownContainerHeader} + theme={theme} + style={{ backgroundColor: themes[theme].headerSecondaryBackground }} > - <View style={styles.sortItemContainer}> - <Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text> - <CustomIcon style={styles.sortIcon} size={22} name='sort1' /> + <View + style={[ + styles.dropdownContainerHeader, + { borderBottomWidth: StyleSheet.hairlineWidth, borderColor: themes[theme].separatorColor } + ]} + > + <Text style={[styles.sortToggleText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text> + <CustomIcon style={[styles.sortIcon, { color: themes[theme].auxiliaryText }]} size={22} name='sort1' /> </View> </Touch> ); @@ -29,7 +38,8 @@ const Sort = React.memo(({ searchLength, sortBy, toggleSort }) => { Sort.propTypes = { searchLength: PropTypes.number, sortBy: PropTypes.string, + theme: PropTypes.string, toggleSort: PropTypes.func }; -export default Sort; +export default withTheme(Sort); diff --git a/app/views/RoomsListView/ListHeader/index.js b/app/views/RoomsListView/ListHeader/index.js index bdef7d1cc..106d80c71 100644 --- a/app/views/RoomsListView/ListHeader/index.js +++ b/app/views/RoomsListView/ListHeader/index.js @@ -6,10 +6,10 @@ import Directory from './Directory'; import Sort from './Sort'; const ListHeader = React.memo(({ - searchLength, sortBy, onChangeSearchText, toggleSort, goDirectory + searchLength, sortBy, onChangeSearchText, toggleSort, goDirectory, inputRef }) => ( <> - <SearchBar onChangeSearchText={onChangeSearchText} /> + <SearchBar onChangeSearchText={onChangeSearchText} inputRef={inputRef} /> <Directory goDirectory={goDirectory} /> <Sort searchLength={searchLength} sortBy={sortBy} toggleSort={toggleSort} /> </> @@ -20,7 +20,8 @@ ListHeader.propTypes = { sortBy: PropTypes.string, onChangeSearchText: PropTypes.func, toggleSort: PropTypes.func, - goDirectory: PropTypes.func + goDirectory: PropTypes.func, + inputRef: PropTypes.func }; export default ListHeader; diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js index b7daac0a8..88fe27117 100644 --- a/app/views/RoomsListView/ServerDropdown.js +++ b/app/views/RoomsListView/ServerDropdown.js @@ -18,6 +18,11 @@ import I18n from '../../i18n'; import EventEmitter from '../../utils/events'; import Check from '../../containers/Check'; import database from '../../lib/database'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { KEY_COMMAND, handleCommandSelectServer } from '../../commands'; +import { isTablet } from '../../utils/deviceInfo'; +import { withSplit } from '../../split'; const ROW_HEIGHT = 68; const ANIMATION_DURATION = 200; @@ -26,7 +31,9 @@ class ServerDropdown extends Component { static propTypes = { navigation: PropTypes.object, closeServerDropdown: PropTypes.bool, + split: PropTypes.bool, server: PropTypes.string, + theme: PropTypes.string, toggleServerDropdown: PropTypes.func, selectServerRequest: PropTypes.func, appStart: PropTypes.func @@ -56,13 +63,19 @@ class ServerDropdown extends Component { duration: ANIMATION_DURATION, easing: Easing.inOut(Easing.quad), useNativeDriver: true - }, + } ).start(); + if (isTablet) { + EventEmitter.addEventListener(KEY_COMMAND, this.handleCommands); + } } shouldComponentUpdate(nextProps, nextState) { const { servers } = this.state; - const { closeServerDropdown, server } = this.props; + const { closeServerDropdown, server, theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextProps.closeServerDropdown !== closeServerDropdown) { return true; } @@ -90,6 +103,9 @@ class ServerDropdown extends Component { if (this.subscription && this.subscription.unsubscribe) { this.subscription.unsubscribe(); } + if (isTablet) { + EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); + } } close = () => { @@ -116,12 +132,15 @@ class ServerDropdown extends Component { select = async(server) => { const { - server: currentServer, selectServerRequest, appStart + server: currentServer, selectServerRequest, appStart, navigation, split } = this.props; this.close(); if (currentServer !== server) { const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); + if (split) { + navigation.navigate('RoomView'); + } if (!userId) { appStart(); this.newServerTimeout = setTimeout(() => { @@ -133,13 +152,32 @@ class ServerDropdown extends Component { } } - renderSeparator = () => <View style={styles.serverSeparator} />; + handleCommands = ({ event }) => { + const { servers } = this.state; + const { navigation } = this.props; + const { input } = event; + if (handleCommandSelectServer(event)) { + if (servers[input - 1]) { + this.select(servers[input - 1].id); + navigation.navigate('RoomView'); + } + } + } + + renderSeparator = () => { + const { theme } = this.props; + return <View style={[styles.serverSeparator, { backgroundColor: themes[theme].separatorColor }]} />; + } renderServer = ({ item }) => { - const { server } = this.props; + const { server, theme } = this.props; return ( - <Touch onPress={() => this.select(item.id)} style={styles.serverItem} testID={`rooms-list-header-server-${ item.id }`}> + <Touch + onPress={() => this.select(item.id)} + testID={`rooms-list-header-server-${ item.id }`} + theme={theme} + > <View style={styles.serverItemContainer}> {item.iconURL ? ( @@ -158,10 +196,10 @@ class ServerDropdown extends Component { ) } <View style={styles.serverTextContainer}> - <Text style={styles.serverName}>{item.name || item.id}</Text> - <Text style={styles.serverUrl}>{item.id}</Text> + <Text style={[styles.serverName, { color: themes[theme].titleText }]}>{item.name || item.id}</Text> + <Text style={[styles.serverUrl, { color: themes[theme].auxiliaryText }]}>{item.id}</Text> </View> - {item.id === server ? <Check /> : null} + {item.id === server ? <Check theme={theme} /> : null} </View> </Touch> ); @@ -169,6 +207,7 @@ class ServerDropdown extends Component { render() { const { servers } = this.state; + const { theme } = this.props; const maxRows = 4; const initialTop = 41 + (Math.min(servers.length, maxRows) * ROW_HEIGHT); const translateY = this.animatedValue.interpolate({ @@ -177,22 +216,34 @@ class ServerDropdown extends Component { }); const backdropOpacity = this.animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [0, 0.3] + outputRange: [0, 0.6] }); return ( - [ - <TouchableWithoutFeedback key='sort-backdrop' onPress={this.close}> - <Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]} /> - </TouchableWithoutFeedback>, + <> + <TouchableWithoutFeedback onPress={this.close}> + <Animated.View style={[styles.backdrop, { backgroundColor: themes[theme].backdropColor, opacity: backdropOpacity }]} /> + </TouchableWithoutFeedback> <Animated.View - key='sort-container' - style={[styles.dropdownContainer, { transform: [{ translateY }] }]} + style={[ + styles.dropdownContainer, + { + transform: [{ translateY }], + backgroundColor: themes[theme].backgroundColor, + borderColor: themes[theme].separatorColor + } + ]} testID='rooms-list-header-server-dropdown' > - <View style={[styles.dropdownContainerHeader, styles.serverHeader]}> - <Text style={styles.serverHeaderText}>{I18n.t('Server')}</Text> + <View + style={[ + styles.dropdownContainerHeader, + styles.serverHeader, + { borderColor: themes[theme].separatorColor } + ]} + > + <Text style={[styles.serverHeaderText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Server')}</Text> <TouchableOpacity onPress={this.addServer} testID='rooms-list-header-server-add'> - <Text style={styles.serverHeaderAdd}>{I18n.t('Add_Server')}</Text> + <Text style={[styles.serverHeaderAdd, { color: themes[theme].tintColor }]}>{I18n.t('Add_Server')}</Text> </TouchableOpacity> </View> <FlatList @@ -201,9 +252,10 @@ class ServerDropdown extends Component { keyExtractor={item => item.id} renderItem={this.renderServer} ItemSeparatorComponent={this.renderSeparator} + keyboardShouldPersistTaps='always' /> </Animated.View> - ] + </> ); } } @@ -219,4 +271,4 @@ const mapDispatchToProps = dispatch => ({ appStart: () => dispatch(appStartAction('outside')) }); -export default withNavigation(connect(mapStateToProps, mapDispatchToProps)(ServerDropdown)); +export default withNavigation(connect(mapStateToProps, mapDispatchToProps)(withTheme(withSplit(ServerDropdown)))); diff --git a/app/views/RoomsListView/SortDropdown.js b/app/views/RoomsListView/SortDropdown.js index e11d980e8..caa4d41f5 100644 --- a/app/views/RoomsListView/SortDropdown.js +++ b/app/views/RoomsListView/SortDropdown.js @@ -40,7 +40,7 @@ class Sort extends PureComponent { duration: ANIMATION_DURATION, easing: Easing.inOut(Easing.quad), useNativeDriver: true - }, + } ).start(); } @@ -96,7 +96,7 @@ class Sort extends PureComponent { duration: ANIMATION_DURATION, easing: Easing.inOut(Easing.quad), useNativeDriver: true - }, + } ).start(() => close()); } diff --git a/app/views/RoomsListView/SortDropdown/Item.js b/app/views/RoomsListView/SortDropdown/Item.js new file mode 100644 index 000000000..b842b3f72 --- /dev/null +++ b/app/views/RoomsListView/SortDropdown/Item.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { View, Text, Image } from 'react-native'; +import PropTypes from 'prop-types'; + +import styles from '../styles'; +import Touch from '../../../utils/touch'; +import I18n from '../../../i18n'; +import { CustomIcon } from '../../../lib/Icons'; +import Check from '../../../containers/Check'; +import { themes } from '../../../constants/colors'; + + +export const SortItemButton = ({ children, onPress, theme }) => ( + <Touch + style={styles.sortItemButton} + onPress={onPress} + theme={theme} + > + {children} + </Touch> +); + +SortItemButton.propTypes = { + theme: PropTypes.string, + children: PropTypes.node, + onPress: PropTypes.func +}; + +export const SortItemContent = ({ + label, icon, imageUri, checked, theme +}) => ( + <View style={styles.sortItemContainer}> + {icon && <CustomIcon style={[styles.sortIcon, { color: themes[theme].controlText }]} size={22} name={icon} />} + {imageUri && <Image style={[styles.sortIcon, { tintColor: themes[theme].controlText }]} source={{ uri: imageUri }} />} + <Text style={[styles.sortItemText, { color: themes[theme].controlText }]}>{I18n.t(label)}</Text> + {checked ? <Check theme={theme} /> : null} + </View> +); + +SortItemContent.propTypes = { + theme: PropTypes.string, + label: PropTypes.string, + icon: PropTypes.string, + imageUri: PropTypes.string, + checked: PropTypes.bool +}; diff --git a/app/views/RoomsListView/SortDropdown/index.js b/app/views/RoomsListView/SortDropdown/index.js new file mode 100644 index 000000000..3f68fef0e --- /dev/null +++ b/app/views/RoomsListView/SortDropdown/index.js @@ -0,0 +1,200 @@ +import React, { PureComponent } from 'react'; +import { + View, Text, Animated, Easing, TouchableWithoutFeedback +} from 'react-native'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import styles from '../styles'; +import Touch from '../../../utils/touch'; +import RocketChat from '../../../lib/rocketchat'; +import { setPreference } from '../../../actions/sortPreferences'; +import log from '../../../utils/log'; +import I18n from '../../../i18n'; +import { CustomIcon } from '../../../lib/Icons'; +import { withTheme } from '../../../theme'; +import { themes } from '../../../constants/colors'; +import { SortItemButton, SortItemContent } from './Item'; + +const ANIMATION_DURATION = 200; + +class Sort extends PureComponent { + static propTypes = { + closeSortDropdown: PropTypes.bool, + close: PropTypes.func, + sortBy: PropTypes.string, + groupByType: PropTypes.bool, + showFavorites: PropTypes.bool, + showUnread: PropTypes.bool, + theme: PropTypes.string, + setSortPreference: PropTypes.func + } + + constructor(props) { + super(props); + this.animatedValue = new Animated.Value(0); + } + + componentDidMount() { + Animated.timing( + this.animatedValue, + { + toValue: 1, + duration: ANIMATION_DURATION, + easing: Easing.inOut(Easing.quad), + useNativeDriver: true + } + ).start(); + } + + componentDidUpdate(prevProps) { + const { closeSortDropdown } = this.props; + if (prevProps.closeSortDropdown !== closeSortDropdown) { + this.close(); + } + } + + setSortPreference = (param) => { + const { setSortPreference } = this.props; + + try { + setSortPreference(param); + RocketChat.saveSortPreference(param); + } catch (e) { + log(e); + } + } + + sortByName = () => { + this.setSortPreference({ sortBy: 'alphabetical' }); + this.close(); + } + + sortByActivity = () => { + this.setSortPreference({ sortBy: 'activity' }); + this.close(); + } + + toggleGroupByType = () => { + const { groupByType } = this.props; + this.setSortPreference({ groupByType: !groupByType }); + } + + toggleGroupByFavorites = () => { + const { showFavorites } = this.props; + this.setSortPreference({ showFavorites: !showFavorites }); + } + + toggleUnread = () => { + const { showUnread } = this.props; + this.setSortPreference({ showUnread: !showUnread }); + } + + close = () => { + const { close } = this.props; + Animated.timing( + this.animatedValue, + { + toValue: 0, + duration: ANIMATION_DURATION, + easing: Easing.inOut(Easing.quad), + useNativeDriver: true + } + ).start(() => close()); + } + + render() { + const translateY = this.animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [-326, 0] + }); + const backdropOpacity = this.animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.3] + }); + const { + sortBy, groupByType, showFavorites, showUnread, theme + } = this.props; + + return ( + <> + <TouchableWithoutFeedback onPress={this.close}> + <Animated.View style={[styles.backdrop, { backgroundColor: themes[theme].backdropColor, opacity: backdropOpacity }]} /> + </TouchableWithoutFeedback> + <Animated.View + style={[ + styles.dropdownContainer, + { + transform: [{ translateY }], + backgroundColor: themes[theme].backgroundColor, + borderColor: themes[theme].separatorColor + } + ]} + > + <Touch + onPress={this.close} + theme={theme} + > + <View style={[styles.dropdownContainerHeader, { borderColor: themes[theme].separatorColor }]}> + <View style={styles.sortItemContainer}> + <Text style={[styles.sortToggleText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text> + <CustomIcon style={[styles.sortIcon, { color: themes[theme].auxiliaryText }]} size={22} name='sort1' /> + </View> + </View> + </Touch> + <SortItemButton onPress={this.sortByName} theme={theme}> + <SortItemContent + icon='sort' + label='Alphabetical' + checked={sortBy === 'alphabetical'} + theme={theme} + /> + </SortItemButton> + <SortItemButton onPress={this.sortByActivity} theme={theme}> + <SortItemContent + imageUri='sort_activity' + label='Activity' + checked={sortBy === 'activity'} + theme={theme} + /> + </SortItemButton> + <View style={[styles.sortSeparator, { backgroundColor: themes[theme].separatorColor }]} /> + <SortItemButton onPress={this.toggleGroupByType} theme={theme}> + <SortItemContent + icon='sort1' + label='Group_by_type' + checked={groupByType} + theme={theme} + /> + </SortItemButton> + <SortItemButton onPress={this.toggleGroupByFavorites} theme={theme}> + <SortItemContent + icon='star' + label='Group_by_favorites' + checked={showFavorites} + theme={theme} + /> + </SortItemButton> + <SortItemButton onPress={this.toggleUnread} theme={theme}> + <SortItemContent + icon='eye-off' + label='Unread_on_top' + checked={showUnread} + theme={theme} + /> + </SortItemButton> + </Animated.View> + </> + ); + } +} + +const mapStateToProps = state => ({ + closeSortDropdown: state.rooms.closeSortDropdown +}); + +const mapDispatchToProps = dispatch => ({ + setSortPreference: preference => dispatch(setPreference(preference)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(Sort)); diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index f3f25a6e5..fa21c2680 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -4,9 +4,7 @@ import { View, FlatList, BackHandler, - ActivityIndicator, Text, - ScrollView, Keyboard, Dimensions } from 'react-native'; @@ -28,11 +26,12 @@ import { toggleSortDropdown as toggleSortDropdownAction, openSearchHeader as openSearchHeaderAction, closeSearchHeader as closeSearchHeaderAction, - roomsRequest as roomsRequestAction + roomsRequest as roomsRequestAction, + closeServerDropdown as closeServerDropdownAction } from '../../actions/rooms'; import { appStart as appStartAction } from '../../actions'; import debounce from '../../utils/debounce'; -import { isIOS, isAndroid } from '../../utils/deviceInfo'; +import { isIOS, isAndroid, isTablet } from '../../utils/deviceInfo'; import RoomsListHeaderView from './Header'; import { DrawerButton, @@ -40,11 +39,39 @@ import { Item } from '../../containers/HeaderButton'; import StatusBar from '../../containers/StatusBar'; +import ActivityIndicator from '../../containers/ActivityIndicator'; import ListHeader from './ListHeader'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; import { animateNextTransition } from '../../utils/layoutAnimation'; +import { withTheme } from '../../theme'; +import { themes } from '../../constants/colors'; +import { themedHeader } from '../../utils/navigation'; +import EventEmitter from '../../utils/events'; +import { + KEY_COMMAND, + handleCommandShowPreferences, + handleCommandSearching, + handleCommandSelectRoom, + handleCommandPreviousRoom, + handleCommandNextRoom, + handleCommandShowNewMessage, + handleCommandAddNewServer +} from '../../commands'; +import { MAX_SIDEBAR_WIDTH } from '../../constants/tablet'; +import { withSplit } from '../../split'; const SCROLL_OFFSET = 56; +const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12; +const CHATS_HEADER = 'Chats'; +const UNREAD_HEADER = 'Unread'; +const FAVORITES_HEADER = 'Favorites'; +const DISCUSSIONS_HEADER = 'Discussions'; +const CHANNELS_HEADER = 'Channels'; +const DM_HEADER = 'Direct_Messages'; +const GROUPS_HEADER = 'Private_Groups'; + +const filterIsUnread = s => (s.unread > 0 || s.alert) && !s.hideUnreadStatus; +const filterIsFavorite = s => s.f; const shouldUpdateProps = [ 'searchText', @@ -57,7 +84,9 @@ const shouldUpdateProps = [ 'showUnread', 'useRealName', 'StoreLastMessage', - 'appState' + 'appState', + 'theme', + 'split' ]; const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, @@ -67,7 +96,7 @@ const getItemLayout = (data, index) => ({ const keyExtractor = item => item.rid; class RoomsListView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const searching = navigation.getParam('searching'); const cancelSearchingAndroid = navigation.getParam( 'cancelSearchingAndroid' @@ -79,6 +108,7 @@ class RoomsListView extends React.Component { ); return { + ...themedHeader(screenProps.theme), headerLeft: searching ? ( <CustomHeaderButtons left> <Item @@ -134,11 +164,14 @@ class RoomsListView extends React.Component { useRealName: PropTypes.bool, StoreLastMessage: PropTypes.bool, appState: PropTypes.string, + theme: PropTypes.string, toggleSortDropdown: PropTypes.func, openSearchHeader: PropTypes.func, closeSearchHeader: PropTypes.func, appStart: PropTypes.func, - roomsRequest: PropTypes.func + roomsRequest: PropTypes.func, + closeServerDropdown: PropTypes.func, + split: PropTypes.bool }; constructor(props) { @@ -146,6 +179,7 @@ class RoomsListView extends React.Component { console.time(`${ this.constructor.name } init`); console.time(`${ this.constructor.name } mount`); + this.gotSubscriptions = false; const { width } = Dimensions.get('window'); this.state = { searching: false, @@ -153,16 +187,24 @@ class RoomsListView extends React.Component { loading: true, allChats: [], chats: [], - unread: [], - favorites: [], - discussions: [], - channels: [], - privateGroup: [], - direct: [], width }; + } + + componentDidMount() { + this.getSubscriptions(); + const { navigation, closeServerDropdown } = this.props; + navigation.setParams({ + onPressItem: this._onPressItem, + initSearchingAndroid: this.initSearchingAndroid, + cancelSearchingAndroid: this.cancelSearchingAndroid + }); + if (isTablet) { + EventEmitter.addEventListener(KEY_COMMAND, this.handleCommands); + } + Dimensions.addEventListener('change', this.onDimensionsChange); Orientation.unlockAllOrientations(); - this.willFocusListener = props.navigation.addListener('willFocus', () => { + this.willFocusListener = navigation.addListener('willFocus', () => { // Check if there were changes while not focused (it's set on sCU) if (this.shouldUpdate) { // animateNextTransition(); @@ -170,40 +212,32 @@ class RoomsListView extends React.Component { this.shouldUpdate = false; } }); - this.didFocusListener = props.navigation.addListener('didFocus', () => { - BackHandler.addEventListener( - 'hardwareBackPress', - this.handleBackPress - ); + this.didFocusListener = navigation.addListener('didFocus', () => { + this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); }); - this.willBlurListener = props.navigation.addListener('willBlur', () => BackHandler.addEventListener( - 'hardwareBackPress', - this.handleBackPress - )); - } - - componentDidMount() { - this.getSubscriptions(); - const { navigation } = this.props; - navigation.setParams({ - onPressItem: this._onPressItem, - initSearchingAndroid: this.initSearchingAndroid, - cancelSearchingAndroid: this.cancelSearchingAndroid + this.willBlurListener = navigation.addListener('willBlur', () => { + closeServerDropdown(); + if (this.backHandler && this.backHandler.remove) { + this.backHandler.remove(); + } }); - Dimensions.addEventListener('change', this.onDimensionsChange); console.timeEnd(`${ this.constructor.name } mount`); } componentWillReceiveProps(nextProps) { - const { loadingServer, searchText } = this.props; + const { loadingServer, searchText, server } = this.props; if (nextProps.server && loadingServer !== nextProps.loadingServer) { if (nextProps.loadingServer) { - this.internalSetState({ loading: true }); + this.setState({ loading: true }); } else { this.getSubscriptions(); } - } else if (searchText !== nextProps.searchText) { + } + if (server && server !== nextProps.server) { + this.gotSubscriptions = false; + } + if (searchText !== nextProps.searchText) { this.search(nextProps.searchText); } } @@ -273,7 +307,7 @@ class RoomsListView extends React.Component { && prevProps.showUnread === showUnread ) ) { - this.getSubscriptions(); + this.getSubscriptions(true); } else if ( appState === 'foreground' && appState !== prevProps.appState @@ -283,9 +317,6 @@ class RoomsListView extends React.Component { } componentWillUnmount() { - if (this.getSubscriptions && this.getSubscriptions.stop) { - this.getSubscriptions.stop(); - } if (this.querySubscription && this.querySubscription.unsubscribe) { this.querySubscription.unsubscribe(); } @@ -298,6 +329,9 @@ class RoomsListView extends React.Component { if (this.willBlurListener && this.willBlurListener.remove) { this.willBlurListener.remove(); } + if (isTablet) { + EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); + } Dimensions.removeEventListener('change', this.onDimensionsChange); console.countReset(`${ this.constructor.name }.render calls`); } @@ -313,11 +347,28 @@ class RoomsListView extends React.Component { this.setState(...args); }; - getSubscriptions = debounce(async() => { + addRoomsGroup = (data, header, allData) => { + if (data.length > 0) { + if (header) { + allData.push({ rid: header, separator: true }); + } + allData = allData.concat(data); + } + return allData; + } + + getSubscriptions = async(force = false) => { + if (this.gotSubscriptions && !force) { + return; + } + this.gotSubscriptions = true; + if (this.querySubscription && this.querySubscription.unsubscribe) { this.querySubscription.unsubscribe(); } + this.setState({ loading: true }); + const { sortBy, showUnread, @@ -336,13 +387,8 @@ class RoomsListView extends React.Component { .observeWithColumns(['room_updated_at', 'unread', 'alert', 'user_mentions', 'f', 't']); this.querySubscription = observable.subscribe((data) => { + let tempChats = []; let chats = []; - let unread = []; - let favorites = []; - let discussions = []; - let channels = []; - let privateGroup = []; - let direct = []; if (sortBy === 'alphabetical') { chats = orderBy(data, ['name'], ['asc']); } else { @@ -367,41 +413,41 @@ class RoomsListView extends React.Component { // unread if (showUnread) { - unread = chats.filter(s => s.unread > 0 || s.alert); - } else { - unread = []; + const unread = chats.filter(s => filterIsUnread(s)); + chats = chats.filter(s => !filterIsUnread(s)); + tempChats = this.addRoomsGroup(unread, UNREAD_HEADER, tempChats); } // favorites if (showFavorites) { - favorites = chats.filter(s => s.f); - } else { - favorites = []; + const favorites = chats.filter(s => filterIsFavorite(s)); + chats = chats.filter(s => !filterIsFavorite(s)); + tempChats = this.addRoomsGroup(favorites, FAVORITES_HEADER, tempChats); } // type if (groupByType) { - discussions = chats.filter(s => s.prid); - channels = chats.filter(s => s.t === 'c' && !s.prid); - privateGroup = chats.filter(s => s.t === 'p' && !s.prid); - direct = chats.filter(s => s.t === 'd' && !s.prid); - } else if (showUnread) { - chats = chats.filter(s => !s.unread && !s.alert); + const discussions = chats.filter(s => s.prid); + const channels = chats.filter(s => s.t === 'c' && !s.prid); + const privateGroup = chats.filter(s => s.t === 'p' && !s.prid); + const direct = chats.filter(s => s.t === 'd' && !s.prid); + tempChats = this.addRoomsGroup(discussions, DISCUSSIONS_HEADER, tempChats); + tempChats = this.addRoomsGroup(channels, CHANNELS_HEADER, tempChats); + tempChats = this.addRoomsGroup(privateGroup, GROUPS_HEADER, tempChats); + tempChats = this.addRoomsGroup(direct, DM_HEADER, tempChats); + } else if (showUnread || showFavorites) { + tempChats = this.addRoomsGroup(chats, CHATS_HEADER, tempChats); + } else { + tempChats = chats; } this.internalSetState({ + chats: tempChats, allChats, - chats, - unread, - favorites, - discussions, - channels, - privateGroup, - direct, loading: false }); }); - }, 300, true); + } initSearchingAndroid = () => { const { openSearchHeader, navigation } = this.props; @@ -417,8 +463,8 @@ class RoomsListView extends React.Component { navigation.setParams({ searching: false }); closeSearchHeader(); this.internalSetState({ search: [] }); - Keyboard.dismiss(); } + Keyboard.dismiss(); }; handleBackPress = () => { @@ -448,6 +494,7 @@ class RoomsListView extends React.Component { goRoom = (item) => { this.cancelSearchingAndroid(); const { navigation } = this.props; + this.item = item; navigation.navigate('RoomView', { rid: item.rid, name: this.getRoomTitle(item), @@ -564,6 +611,70 @@ class RoomsListView extends React.Component { navigation.navigate('DirectoryView'); }; + goRoomByIndex = (index) => { + const { chats } = this.state; + const filteredChats = chats.filter(c => !c.separator); + const room = filteredChats[index - 1]; + if (room) { + this.goRoom(room); + } + } + + findOtherRoom = (index, sign) => { + const { chats } = this.state; + const otherIndex = index + sign; + const otherRoom = chats[otherIndex]; + if (!otherRoom) { + return; + } + if (otherRoom.separator) { + return this.findOtherRoom(otherIndex, sign); + } else { + return otherRoom; + } + } + + // Go to previous or next room based on sign (-1 or 1) + // It's used by iPad key commands + goOtherRoom = (sign) => { + if (!this.item) { + return; + } + // Don't run during search + const { search } = this.state; + if (search.length > 0) { + return; + } + + const { chats } = this.state; + const index = chats.findIndex(c => c.rid === this.item.rid); + const otherRoom = this.findOtherRoom(index, sign); + if (otherRoom) { + this.goRoom(otherRoom); + } + } + + handleCommands = ({ event }) => { + const { navigation, server } = this.props; + const { input } = event; + if (handleCommandShowPreferences(event)) { + navigation.toggleDrawer(); + } else if (handleCommandSearching(event)) { + this.scroll.scrollToOffset({ animated: true, offset: 0 }); + this.inputRef.focus(); + } else if (handleCommandSelectRoom(event)) { + this.goRoomByIndex(input); + } else if (handleCommandPreviousRoom(event)) { + this.goOtherRoom(-1); + } else if (handleCommandNextRoom(event)) { + this.goOtherRoom(1); + } else if (handleCommandShowNewMessage(event)) { + navigation.navigate('NewMessageView', { onPressItem: this._onPressItem }); + } else if (handleCommandAddNewServer(event)) { + navigation.navigate('OnboardingView', { previousServer: server }); + } + }; + getScrollRef = ref => (this.scroll = ref); renderListHeader = () => { @@ -571,6 +682,7 @@ class RoomsListView extends React.Component { const { sortBy } = this.props; return ( <ListHeader + inputRef={(ref) => { this.inputRef = ref; }} searchLength={search.length} sortBy={sortBy} onChangeSearchText={this.search} @@ -587,20 +699,28 @@ class RoomsListView extends React.Component { }; renderItem = ({ item }) => { + if (item.separator) { + return this.renderSectionHeader(item.rid); + } + const { width } = this.state; const { userId, username, token, baseUrl, - StoreLastMessage + StoreLastMessage, + theme, + split } = this.props; const id = item.rid.replace(userId, '').trim(); return ( <RoomItem + theme={theme} alert={item.alert} unread={item.unread} + hideUnreadStatus={item.hideUnreadStatus} userMentions={item.userMentions} isRead={this.getIsRead(item)} favorite={item.f} @@ -620,7 +740,7 @@ class RoomsListView extends React.Component { showLastMessage={StoreLastMessage} onPress={() => this._onPressItem(item)} testID={`rooms-list-view-item-${ item.name }`} - width={width} + width={split ? MAX_SIDEBAR_WIDTH : width} toggleFav={this.toggleFav} toggleRead={this.toggleRead} hideChannel={this.hideChannel} @@ -628,135 +748,39 @@ class RoomsListView extends React.Component { ); }; - renderSectionHeader = header => ( - <View style={styles.groupTitleContainer}> - <Text style={styles.groupTitle}>{I18n.t(header)}</Text> - </View> - ); - - renderSection = (data, header) => { - const { showUnread, showFavorites, groupByType } = this.props; - - if (header === 'Unread' && !showUnread) { - return null; - } else if (header === 'Favorites' && !showFavorites) { - return null; - } else if ( - [ - 'Discussions', - 'Channels', - 'Direct_Messages', - 'Private_Groups' - ].includes(header) - && !groupByType - ) { - return null; - } else if (header === 'Chats' && groupByType) { - return null; - } - if (data && data.length > 0) { - return ( - <FlatList - data={data} - extraData={data} - keyExtractor={keyExtractor} - style={styles.list} - renderItem={this.renderItem} - ListHeaderComponent={() => this.renderSectionHeader(header)} - getItemLayout={getItemLayout} - enableEmptySections - removeClippedSubviews={isIOS} - keyboardShouldPersistTaps='always' - initialNumToRender={12} - windowSize={7} - /> - ); - } - return null; - }; - - renderList = () => { - const { - search, - chats, - unread, - favorites, - discussions, - channels, - direct, - privateGroup - } = this.state; - - if (search.length > 0) { - return ( - <FlatList - data={search} - extraData={search} - keyExtractor={keyExtractor} - style={styles.list} - renderItem={this.renderItem} - getItemLayout={getItemLayout} - enableEmptySections - removeClippedSubviews={isIOS} - keyboardShouldPersistTaps='always' - initialNumToRender={12} - windowSize={7} - /> - ); - } - + renderSectionHeader = (header) => { + const { theme } = this.props; return ( - <View style={styles.container}> - {this.renderSection(unread, 'Unread')} - {this.renderSection(favorites, 'Favorites')} - {this.renderSection(discussions, 'Discussions')} - {this.renderSection(channels, 'Channels')} - {this.renderSection(direct, 'Direct_Messages')} - {this.renderSection(privateGroup, 'Private_Groups')} - {this.renderSection(chats, 'Chats')} + <View style={[styles.groupTitleContainer, { backgroundColor: themes[theme].backgroundColor }]}> + <Text style={[styles.groupTitle, { color: themes[theme].controlText }]}>{I18n.t(header)}</Text> </View> ); }; renderScroll = () => { - const { loading } = this.state; + const { loading, chats, search } = this.state; + const { theme } = this.props; if (loading) { - return <ActivityIndicator style={styles.loading} />; - } - - const { showUnread, showFavorites, groupByType } = this.props; - if (!(showUnread || showFavorites || groupByType)) { - const { chats, search } = this.state; - return ( - <FlatList - ref={this.getScrollRef} - data={search.length ? search : chats} - extraData={search.length ? search : chats} - contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}} - keyExtractor={keyExtractor} - style={styles.list} - renderItem={this.renderItem} - ListHeaderComponent={this.renderListHeader} - getItemLayout={getItemLayout} - removeClippedSubviews={isIOS} - keyboardShouldPersistTaps='always' - initialNumToRender={9} - windowSize={9} - /> - ); + return <ActivityIndicator theme={theme} />; } return ( - <ScrollView + <FlatList ref={this.getScrollRef} + data={search.length ? search : chats} + extraData={search.length ? search : chats} contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}} + keyExtractor={keyExtractor} + style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} + renderItem={this.renderItem} + ListHeaderComponent={this.renderListHeader} + getItemLayout={getItemLayout} + removeClippedSubviews={isIOS} keyboardShouldPersistTaps='always' - testID='rooms-list-view-list' - > - {this.renderListHeader()} - {this.renderList()} - </ScrollView> + initialNumToRender={INITIAL_NUM_TO_RENDER} + windowSize={9} + /> ); }; @@ -768,16 +792,17 @@ class RoomsListView extends React.Component { showFavorites, showUnread, showServerDropdown, - showSortDropdown + showSortDropdown, + theme } = this.props; return ( <SafeAreaView - style={styles.container} + style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='rooms-list-view' forceInset={{ vertical: 'never' }} > - <StatusBar /> + <StatusBar theme={theme} /> {this.renderScroll()} {showSortDropdown ? ( <SortDropdown @@ -819,7 +844,8 @@ const mapDispatchToProps = dispatch => ({ closeSearchHeader: () => dispatch(closeSearchHeaderAction()), appStart: () => dispatch(appStartAction()), roomsRequest: () => dispatch(roomsRequestAction()), - selectServerRequest: server => dispatch(selectServerRequestAction(server)) + selectServerRequest: server => dispatch(selectServerRequestAction(server)), + closeServerDropdown: () => dispatch(closeServerDropdownAction()) }); -export default connect(mapStateToProps, mapDispatchToProps)(RoomsListView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(withSplit(RoomsListView))); diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js index 0c19c11ee..9deb03dc1 100644 --- a/app/views/RoomsListView/styles.js +++ b/app/views/RoomsListView/styles.js @@ -1,34 +1,18 @@ import { StyleSheet } from 'react-native'; -import { isIOS } from '../../utils/deviceInfo'; -import { - COLOR_SEPARATOR, COLOR_TEXT, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION -} from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: isIOS ? COLOR_WHITE : '#E1E5E8' + flex: 1 }, list: { - width: '100%', - backgroundColor: COLOR_WHITE - }, - actionButtonIcon: { - fontSize: 20, - height: 22, - color: 'white' - }, - loading: { - flex: 1 + width: '100%' }, dropdownContainerHeader: { height: 41, borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: COLOR_SEPARATOR, alignItems: 'center', - backgroundColor: isIOS ? COLOR_WHITE : '#54585E', flexDirection: 'row' }, sortToggleContainerClose: { @@ -37,17 +21,16 @@ export default StyleSheet.create({ width: '100%' }, sortToggleText: { - fontSize: 15, + fontSize: 16, flex: 1, marginLeft: 15, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, dropdownContainer: { - backgroundColor: COLOR_WHITE, width: '100%', position: 'absolute', - top: 0 + top: 0, + borderBottomWidth: StyleSheet.hairlineWidth }, sortItemButton: { height: 57, @@ -60,51 +43,43 @@ export default StyleSheet.create({ sortItemText: { fontSize: 18, flex: 1, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, backdrop: { - ...StyleSheet.absoluteFill, - backgroundColor: '#000000' + ...StyleSheet.absoluteFill }, sortSeparator: { height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR, marginHorizontal: 15, flex: 1 }, sortIcon: { width: 22, height: 22, - marginHorizontal: 15, - ...sharedStyles.textColorDescription + marginHorizontal: 15 }, groupTitleContainer: { paddingHorizontal: 15, paddingTop: 17, - paddingBottom: 10, - backgroundColor: isIOS ? COLOR_WHITE : '#9ea2a8' + paddingBottom: 10 }, groupTitle: { - color: isIOS ? COLOR_TEXT : '#54585E', - fontSize: isIOS ? 22 : 15, + fontSize: 16, letterSpacing: 0.27, flex: 1, - lineHeight: isIOS ? 41 : 24, + lineHeight: 24, ...sharedStyles.textBold }, serverHeader: { justifyContent: 'space-between' }, serverHeaderText: { - fontSize: 15, + fontSize: 16, marginLeft: 15, - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, serverHeaderAdd: { - color: isIOS ? COLOR_PRIMARY : COLOR_WHITE, - fontSize: 15, + fontSize: 16, marginRight: 15, paddingVertical: 10, ...sharedStyles.textRegular @@ -114,7 +89,8 @@ export default StyleSheet.create({ }, serverItemContainer: { flexDirection: 'row', - alignItems: 'center' + alignItems: 'center', + height: 68 }, serverIcon: { width: 42, @@ -131,33 +107,24 @@ export default StyleSheet.create({ }, serverName: { fontSize: 18, - ...sharedStyles.textColorNormal, ...sharedStyles.textSemibold }, serverUrl: { - fontSize: 15, - ...sharedStyles.textColorDescription, + fontSize: 16, ...sharedStyles.textRegular }, - checkIcon: { - marginHorizontal: 15, - color: COLOR_PRIMARY - }, serverSeparator: { height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR, marginLeft: 72 }, directoryIcon: { width: 22, height: 22, - marginHorizontal: 15, - color: isIOS ? COLOR_PRIMARY : COLOR_TEXT_DESCRIPTION + marginHorizontal: 15 }, directoryText: { - fontSize: 15, + fontSize: 16, flex: 1, - color: isIOS ? COLOR_PRIMARY : COLOR_TEXT_DESCRIPTION, ...sharedStyles.textRegular } }); diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index 5d7f33d0f..1a50e3466 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -6,7 +6,7 @@ import { SafeAreaView } from 'react-navigation'; import equal from 'deep-equal'; import RCTextInput from '../../containers/TextInput'; -import RCActivityIndicator from '../../containers/ActivityIndicator'; +import ActivityIndicator from '../../containers/ActivityIndicator'; import styles from './styles'; import Markdown from '../../containers/markdown'; import debounce from '../../utils/debounce'; @@ -16,17 +16,22 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps'; import I18n from '../../i18n'; import StatusBar from '../../containers/StatusBar'; import log from '../../utils/log'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; class SearchMessagesView extends React.Component { - static navigationOptions = { - title: I18n.t('Search') - } + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Search'), + ...themedHeader(screenProps.theme) + }) static propTypes = { navigation: PropTypes.object, user: PropTypes.object, baseUrl: PropTypes.string, - customEmojis: PropTypes.object + customEmojis: PropTypes.object, + theme: PropTypes.string } constructor(props) { @@ -41,6 +46,10 @@ class SearchMessagesView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { loading, searchText, messages } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.loading !== loading) { return true; } @@ -84,14 +93,17 @@ class SearchMessagesView extends React.Component { return null; } - renderEmpty = () => ( - <View style={styles.listEmptyContainer}> - <Text style={styles.noDataFound}>{I18n.t('No_results_found')}</Text> - </View> - ) + renderEmpty = () => { + const { theme } = this.props; + return ( + <View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].backgroundColor }]}> + <Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text> + </View> + ); + } renderItem = ({ item }) => { - const { user, baseUrl } = this.props; + const { user, baseUrl, theme } = this.props; return ( <Message baseUrl={baseUrl} @@ -105,12 +117,14 @@ class SearchMessagesView extends React.Component { isHeader onOpenFileModal={() => {}} getCustomEmoji={this.getCustomEmoji} + theme={theme} /> ); } renderList = () => { const { messages, loading, searchText } = this.state; + const { theme } = this.props; if (!loading && messages.length === 0 && searchText.length) { return this.renderEmpty(); @@ -120,19 +134,20 @@ class SearchMessagesView extends React.Component { <FlatList data={messages} renderItem={this.renderItem} - style={styles.list} + style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} keyExtractor={item => item._id} onEndReached={this.load} - ListFooterComponent={loading ? <RCActivityIndicator /> : null} + ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null} {...scrollPersistTaps} /> ); } render() { + const { theme } = this.props; return ( - <SafeAreaView style={styles.container} testID='search-messages-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='search-messages-view' forceInset={{ vertical: 'never' }}> + <StatusBar theme={theme} /> <View style={styles.searchContainer}> <RCTextInput autoFocus @@ -140,9 +155,10 @@ class SearchMessagesView extends React.Component { onChangeText={this.search} placeholder={I18n.t('Search_Messages')} testID='search-message-view-input' + theme={theme} /> - <Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' /> - <View style={styles.divider} /> + <Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' theme={theme} /> + <View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} /> </View> {this.renderList()} </SafeAreaView> @@ -160,4 +176,4 @@ const mapStateToProps = state => ({ customEmojis: state.customEmojis }); -export default connect(mapStateToProps)(SearchMessagesView); +export default connect(mapStateToProps)(withTheme(SearchMessagesView)); diff --git a/app/views/SearchMessagesView/styles.js b/app/views/SearchMessagesView/styles.js index 620ec0af1..7c5cdd82c 100644 --- a/app/views/SearchMessagesView/styles.js +++ b/app/views/SearchMessagesView/styles.js @@ -1,36 +1,30 @@ import { StyleSheet } from 'react-native'; -import { COLOR_SEPARATOR, COLOR_WHITE } from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, searchContainer: { padding: 20, paddingBottom: 0 }, list: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, divider: { width: '100%', height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR, marginVertical: 20 }, listEmptyContainer: { flex: 1, alignItems: 'center', - justifyContent: 'flex-start', - backgroundColor: COLOR_WHITE + justifyContent: 'flex-start' }, noDataFound: { fontSize: 14, - ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal + ...sharedStyles.textRegular } }); diff --git a/app/views/SelectServerView.js b/app/views/SelectServerView.js index 29d59b1b6..201642905 100644 --- a/app/views/SelectServerView.js +++ b/app/views/SelectServerView.js @@ -8,19 +8,20 @@ import { SafeAreaView } from 'react-navigation'; import I18n from '../i18n'; import StatusBar from '../containers/StatusBar'; -import { COLOR_BACKGROUND_CONTAINER } from '../constants/colors'; +import { themes } from '../constants/colors'; import Navigation from '../lib/ShareNavigation'; import ServerItem, { ROW_HEIGHT } from '../presentation/ServerItem'; import sharedStyles from './Styles'; import RocketChat from '../lib/rocketchat'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const keyExtractor = item => item.id; const styles = StyleSheet.create({ container: { - flex: 1, - backgroundColor: COLOR_BACKGROUND_CONTAINER + flex: 1 }, list: { marginVertical: 32, @@ -33,13 +34,15 @@ const styles = StyleSheet.create({ }); class SelectServerView extends React.Component { - static navigationOptions = () => ({ + static navigationOptions = ({ screenProps }) => ({ + ...themedHeader(screenProps.theme), title: I18n.t('Select_Server') }) static propTypes = { server: PropTypes.string, - navigation: PropTypes.object + navigation: PropTypes.object, + theme: PropTypes.string } constructor(props) { @@ -64,33 +67,39 @@ class SelectServerView extends React.Component { } renderItem = ({ item }) => { - const { server } = this.props; + const { server, theme } = this.props; return ( <ServerItem server={server} onPress={() => this.select(item.id)} item={item} hasCheck + theme={theme} /> ); } - renderSeparator = () => <View style={styles.separator} />; + renderSeparator = () => { + const { theme } = this.props; + return <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />; + } render() { const { servers } = this.state; + const { theme } = this.props; return ( <SafeAreaView - style={styles.container} + style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} forceInset={{ vertical: 'never' }} > - <StatusBar /> - <View style={styles.list}> + <StatusBar theme={theme} /> + <View style={[styles.list, { borderColor: themes[theme].separatorColor }]}> <FlatList data={servers} keyExtractor={keyExtractor} renderItem={this.renderItem} getItemLayout={getItemLayout} + contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} ItemSeparatorComponent={this.renderSeparator} enableEmptySections removeClippedSubviews @@ -108,4 +117,4 @@ const mapStateToProps = (({ share }) => ({ server: share.server })); -export default connect(mapStateToProps)(SelectServerView); +export default connect(mapStateToProps)(withTheme(SelectServerView)); diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js index b9209f7d7..82055075d 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.js @@ -17,21 +17,18 @@ import Loading from '../containers/Loading'; import debounce from '../utils/debounce'; import I18n from '../i18n'; import log from '../utils/log'; -import { isIOS } from '../utils/deviceInfo'; import SearchBox from '../containers/SearchBox'; import sharedStyles from './Styles'; import { Item, CustomHeaderButtons } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; -import { COLOR_WHITE } from '../constants/colors'; +import { themes } from '../constants/colors'; import { animateNextTransition } from '../utils/layoutAnimation'; +import { withTheme } from '../theme'; +import { themedHeader } from '../utils/navigation'; const styles = StyleSheet.create({ safeAreaView: { - flex: 1, - backgroundColor: isIOS ? '#F7F8FA' : '#E1E5E8' - }, - header: { - backgroundColor: COLOR_WHITE + flex: 1 }, separator: { marginLeft: 60 @@ -39,10 +36,11 @@ const styles = StyleSheet.create({ }); class SelectedUsersView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const title = navigation.getParam('title'); const nextAction = navigation.getParam('nextAction', () => {}); return { + ...themedHeader(screenProps.theme), title, headerRight: ( <CustomHeaderButtons> @@ -64,7 +62,8 @@ class SelectedUsersView extends React.Component { user: PropTypes.shape({ id: PropTypes.string, token: PropTypes.string - }) + }), + theme: PropTypes.string }; constructor(props) { @@ -83,7 +82,10 @@ class SelectedUsersView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { search, chats } = this.state; - const { users, loading } = this.props; + const { users, loading, theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextProps.loading !== loading) { return true; } @@ -186,15 +188,18 @@ class SelectedUsersView extends React.Component { _onPressSelectedItem = item => this.toggleUser(item); - renderHeader = () => ( - <View style={styles.header}> - <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='select-users-view-search' /> - {this.renderSelected()} - </View> - ) + renderHeader = () => { + const { theme } = this.props; + return ( + <View style={{ backgroundColor: themes[theme].backgroundColor }}> + <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='select-users-view-search' /> + {this.renderSelected()} + </View> + ); + } renderSelected = () => { - const { users } = this.props; + const { users, theme } = this.props; if (users.length === 0) { return null; @@ -203,7 +208,7 @@ class SelectedUsersView extends React.Component { <FlatList data={users} keyExtractor={item => item._id} - style={[styles.list, sharedStyles.separatorTop]} + style={[sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]} contentContainerStyle={{ marginVertical: 5 }} renderItem={this.renderSelectedItem} enableEmptySections @@ -214,7 +219,7 @@ class SelectedUsersView extends React.Component { } renderSelectedItem = ({ item }) => { - const { baseUrl, user } = this.props; + const { baseUrl, user, theme } = this.props; return ( <UserItem name={item.fname} @@ -224,21 +229,25 @@ class SelectedUsersView extends React.Component { baseUrl={baseUrl} style={{ paddingRight: 15 }} user={user} + theme={theme} /> ); } - renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} /> + renderSeparator = () => { + const { theme } = this.props; + return <View style={[sharedStyles.separator, styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; + } renderItem = ({ item, index }) => { const { search, chats } = this.state; - const { baseUrl, user } = this.props; + const { baseUrl, user, theme } = this.props; const name = item.search ? item.name : item.fname; const username = item.search ? item.username : item.name; - let style = {}; + let style = { borderColor: themes[theme].separatorColor }; if (index === 0) { - style = { ...sharedStyles.separatorTop }; + style = { ...style, ...sharedStyles.separatorTop }; } if (search.length > 0 && index === search.length - 1) { style = { ...style, ...sharedStyles.separatorBottom }; @@ -256,12 +265,14 @@ class SelectedUsersView extends React.Component { baseUrl={baseUrl} style={style} user={user} + theme={theme} /> ); } renderList = () => { const { search, chats } = this.state; + const { theme } = this.props; return ( <FlatList data={search.length > 0 ? search : chats} @@ -270,6 +281,7 @@ class SelectedUsersView extends React.Component { renderItem={this.renderItem} ItemSeparatorComponent={this.renderSeparator} ListHeaderComponent={this.renderHeader} + contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} enableEmptySections keyboardShouldPersistTaps='always' /> @@ -277,10 +289,14 @@ class SelectedUsersView extends React.Component { } render = () => { - const { loading } = this.props; + const { loading, theme } = this.props; return ( - <SafeAreaView style={styles.safeAreaView} testID='select-users-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView + style={[styles.safeAreaView, { backgroundColor: themes[theme].auxiliaryBackground }]} + forceInset={{ vertical: 'never' }} + testID='select-users-view' + > + <StatusBar theme={theme} /> {this.renderList()} <Loading visible={loading} /> </SafeAreaView> @@ -305,4 +321,4 @@ const mapDispatchToProps = dispatch => ({ setLoadingInvite: loading => dispatch(setLoadingAction(loading)) }); -export default connect(mapStateToProps, mapDispatchToProps)(SelectedUsersView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SelectedUsersView)); diff --git a/app/views/SetUsernameView.js b/app/views/SetUsernameView.js index f7122f466..4c3c37abb 100644 --- a/app/views/SetUsernameView.js +++ b/app/views/SetUsernameView.js @@ -17,6 +17,10 @@ import I18n from '../i18n'; import RocketChat from '../lib/rocketchat'; import StatusBar from '../containers/StatusBar'; import log from '../utils/log'; +import { themedHeader } from '../utils/navigation'; +import { withTheme } from '../theme'; +import { themes } from '../constants/colors'; +import { isTablet } from '../utils/deviceInfo'; const styles = StyleSheet.create({ loginTitle: { @@ -26,9 +30,10 @@ const styles = StyleSheet.create({ }); class SetUsernameView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const title = navigation.getParam('title'); return { + ...themedHeader(screenProps.theme), title }; } @@ -38,7 +43,8 @@ class SetUsernameView extends React.Component { server: PropTypes.string, userId: PropTypes.string, loginRequest: PropTypes.func, - token: PropTypes.string + token: PropTypes.string, + theme: PropTypes.string } constructor(props) { @@ -49,7 +55,9 @@ class SetUsernameView extends React.Component { }; const { server } = this.props; props.navigation.setParams({ title: server }); - Orientation.lockToPortrait(); + if (!isTablet) { + Orientation.lockToPortrait(); + } } async componentDidMount() { @@ -61,6 +69,10 @@ class SetUsernameView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { username, saving } = this.state; + const { theme } = this.props; + if (nextProps.theme !== theme) { + return true; + } if (nextState.username !== username) { return true; } @@ -90,13 +102,34 @@ class SetUsernameView extends React.Component { render() { const { username, saving } = this.state; + const { theme } = this.props; return ( - <KeyboardView contentContainerStyle={sharedStyles.container}> - <StatusBar /> + <KeyboardView + style={{ backgroundColor: themes[theme].auxiliaryBackground }} + contentContainerStyle={sharedStyles.container} + > + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} contentContainerStyle={sharedStyles.containerScrollView}> <SafeAreaView style={sharedStyles.container} testID='set-username-view' forceInset={{ vertical: 'never' }}> - <Text style={[sharedStyles.loginTitle, sharedStyles.textBold, styles.loginTitle]}>{I18n.t('Username')}</Text> - <Text style={[sharedStyles.loginSubtitle, sharedStyles.textRegular]}>{I18n.t('Set_username_subtitle')}</Text> + <Text + style={[ + sharedStyles.loginTitle, + sharedStyles.textBold, + styles.loginTitle, + { color: themes[theme].titleText } + ]} + > + {I18n.t('Username')} + </Text> + <Text + style={[ + sharedStyles.loginSubtitle, + sharedStyles.textRegular, + { color: themes[theme].titleText } + ]} + > + {I18n.t('Set_username_subtitle')} + </Text> <TextInput autoFocus placeholder={I18n.t('Username')} @@ -108,6 +141,7 @@ class SetUsernameView extends React.Component { testID='set-username-view-input' clearButtonMode='while-editing' containerStyle={sharedStyles.inputLastChild} + theme={theme} /> <Button title={I18n.t('Register')} @@ -116,6 +150,7 @@ class SetUsernameView extends React.Component { testID='set-username-view-submit' disabled={!username} loading={saving} + theme={theme} /> </SafeAreaView> </ScrollView> @@ -133,4 +168,4 @@ const mapDispatchToProps = dispatch => ({ loginRequest: params => dispatch(loginRequestAction(params)) }); -export default connect(mapStateToProps, mapDispatchToProps)(SetUsernameView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SetUsernameView)); diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js index a21fb545e..b4cf64ee6 100644 --- a/app/views/SettingsView/index.js +++ b/app/views/SettingsView/index.js @@ -5,17 +5,20 @@ import { import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { logout as logoutAction } from '../../actions/login'; import { toggleMarkdown as toggleMarkdownAction } from '../../actions/markdown'; import { toggleCrashReport as toggleCrashReportAction } from '../../actions/crashReport'; -import { SWITCH_TRACK_COLOR } from '../../constants/colors'; -import { DrawerButton } from '../../containers/HeaderButton'; +import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; +import { DrawerButton, CloseModalButton } from '../../containers/HeaderButton'; import StatusBar from '../../containers/StatusBar'; import ListItem from '../../containers/ListItem'; import { DisclosureImage } from '../../containers/DisclosureIndicator'; import Separator from '../../containers/Separator'; import I18n from '../../i18n'; import { MARKDOWN_KEY, CRASH_REPORT_KEY } from '../../lib/rocketchat'; -import { getReadableVersion, getDeviceModel, isAndroid } from '../../utils/deviceInfo'; +import { + getReadableVersion, getDeviceModel, isAndroid +} from '../../utils/deviceInfo'; import openLink from '../../utils/openLink'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; import { showErrorAlert } from '../../utils/info'; @@ -23,20 +26,45 @@ import styles from './styles'; import sharedStyles from '../Styles'; import { loggerConfig, analytics } from '../../utils/log'; import { PLAY_MARKET_LINK, APP_STORE_LINK, LICENSE_LINK } from '../../constants/links'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; +import SidebarView from '../SidebarView'; +import { withSplit } from '../../split'; +import Navigation from '../../lib/Navigation'; -const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />); -const ItemInfo = React.memo(({ info }) => ( - <View style={styles.infoContainer}> - <Text style={styles.infoText}>{info}</Text> +const SectionSeparator = React.memo(({ theme }) => ( + <View + style={[ + styles.sectionSeparatorBorder, + { + borderColor: themes[theme].separatorColor, + backgroundColor: themes[theme].auxiliaryBackground + } + ]} + /> +)); +SectionSeparator.propTypes = { + theme: PropTypes.string +}; + +const ItemInfo = React.memo(({ info, theme }) => ( + <View style={[styles.infoContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}> + <Text style={[styles.infoText, { color: themes[theme].infoText }]}>{info}</Text> </View> )); ItemInfo.propTypes = { - info: PropTypes.string + info: PropTypes.string, + theme: PropTypes.string }; class SettingsView extends React.Component { - static navigationOptions = ({ navigation }) => ({ - headerLeft: <DrawerButton navigation={navigation} />, + static navigationOptions = ({ navigation, screenProps }) => ({ + ...themedHeader(screenProps.theme), + headerLeft: screenProps.split ? ( + <CloseModalButton navigation={navigation} testID='settings-view-close' /> + ) : ( + <DrawerButton navigation={navigation} /> + ), title: I18n.t('Settings') }); @@ -46,7 +74,18 @@ class SettingsView extends React.Component { useMarkdown: PropTypes.bool, allowCrashReport: PropTypes.bool, toggleMarkdown: PropTypes.func, - toggleCrashReport: PropTypes.func + toggleCrashReport: PropTypes.func, + theme: PropTypes.string, + split: PropTypes.bool, + logout: PropTypes.func.isRequired + } + + logout = () => { + const { logout, split } = this.props; + if (split) { + Navigation.navigate('RoomView'); + } + logout(); } toggleMarkdown = (value) => { @@ -92,9 +131,39 @@ class SettingsView extends React.Component { Share.share({ message: isAndroid ? PLAY_MARKET_LINK : APP_STORE_LINK }); } - onPressLicense = () => openLink(LICENSE_LINK) + changeTheme = () => { + const { navigation } = this.props; + navigation.navigate('ThemeView'); + } - renderDisclosure = () => <DisclosureImage /> + onPressLicense = () => { + const { theme } = this.props; + openLink(LICENSE_LINK, theme); + } + + renderDisclosure = () => { + const { theme } = this.props; + return <DisclosureImage theme={theme} />; + } + + renderLogout = () => { + const { theme } = this.props; + return ( + <> + <Separator theme={theme} /> + <ListItem + title={I18n.t('Logout')} + testID='settings-logout' + onPress={this.logout} + right={this.renderDisclosure} + color={themes[theme].dangerColor} + theme={theme} + /> + <Separator theme={theme} /> + <ItemInfo theme={theme} /> + </> + ); + } renderMarkdownSwitch = () => { const { useMarkdown } = this.props; @@ -119,49 +188,77 @@ class SettingsView extends React.Component { } render() { - const { server } = this.props; + const { server, split, theme } = this.props; return ( - <SafeAreaView style={sharedStyles.listSafeArea} testID='settings-view'> - <StatusBar /> + <SafeAreaView + style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} + testID='settings-view' + > + <StatusBar theme={theme} /> <ScrollView {...scrollPersistTaps} - contentContainerStyle={[sharedStyles.listContentContainer, styles.listWithoutBorderBottom]} + contentContainerStyle={[ + sharedStyles.listContentContainer, + styles.listWithoutBorderBottom, + { borderColor: themes[theme].separatorColor } + ]} showsVerticalScrollIndicator={false} testID='settings-view-list' > + {split ? ( + <> + <SidebarView theme={theme} /> + <SectionSeparator theme={theme} /> + <ListItem + title={I18n.t('Profile')} + onPress={() => this.navigateToRoom('ProfileView')} + showActionIndicator + testID='settings-profile' + right={this.renderDisclosure} + theme={theme} + /> + <Separator theme={theme} /> + </> + ) : null} + <ListItem title={I18n.t('Contact_us')} onPress={this.sendEmail} showActionIndicator testID='settings-view-contact' right={this.renderDisclosure} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> <ListItem title={I18n.t('Language')} onPress={() => this.navigateToRoom('LanguageView')} showActionIndicator testID='settings-view-language' right={this.renderDisclosure} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> <ListItem title={I18n.t('Share_this_app')} showActionIndicator onPress={this.shareApp} testID='settings-view-share-app' right={this.renderDisclosure} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> <ListItem title={I18n.t('Theme')} showActionIndicator - disabled + onPress={this.changeTheme} testID='settings-view-theme' + right={this.renderDisclosure} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> - <SectionSeparator /> + <SectionSeparator theme={theme} /> <ListItem title={I18n.t('License')} @@ -169,35 +266,42 @@ class SettingsView extends React.Component { showActionIndicator testID='settings-view-license' right={this.renderDisclosure} + theme={theme} /> - <Separator /> - <ListItem title={I18n.t('Version_no', { version: getReadableVersion })} testID='settings-view-version' /> - <Separator /> + <Separator theme={theme} /> + <ListItem title={I18n.t('Version_no', { version: getReadableVersion })} testID='settings-view-version' theme={theme} /> + <Separator theme={theme} /> <ListItem title={I18n.t('Server_version', { version: server.version })} subtitle={`${ server.server.split('//')[1] }`} testID='settings-view-server-version' + theme={theme} /> - <SectionSeparator /> + <SectionSeparator theme={theme} /> <ListItem title={I18n.t('Enable_markdown')} testID='settings-view-markdown' right={() => this.renderMarkdownSwitch()} + theme={theme} /> - <SectionSeparator /> + <SectionSeparator theme={theme} /> <ListItem title={I18n.t('Send_crash_report')} testID='settings-view-crash-report' right={() => this.renderCrashReportSwitch()} + theme={theme} /> - <Separator /> + <Separator theme={theme} /> <ItemInfo info={I18n.t('Crash_report_disclaimer')} + theme={theme} /> + + { split ? this.renderLogout() : null } </ScrollView> </SafeAreaView> ); @@ -211,8 +315,9 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ + logout: () => dispatch(logoutAction()), toggleMarkdown: params => dispatch(toggleMarkdownAction(params)), toggleCrashReport: params => dispatch(toggleCrashReportAction(params)) }); -export default connect(mapStateToProps, mapDispatchToProps)(SettingsView); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(withSplit(SettingsView))); diff --git a/app/views/SettingsView/styles.js b/app/views/SettingsView/styles.js index 864d5c5d8..64c393fe6 100644 --- a/app/views/SettingsView/styles.js +++ b/app/views/SettingsView/styles.js @@ -1,25 +1,21 @@ import { StyleSheet } from 'react-native'; -import { COLOR_BACKGROUND_CONTAINER } from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ sectionSeparatorBorder: { ...sharedStyles.separatorVertical, - backgroundColor: COLOR_BACKGROUND_CONTAINER, - height: 10 + height: 36 }, listWithoutBorderBottom: { borderBottomWidth: 0 }, infoContainer: { padding: 15, - paddingBottom: 40, - backgroundColor: COLOR_BACKGROUND_CONTAINER + marginBottom: 40 }, infoText: { fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular } }); diff --git a/app/views/ShareListView/Header/Header.android.js b/app/views/ShareListView/Header/Header.android.js index 55720d920..a7fcd0351 100644 --- a/app/views/ShareListView/Header/Header.android.js +++ b/app/views/ShareListView/Header/Header.android.js @@ -1,11 +1,10 @@ import React from 'react'; -import { - View, StyleSheet, Text, TextInput -} from 'react-native'; +import { View, StyleSheet, Text } from 'react-native'; import PropTypes from 'prop-types'; +import TextInput from '../../../presentation/TextInput'; import I18n from '../../../i18n'; -import { COLOR_WHITE, HEADER_TITLE } from '../../../constants/colors'; +import { themes } from '../../../constants/colors'; import sharedStyles from '../../Styles'; const styles = StyleSheet.create({ @@ -15,38 +14,39 @@ const styles = StyleSheet.create({ }, search: { fontSize: 20, - color: COLOR_WHITE, ...sharedStyles.textRegular, marginHorizontal: 14 }, title: { fontSize: 20, ...sharedStyles.textBold, - color: HEADER_TITLE, marginHorizontal: 16 } }); -const Header = React.memo(({ searching, onChangeSearchText }) => { +const Header = React.memo(({ searching, onChangeSearchText, theme }) => { + const titleColorStyle = { color: themes[theme].headerTintColor }; + const isLight = theme === 'light'; if (searching) { return ( <View style={styles.container}> <TextInput - style={styles.search} + style={[styles.search, isLight && titleColorStyle]} placeholder={I18n.t('Search')} - placeholderTextColor='rgba(255, 255, 255, 0.5)' onChangeText={onChangeSearchText} + theme={theme} autoFocus /> </View> ); } - return <Text style={styles.title}>{I18n.t('Send_to')}</Text>; + return <Text style={[styles.title, titleColorStyle]}>{I18n.t('Send_to')}</Text>; }); Header.propTypes = { searching: PropTypes.bool, - onChangeSearchText: PropTypes.func + onChangeSearchText: PropTypes.func, + theme: PropTypes.string }; export default Header; diff --git a/app/views/ShareListView/Header/Header.ios.js b/app/views/ShareListView/Header/Header.ios.js index d67348818..c53947914 100644 --- a/app/views/ShareListView/Header/Header.ios.js +++ b/app/views/ShareListView/Header/Header.ios.js @@ -5,21 +5,20 @@ import ShareExtension from 'rn-extensions-share'; import SearchBox from '../../../containers/SearchBox'; import { CloseShareExtensionButton } from '../../../containers/HeaderButton'; -import { HEADER_BACKGROUND } from '../../../constants/colors'; +import { themes } from '../../../constants/colors'; import sharedStyles from '../../Styles'; import { animateNextTransition } from '../../../utils/layoutAnimation'; const styles = StyleSheet.create({ container: { - backgroundColor: HEADER_BACKGROUND, flexDirection: 'row', ...sharedStyles.separatorBottom } }); const Header = React.memo(({ - searching, onChangeSearchText, initSearch, cancelSearch + searching, onChangeSearchText, initSearch, cancelSearch, theme }) => { const [text, setText] = useState(''); @@ -41,7 +40,15 @@ const Header = React.memo(({ }; return ( - <View style={styles.container}> + <View + style={[ + styles.container, + { + borderColor: themes[theme].separatorColor, + backgroundColor: themes[theme].headerBackground + } + ]} + > { !searching ? ( @@ -69,7 +76,8 @@ Header.propTypes = { searching: PropTypes.bool, onChangeSearchText: PropTypes.func, initSearch: PropTypes.func, - cancelSearch: PropTypes.func + cancelSearch: PropTypes.func, + theme: PropTypes.string }; export default Header; diff --git a/app/views/ShareListView/Header/index.js b/app/views/ShareListView/Header/index.js index 3cca590fa..eff18acda 100644 --- a/app/views/ShareListView/Header/index.js +++ b/app/views/ShareListView/Header/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import Header from './Header'; const ShareListHeader = React.memo(({ - searching, initSearch, cancelSearch, search + searching, initSearch, cancelSearch, search, theme }) => { const onSearchChangeText = (text) => { search(text.trim()); @@ -12,6 +12,7 @@ const ShareListHeader = React.memo(({ return ( <Header + theme={theme} searching={searching} initSearch={initSearch} cancelSearch={cancelSearch} @@ -24,7 +25,8 @@ ShareListHeader.propTypes = { searching: PropTypes.bool, initSearch: PropTypes.func, cancelSearch: PropTypes.func, - search: PropTypes.func + search: PropTypes.func, + theme: PropTypes.string }; export default ShareListHeader; diff --git a/app/views/ShareListView/index.js b/app/views/ShareListView/index.js index f419ffc60..ee500231c 100644 --- a/app/views/ShareListView/index.js +++ b/app/views/ShareListView/index.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - View, Text, FlatList, ActivityIndicator, Keyboard, BackHandler + View, Text, FlatList, Keyboard, BackHandler } from 'react-native'; import { SafeAreaView } from 'react-navigation'; import ShareExtension from 'rn-extensions-share'; @@ -22,17 +22,21 @@ import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem'; import ServerItem from '../../presentation/ServerItem'; import { CloseShareExtensionButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import ShareListHeader from './Header'; +import ActivityIndicator from '../../containers/ActivityIndicator'; import styles from './styles'; import StatusBar from '../../containers/StatusBar'; +import { themes } from '../../constants/colors'; import { animateNextTransition } from '../../utils/layoutAnimation'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; const LIMIT = 50; const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const keyExtractor = item => item.rid; class ShareListView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const searching = navigation.getParam('searching'); const initSearch = navigation.getParam('initSearch', () => {}); const cancelSearch = navigation.getParam('cancelSearch', () => {}); @@ -40,19 +44,21 @@ class ShareListView extends React.Component { if (isIOS) { return { + headerStyle: { backgroundColor: themes[screenProps.theme].headerBackground }, headerTitle: ( <ShareListHeader searching={searching} initSearch={initSearch} cancelSearch={cancelSearch} search={search} + theme={screenProps.theme} /> ) }; } return { - headerBackTitle: null, + ...themedHeader(screenProps.theme), headerLeft: searching ? ( <CustomHeaderButtons left> @@ -65,7 +71,7 @@ class ShareListView extends React.Component { testID='share-extension-close' /> ), - headerTitle: <ShareListHeader searching={searching} search={search} />, + headerTitle: <ShareListHeader searching={searching} search={search} theme={screenProps.theme} />, headerRight: ( searching ? null @@ -83,7 +89,8 @@ class ShareListView extends React.Component { server: PropTypes.string, baseUrl: PropTypes.string, token: PropTypes.string, - userId: PropTypes.string + userId: PropTypes.string, + theme: PropTypes.string } constructor(props) { @@ -160,10 +167,13 @@ class ShareListView extends React.Component { return true; } - const { server } = this.props; + const { server, theme } = this.props; if (server !== nextProps.server) { return true; } + if (theme !== nextProps.theme) { + return true; + } const { searchResults } = this.state; if (!isEqual(nextState.searchResults, searchResults)) { @@ -198,7 +208,13 @@ class ShareListView extends React.Component { const serversCollection = serversDB.collections.get('servers'); this.servers = await serversCollection.query().fetch(); this.chats = this.data.slice(0, LIMIT); - const serverInfo = await serversCollection.find(server); + let serverInfo = {}; + try { + serverInfo = await serversCollection.find(server); + } catch (error) { + // Do nothing + } + const canUploadFileResult = canUploadFile(fileInfo || fileData, serverInfo); this.internalSetState({ @@ -267,13 +283,14 @@ class ShareListView extends React.Component { renderSectionHeader = (header) => { const { searching } = this.state; + const { theme } = this.props; if (searching) { return null; } return ( - <View style={styles.headerContainer}> - <Text style={styles.headerText}> + <View style={[styles.headerContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}> + <Text style={[styles.headerText, { color: themes[theme].titleText }]}> {I18n.t(header)} </Text> </View> @@ -281,11 +298,13 @@ class ShareListView extends React.Component { } renderItem = ({ item }) => { - const { userId, token, baseUrl } = this.props; + const { + userId, token, baseUrl, theme + } = this.props; return ( <DirectoryItem user={{ - userId, + id: userId, token }} title={this.getRoomTitle(item)} @@ -299,37 +318,56 @@ class ShareListView extends React.Component { type={item.t} onPress={() => this.shareMessage(item)} testID={`share-extension-item-${ item.name }`} + theme={theme} /> ); } - renderSeparator = () => <View style={styles.separator} />; + renderSeparator = () => { + const { theme } = this.props; + return <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />; + } - renderBorderBottom = () => <View style={styles.borderBottom} />; + renderBorderBottom = () => { + const { theme } = this.props; + return <View style={[styles.borderBottom, { borderColor: themes[theme].separatorColor }]} />; + } renderSelectServer = () => { const { servers } = this.state; - const { server } = this.props; + const { server, theme } = this.props; const currentServer = servers.find(serverFiltered => serverFiltered.id === server); return currentServer ? ( <> {this.renderSectionHeader('Select_Server')} - <View style={styles.bordered}> + <View + style={[ + styles.bordered, + { + borderColor: themes[theme].separatorColor, + backgroundColor: themes[theme].auxiliaryBackground + } + ]} + > <ServerItem server={server} onPress={() => Navigation.navigate('SelectServerView', { servers: this.servers })} item={currentServer} + theme={theme} /> </View> </> ) : null; } - renderEmptyComponent = () => ( - <View style={[styles.container, styles.emptyContainer]}> - <Text style={styles.title}>{I18n.t('No_results_found')}</Text> - </View> - ); + renderEmptyComponent = () => { + const { theme } = this.props; + return ( + <View style={[styles.container, styles.emptyContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}> + <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text> + </View> + ); + } renderHeader = () => { const { searching } = this.state; @@ -352,22 +390,24 @@ class ShareListView extends React.Component { const { chats, mediaLoading, loading, searchResults, searching, searchText } = this.state; + const { theme } = this.props; if (mediaLoading || loading) { - return <ActivityIndicator style={styles.loading} />; + return <ActivityIndicator theme={theme} />; } return ( <FlatList data={searching ? searchResults : chats} keyExtractor={keyExtractor} - style={styles.flatlist} + style={[styles.flatlist, { backgroundColor: themes[theme].auxiliaryBackground }]} + contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} renderItem={this.renderItem} getItemLayout={getItemLayout} ItemSeparatorComponent={this.renderSeparator} ListHeaderComponent={this.renderHeader} ListFooterComponent={!searching && this.renderBorderBottom} - ListHeaderComponentStyle={!searching ? styles.borderBottom : {}} + ListHeaderComponentStyle={!searching ? { ...styles.borderBottom, borderColor: themes[theme].separatorColor } : {}} ListEmptyComponent={searching && searchText ? this.renderEmptyComponent : null} enableEmptySections removeClippedSubviews @@ -382,13 +422,14 @@ class ShareListView extends React.Component { const { fileInfo: file, loading, searching, error } = this.state; + const { theme } = this.props; if (loading) { - return <ActivityIndicator style={styles.loading} />; + return <ActivityIndicator theme={theme} />; } return ( - <View style={styles.container}> + <View style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}> { !searching ? ( <> @@ -397,10 +438,10 @@ class ShareListView extends React.Component { ) : null } - <View style={[styles.container, styles.centered]}> - <Text style={styles.title}>{I18n.t(error)}</Text> - <CustomIcon name='circle-cross' size={120} style={styles.errorIcon} /> - <Text style={styles.fileMime}>{ file.mime }</Text> + <View style={[styles.container, styles.centered, { backgroundColor: themes[theme].auxiliaryBackground }]}> + <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t(error)}</Text> + <CustomIcon name='circle-cross' size={120} color={themes[theme].dangerColor} /> + <Text style={[styles.fileMime, { color: themes[theme].titleText }]}>{ file.mime }</Text> </View> </View> ); @@ -408,9 +449,10 @@ class ShareListView extends React.Component { render() { const { showError } = this.state; + const { theme } = this.props; return ( - <SafeAreaView style={styles.container} forceInset={{ vertical: 'never' }}> - <StatusBar /> + <SafeAreaView style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} forceInset={{ vertical: 'never' }}> + <StatusBar theme={theme} /> { showError ? this.renderError() : this.renderContent() } </SafeAreaView> ); @@ -424,4 +466,4 @@ const mapStateToProps = (({ share }) => ({ baseUrl: share ? share.server : '' })); -export default connect(mapStateToProps)(ShareListView); +export default connect(mapStateToProps)(withTheme(ShareListView)); diff --git a/app/views/ShareListView/styles.js b/app/views/ShareListView/styles.js index 63d7e39ff..59cecb736 100644 --- a/app/views/ShareListView/styles.js +++ b/app/views/ShareListView/styles.js @@ -2,14 +2,9 @@ import { StyleSheet } from 'react-native'; import { isIOS } from '../../utils/deviceInfo'; import sharedStyles from '../Styles'; -import { - COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_DANGER -} from '../../constants/colors'; - export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: COLOR_BACKGROUND_CONTAINER + flex: 1 }, emptyContainer: { padding: 20, @@ -18,7 +13,6 @@ export default StyleSheet.create({ }, content: { flex: 1, - backgroundColor: isIOS ? COLOR_WHITE : '#E1E5E8', justifyContent: 'center', alignItems: 'center' }, @@ -28,8 +22,7 @@ export default StyleSheet.create({ }, flatlist: { marginTop: isIOS ? 6 : 0, // the height of the navigation bar with the searchbar is larger - width: '100%', - backgroundColor: COLOR_BACKGROUND_CONTAINER + width: '100%' }, bordered: { ...sharedStyles.separatorVertical @@ -39,12 +32,10 @@ export default StyleSheet.create({ }, headerContainer: { paddingHorizontal: 15, - backgroundColor: COLOR_BACKGROUND_CONTAINER, paddingBottom: 10, paddingTop: 17 }, headerText: { - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular, fontSize: 17, letterSpacing: 0.27 @@ -53,14 +44,7 @@ export default StyleSheet.create({ ...sharedStyles.separatorBottom, marginLeft: 48 }, - loading: { - flex: 1 - }, - errorIcon: { - color: COLOR_DANGER - }, fileMime: { - ...sharedStyles.textColorNormal, ...sharedStyles.textBold, ...sharedStyles.textAlignCenter, fontSize: 20, @@ -68,7 +52,6 @@ export default StyleSheet.create({ }, title: { fontSize: 14, - ...sharedStyles.textColorTitle, ...sharedStyles.textBold } }); diff --git a/app/views/ShareView/Loading.js b/app/views/ShareView/Loading.js deleted file mode 100644 index 82c3e2a81..000000000 --- a/app/views/ShareView/Loading.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { - StyleSheet, ActivityIndicator, View -} from 'react-native'; -import { COLOR_TEXT } from '../../constants/colors'; - -const styles = StyleSheet.create({ - container: { - height: '100%', - width: '100%', - position: 'absolute', - justifyContent: 'center', - alignItems: 'center' - } -}); - -const Loading = React.memo(() => ( - <View style={styles.container}> - <ActivityIndicator size='large' color={COLOR_TEXT} /> - </View> -)); - -export default Loading; diff --git a/app/views/ShareView/index.js b/app/views/ShareView/index.js index ee0857599..5047e6926 100644 --- a/app/views/ShareView/index.js +++ b/app/views/ShareView/index.js @@ -1,29 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - View, Text, TextInput, Image -} from 'react-native'; +import { View, Text, Image } from 'react-native'; import { connect } from 'react-redux'; import ShareExtension from 'rn-extensions-share'; -import { - COLOR_TEXT_DESCRIPTION -} from '../../constants/colors'; +import { themes } from '../../constants/colors'; import I18n from '../../i18n'; import RocketChat from '../../lib/rocketchat'; import { CustomIcon } from '../../lib/Icons'; import log from '../../utils/log'; import styles from './styles'; -import Loading from './Loading'; +import TextInput from '../../containers/TextInput'; +import ActivityIndicator from '../../containers/ActivityIndicator'; import { CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import { isReadOnly, isBlocked } from '../../utils/room'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; class ShareView extends React.Component { - static navigationOptions = ({ navigation }) => { + static navigationOptions = ({ navigation, screenProps }) => { const canSend = navigation.getParam('canSend', true); return ({ title: I18n.t('Share'), + ...themedHeader(screenProps.theme), headerRight: canSend ? ( @@ -42,6 +42,7 @@ class ShareView extends React.Component { static propTypes = { navigation: PropTypes.object, + theme: PropTypes.string, user: PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, @@ -133,6 +134,7 @@ class ShareView extends React.Component { renderPreview = () => { const { fileInfo } = this.state; + const { theme } = this.props; const icon = fileInfo.mime.match(/image/) ? <Image source={{ isStatic: true, uri: fileInfo.path }} style={styles.mediaImage} /> @@ -143,11 +145,19 @@ class ShareView extends React.Component { ); return ( - <View style={styles.mediaContent}> + <View + style={[ + styles.mediaContent, + { + borderColor: themes[theme].separatorColor, + backgroundColor: themes[theme].auxiliaryBackground + } + ]} + > {icon} <View style={styles.mediaInfo}> - <Text style={styles.mediaText} numberOfLines={1}>{fileInfo.name}</Text> - <Text style={styles.mediaText}>{this.bytesToSize(fileInfo.size)}</Text> + <Text style={[styles.mediaText, { color: themes[theme].titleText }]} numberOfLines={1}>{fileInfo.name}</Text> + <Text style={[styles.mediaText, { color: themes[theme].titleText }]}>{this.bytesToSize(fileInfo.size)}</Text> </View> </View> ); @@ -155,28 +165,42 @@ class ShareView extends React.Component { renderMediaContent = () => { const { fileInfo, file } = this.state; + const { theme } = this.props; + const inputStyle = { + backgroundColor: themes[theme].focusedBackground, + borderColor: themes[theme].separatorColor + }; return fileInfo ? ( <View style={styles.mediaContainer}> {this.renderPreview()} <View style={styles.mediaInputContent}> <TextInput - style={[styles.mediaNameInput, styles.input]} + inputStyle={[ + styles.mediaNameInput, + styles.input, + styles.firstInput, + inputStyle + ]} placeholder={I18n.t('File_name')} onChangeText={name => this.setState({ file: { ...file, name } })} - underlineColorAndroid='transparent' defaultValue={file.name} - placeholderTextColor={COLOR_TEXT_DESCRIPTION} + containerStyle={styles.inputContainer} + theme={theme} /> <TextInput - style={[styles.mediaDescriptionInput, styles.input]} + inputStyle={[ + styles.mediaDescriptionInput, + styles.input, + inputStyle + ]} placeholder={I18n.t('File_description')} onChangeText={description => this.setState({ file: { ...file, description } })} - underlineColorAndroid='transparent' defaultValue={file.description} multiline textAlignVertical='top' - placeholderTextColor={COLOR_TEXT_DESCRIPTION} autoFocus + containerStyle={styles.inputContainer} + theme={theme} /> </View> </View> @@ -185,17 +209,25 @@ class ShareView extends React.Component { renderInput = () => { const { value } = this.state; + const { theme } = this.props; return ( <TextInput - style={[styles.input, styles.textInput]} + containerStyle={[styles.content, styles.inputContainer]} + inputStyle={[ + styles.input, + styles.textInput, + { + borderColor: themes[theme].separatorColor, + backgroundColor: themes[theme].focusedBackground + } + ]} placeholder='' onChangeText={handleText => this.setState({ value: handleText })} - underlineColorAndroid='transparent' defaultValue={value} multiline textAlignVertical='top' - placeholderTextColor={COLOR_TEXT_DESCRIPTION} autoFocus + theme={theme} /> ); } @@ -214,7 +246,7 @@ class ShareView extends React.Component { } render() { - const { user } = this.props; + const { user, theme } = this.props; const { username } = user; const { name, loading, isMedia, room @@ -225,17 +257,28 @@ class ShareView extends React.Component { } return ( - <View style={styles.container}> - <View style={isMedia ? styles.toContent : styles.toContentText}> + <View style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}> + <View + style={[ + isMedia + ? styles.toContent + : styles.toContentText, + { + backgroundColor: isMedia + ? themes[theme].focusedBackground + : themes[theme].auxiliaryBackground + } + ]} + > <Text style={styles.text} numberOfLines={1}> - <Text style={styles.to}>{`${ I18n.t('To') }: `}</Text> - <Text style={styles.name}>{`${ name }`}</Text> + <Text style={[styles.to, { color: themes[theme].auxiliaryText }]}>{`${ I18n.t('To') }: `}</Text> + <Text style={[styles.name, { color: themes[theme].titleText }]}>{`${ name }`}</Text> </Text> </View> - <View style={styles.content}> + <View style={[styles.content, { backgroundColor: themes[theme].auxiliaryBackground }]}> {isMedia ? this.renderMediaContent() : this.renderInput()} </View> - { loading ? <Loading /> : null } + { loading ? <ActivityIndicator size='large' theme={theme} absolute /> : null } </View> ); } @@ -250,4 +293,4 @@ const mapStateToProps = (({ share }) => ({ baseUrl: share ? share.server : '' })); -export default connect(mapStateToProps)(ShareView); +export default connect(mapStateToProps)(withTheme(ShareView)); diff --git a/app/views/ShareView/styles.js b/app/views/ShareView/styles.js index a7c77fda2..d4659d605 100644 --- a/app/views/ShareView/styles.js +++ b/app/views/ShareView/styles.js @@ -1,14 +1,10 @@ import { StyleSheet } from 'react-native'; import sharedStyles from '../Styles'; -import { - COLOR_BACKGROUND_CONTAINER, COLOR_WHITE -} from '../../constants/colors'; export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: COLOR_BACKGROUND_CONTAINER + flex: 1 }, centered: { justifyContent: 'center', @@ -17,54 +13,44 @@ export default StyleSheet.create({ title: { fontSize: 18, ...sharedStyles.textBold, - ...sharedStyles.textColorNormal, ...sharedStyles.textAlignCenter }, text: { paddingHorizontal: 16, paddingVertical: 8, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, to: { - ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, toContent: { - width: '100%', - backgroundColor: COLOR_WHITE + width: '100%' }, toContentText: { width: '100%', - backgroundColor: COLOR_BACKGROUND_CONTAINER, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, name: { - ...sharedStyles.textRegular, - ...sharedStyles.textColorTitle + ...sharedStyles.textRegular }, content: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, mediaContainer: { - flex: 1, - backgroundColor: COLOR_BACKGROUND_CONTAINER + flex: 1 }, mediaContent: { flexDirection: 'row', padding: 16, - backgroundColor: COLOR_BACKGROUND_CONTAINER, - alignItems: 'center' + alignItems: 'center', + ...sharedStyles.separatorTop }, mediaImage: { height: 64, width: 64 }, mediaIcon: { - fontSize: 64, - ...sharedStyles.textColorNormal + fontSize: 64 }, mediaIconContainer: { alignItems: 'center', @@ -76,40 +62,36 @@ export default StyleSheet.create({ }, mediaText: { fontSize: 16, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, mediaInputContent: { - width: '100%', - ...sharedStyles.separatorVertical, - backgroundColor: COLOR_WHITE + width: '100%' }, input: { fontSize: 16, - ...sharedStyles.textColorNormal, - ...sharedStyles.textRegular, - backgroundColor: COLOR_WHITE + ...sharedStyles.textRegular + }, + inputContainer: { + marginBottom: 0 + }, + firstInput: { + borderBottomWidth: 0 }, textInput: { - flex: 1, - paddingHorizontal: 16 + height: '100%' }, mediaNameInput: { - marginLeft: 16, + paddingLeft: 16, paddingRight: 16, - paddingVertical: 8, - backgroundColor: COLOR_WHITE, - ...sharedStyles.separatorBottom + paddingVertical: 8 }, mediaDescriptionInput: { - marginLeft: 16, + paddingLeft: 16, paddingRight: 16, - marginVertical: 8, - backgroundColor: COLOR_WHITE, + paddingVertical: 8, height: 100 }, send: { - ...sharedStyles.textColorHeaderBack, ...sharedStyles.textSemibold, fontSize: 16 } diff --git a/app/views/SidebarView/SidebarItem.js b/app/views/SidebarView/SidebarItem.js index e75ad9858..c0eadea63 100644 --- a/app/views/SidebarView/SidebarItem.js +++ b/app/views/SidebarView/SidebarItem.js @@ -1,31 +1,31 @@ import React from 'react'; import { View, Text } from 'react-native'; import PropTypes from 'prop-types'; -import { RectButton } from 'react-native-gesture-handler'; import styles from './styles'; -import { COLOR_TEXT } from '../../constants/colors'; +import Touch from '../../utils/touch'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; const Item = React.memo(({ - left, text, onPress, testID, current + left, text, onPress, testID, current, theme }) => ( - <RectButton + <Touch key={testID} testID={testID} onPress={onPress} - underlayColor={COLOR_TEXT} - activeOpacity={0.1} - style={[styles.item, current && styles.itemCurrent]} + theme={theme} + style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]} > <View style={styles.itemLeft}> {left} </View> <View style={styles.itemCenter}> - <Text style={styles.itemText}> + <Text style={[styles.itemText, { color: themes[theme].titleText }]}> {text} </Text> </View> - </RectButton> + </Touch> )); Item.propTypes = { @@ -33,7 +33,8 @@ Item.propTypes = { text: PropTypes.string, current: PropTypes.bool, onPress: PropTypes.func, - testID: PropTypes.string + testID: PropTypes.string, + theme: PropTypes.string }; -export default Item; +export default withTheme(Item); diff --git a/app/views/SidebarView/index.js b/app/views/SidebarView/index.js index fa077befb..78b789abf 100644 --- a/app/views/SidebarView/index.js +++ b/app/views/SidebarView/index.js @@ -5,9 +5,9 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import equal from 'deep-equal'; -import { RectButton } from 'react-native-gesture-handler'; import { Q } from '@nozbe/watermelondb'; +import Touch from '../../utils/touch'; import { logout as logoutAction } from '../../actions/login'; import Avatar from '../../containers/Avatar'; import Status from '../../containers/Status/Status'; @@ -18,13 +18,18 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps'; import { CustomIcon } from '../../lib/Icons'; import styles from './styles'; import SidebarItem from './SidebarItem'; -import { COLOR_TEXT } from '../../constants/colors'; +import { themes } from '../../constants/colors'; import database from '../../lib/database'; import { animateNextTransition } from '../../utils/layoutAnimation'; +import { withTheme } from '../../theme'; +import { withSplit } from '../../split'; const keyExtractor = item => item.id; -const Separator = React.memo(() => <View style={styles.separator} />); +const Separator = React.memo(({ theme }) => <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />); +Separator.propTypes = { + theme: PropTypes.string +}; const permissions = [ 'view-statistics', @@ -41,7 +46,9 @@ class Sidebar extends Component { user: PropTypes.object, logout: PropTypes.func.isRequired, activeItemKey: PropTypes.string, - loadingServer: PropTypes.bool + theme: PropTypes.string, + loadingServer: PropTypes.bool, + split: PropTypes.bool } constructor(props) { @@ -71,7 +78,7 @@ class Sidebar extends Component { shouldComponentUpdate(nextProps, nextState) { const { status, showStatus, isAdmin } = this.state; const { - Site_Name, user, baseUrl, activeItemKey + Site_Name, user, baseUrl, activeItemKey, split, theme } = this.props; if (nextState.showStatus !== showStatus) { return true; @@ -88,6 +95,9 @@ class Sidebar extends Component { if (nextProps.activeItemKey !== activeItemKey) { return true; } + if (nextProps.theme !== theme) { + return true; + } if (nextProps.user && user) { if (nextProps.user.language !== user.language) { return true; @@ -99,6 +109,9 @@ class Sidebar extends Component { return true; } } + if (nextProps.split !== split) { + return true; + } if (!equal(nextState.status, status)) { return true; } @@ -182,26 +195,26 @@ class Sidebar extends Component { renderNavigation = () => { const { isAdmin } = this.state; - const { activeItemKey } = this.props; + const { activeItemKey, theme } = this.props; return ( <> <SidebarItem text={I18n.t('Chats')} - left={<CustomIcon name='message' size={20} color={COLOR_TEXT} />} + left={<CustomIcon name='message' size={20} color={themes[theme].titleText} />} onPress={() => this.sidebarNavigate('RoomsListView')} testID='sidebar-chats' current={activeItemKey === 'ChatsStack'} /> <SidebarItem text={I18n.t('Profile')} - left={<CustomIcon name='user' size={20} color={COLOR_TEXT} />} + left={<CustomIcon name='user' size={20} color={themes[theme].titleText} />} onPress={() => this.sidebarNavigate('ProfileView')} testID='sidebar-profile' current={activeItemKey === 'ProfileStack'} /> <SidebarItem text={I18n.t('Settings')} - left={<CustomIcon name='cog' size={20} color={COLOR_TEXT} />} + left={<CustomIcon name='cog' size={20} color={themes[theme].titleText} />} onPress={() => this.sidebarNavigate('SettingsView')} testID='sidebar-settings' current={activeItemKey === 'SettingsStack'} @@ -209,16 +222,16 @@ class Sidebar extends Component { {isAdmin ? ( <SidebarItem text={I18n.t('Admin_Panel')} - left={<CustomIcon name='shield-alt' size={20} color={COLOR_TEXT} />} + left={<CustomIcon name='shield-alt' size={20} color={themes[theme].titleText} />} onPress={() => this.sidebarNavigate('AdminPanelView')} testID='sidebar-settings' current={activeItemKey === 'AdminPanelStack'} /> ) : null} - <Separator key='separator-logout' /> + <Separator theme={theme} /> <SidebarItem text={I18n.t('Logout')} - left={<CustomIcon name='sign-out' size={20} color={COLOR_TEXT} />} + left={<CustomIcon name='sign-out' size={20} color={themes[theme].titleText} />} onPress={this.logout} testID='sidebar-logout' /> @@ -231,7 +244,6 @@ class Sidebar extends Component { const { user } = this.props; return ( <FlatList - key='status-list' data={status} extraData={user} renderItem={this.renderStatusItem} @@ -242,20 +254,31 @@ class Sidebar extends Component { render() { const { showStatus } = this.state; - const { user, Site_Name, baseUrl } = this.props; + const { + user, Site_Name, baseUrl, split, theme + } = this.props; if (!user) { return null; } return ( - <SafeAreaView testID='sidebar-view' style={styles.container}> - <ScrollView style={styles.container} {...scrollPersistTaps}> - <RectButton + <SafeAreaView testID='sidebar-view' style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}> + <ScrollView + style={[ + styles.container, + { + backgroundColor: split + ? themes[theme].backgroundColor + : themes[theme].focusedBackground + } + ]} + {...scrollPersistTaps} + > + <Touch onPress={this.toggleStatus} - underlayColor={COLOR_TEXT} - activeOpacity={0.1} testID='sidebar-toggle-status' style={styles.header} + theme={theme} > <Avatar text={user.username} @@ -268,16 +291,16 @@ class Sidebar extends Component { <View style={styles.headerTextContainer}> <View style={styles.headerUsername}> <Status style={styles.status} size={12} status={user && user.status} /> - <Text numberOfLines={1} style={styles.username}>{user.username}</Text> + <Text numberOfLines={1} style={[styles.username, { color: themes[theme].titleText }]}>{user.username}</Text> </View> - <Text style={styles.currentServerText} numberOfLines={1}>{Site_Name}</Text> + <Text style={[styles.currentServerText, { color: themes[theme].titleText }]} numberOfLines={1}>{Site_Name}</Text> </View> - <CustomIcon name='arrow-down' size={20} style={[styles.headerIcon, showStatus && styles.inverted]} /> - </RectButton> + <CustomIcon name='arrow-down' size={20} style={[styles.headerIcon, showStatus && styles.inverted, { color: themes[theme].titleText }]} /> + </Touch> - <Separator key='separator-header' /> + {!split || showStatus ? <Separator theme={theme} /> : null} - {!showStatus ? this.renderNavigation() : null} + {!showStatus && !split ? this.renderNavigation() : null} {showStatus ? this.renderStatus() : null} </ScrollView> </SafeAreaView> @@ -303,4 +326,4 @@ const mapDispatchToProps = dispatch => ({ logout: () => dispatch(logoutAction()) }); -export default connect(mapStateToProps, mapDispatchToProps)(Sidebar); +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(withSplit(Sidebar))); diff --git a/app/views/SidebarView/styles.js b/app/views/SidebarView/styles.js index 98a7d54ef..b3b07f787 100644 --- a/app/views/SidebarView/styles.js +++ b/app/views/SidebarView/styles.js @@ -1,12 +1,10 @@ import { StyleSheet } from 'react-native'; -import { COLOR_SEPARATOR, COLOR_WHITE } from '../../constants/colors'; import sharedStyles from '../Styles'; export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, item: { flexDirection: 'row', @@ -26,12 +24,10 @@ export default StyleSheet.create({ itemText: { marginVertical: 16, fontSize: 14, - ...sharedStyles.textSemibold, - ...sharedStyles.textColorNormal + ...sharedStyles.textSemibold }, separator: { borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: COLOR_SEPARATOR, marginVertical: 4 }, header: { @@ -50,12 +46,10 @@ export default StyleSheet.create({ }, username: { fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textMedium }, headerIcon: { - paddingHorizontal: 10, - ...sharedStyles.textColorNormal + paddingHorizontal: 10 }, avatar: { marginHorizontal: 10 @@ -65,14 +59,12 @@ export default StyleSheet.create({ }, currentServerText: { fontSize: 14, - ...sharedStyles.textColorNormal, ...sharedStyles.textSemibold }, version: { marginHorizontal: 10, marginBottom: 10, fontSize: 13, - ...sharedStyles.textColorNormal, ...sharedStyles.textSemibold }, inverted: { diff --git a/app/views/Styles.js b/app/views/Styles.js index 4861380af..b47df72c5 100644 --- a/app/views/Styles.js +++ b/app/views/Styles.js @@ -1,56 +1,37 @@ import { StyleSheet, Platform } from 'react-native'; -import { - COLOR_DANGER, COLOR_BUTTON_PRIMARY, COLOR_SEPARATOR, COLOR_TEXT, COLOR_TEXT_DESCRIPTION, COLOR_TITLE, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_PRIMARY, HEADER_BACK -} from '../constants/colors'; +import { MAX_SCREEN_CONTENT_WIDTH, MAX_CONTENT_WIDTH } from '../constants/tablet'; export default StyleSheet.create({ root: { flex: 1 }, container: { - backgroundColor: 'white', flex: 1 }, containerScrollView: { padding: 15, paddingBottom: 30 }, - buttonContainerLastChild: { - marginBottom: 40 - }, - buttonContainer: { - paddingVertical: 15, - backgroundColor: '#414852', - marginBottom: 20, - borderRadius: 2 - }, - buttonContainer_inverted: { - paddingVertical: 15, - marginBottom: 0 - }, - button: { - textAlign: 'center', - color: 'white', - fontWeight: '700' - }, - button_inverted: { - textAlign: 'center', - color: '#414852', - fontWeight: '700', - flexGrow: 1 - }, - error: { - textAlign: 'center', - color: COLOR_DANGER, - paddingTop: 5 - }, - loading: { + containerSplitView: { flex: 1, - position: 'absolute', - backgroundColor: 'rgba(255,255,255,.2)', - left: 0, - top: 0 + flexDirection: 'row' + }, + tabletContent: { + maxWidth: MAX_CONTENT_WIDTH + }, + tabletScreenContent: { + alignSelf: 'center', + width: MAX_SCREEN_CONTENT_WIDTH + }, + modal: { + // Following UIModalPresentationFormSheet size + // this not change on different iPad sizes + width: 540, + height: 620, + alignSelf: 'center', + borderRadius: 10, + overflow: 'hidden' }, status: { position: 'absolute', @@ -59,22 +40,6 @@ export default StyleSheet.create({ borderWidth: 3, borderColor: '#fff' }, - link: { - fontWeight: 'bold', - color: COLOR_BUTTON_PRIMARY - }, - alignItemsFlexEnd: { - alignItems: 'flex-end' - }, - alignItemsFlexStart: { - alignItems: 'flex-start' - }, - alignItemsCenter: { - alignItems: 'center' - }, - textAlignRight: { - textAlign: 'right' - }, textAlignCenter: { textAlign: 'center' }, @@ -84,39 +49,29 @@ export default StyleSheet.create({ loginTitle: { fontSize: 20, marginVertical: 15, - color: COLOR_TITLE, lineHeight: 28 }, loginSubtitle: { fontSize: 16, - color: COLOR_TITLE, lineHeight: 20, marginBottom: 15 }, - headerButton: { - backgroundColor: 'transparent', - height: 44, - width: 44, - alignItems: 'center', - justifyContent: 'center' - }, separator: { - height: StyleSheet.hairlineWidth, - backgroundColor: COLOR_SEPARATOR + height: StyleSheet.hairlineWidth }, separatorTop: { - borderColor: COLOR_SEPARATOR, borderTopWidth: StyleSheet.hairlineWidth }, separatorBottom: { - borderColor: COLOR_SEPARATOR, borderBottomWidth: StyleSheet.hairlineWidth }, separatorVertical: { - borderColor: COLOR_SEPARATOR, borderTopWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth }, + separatorLeft: { + borderLeftWidth: StyleSheet.hairlineWidth + }, textRegular: { backgroundColor: 'transparent', ...Platform.select({ @@ -173,38 +128,16 @@ export default StyleSheet.create({ } }) }, - textColorTitle: { - color: COLOR_TITLE - }, - textColorNormal: { - color: COLOR_TEXT - }, - textColorDescription: { - color: COLOR_TEXT_DESCRIPTION - }, - textColorHeaderBack: { - color: HEADER_BACK - }, - colorPrimary: { - color: COLOR_PRIMARY - }, inputLastChild: { marginBottom: 15 }, - listSafeArea: { - flex: 1, - backgroundColor: COLOR_BACKGROUND_CONTAINER - }, listContentContainer: { - borderColor: COLOR_SEPARATOR, borderTopWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth, - backgroundColor: COLOR_WHITE, - marginVertical: 10 + marginVertical: 36 }, notchLandscapeContainer: { marginTop: -34, - paddingHorizontal: 30, - backgroundColor: COLOR_BACKGROUND_CONTAINER + paddingHorizontal: 30 } }); diff --git a/app/views/TableView.js b/app/views/TableView.js deleted file mode 100644 index 6f61fee43..000000000 --- a/app/views/TableView.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { ScrollView } from 'react-native'; -import PropTypes from 'prop-types'; - -import I18n from '../i18n'; -import { isIOS } from '../utils/deviceInfo'; - -export default class TableView extends React.Component { - static navigationOptions = () => ({ - title: I18n.t('Table') - }); - - static propTypes = { - navigation: PropTypes.object - } - - render() { - const { navigation } = this.props; - const renderRows = navigation.getParam('renderRows'); - const tableWidth = navigation.getParam('tableWidth'); - - if (isIOS) { - return ( - <ScrollView contentContainerStyle={{ width: tableWidth }}> - {renderRows()} - </ScrollView> - ); - } - - return ( - <ScrollView> - <ScrollView horizontal> - {renderRows()} - </ScrollView> - </ScrollView> - ); - } -} diff --git a/app/views/ThemeView.js b/app/views/ThemeView.js new file mode 100644 index 000000000..e6634be16 --- /dev/null +++ b/app/views/ThemeView.js @@ -0,0 +1,196 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + FlatList, Text, View, StyleSheet +} from 'react-native'; +import { SafeAreaView } from 'react-navigation'; +import RNUserDefaults from 'rn-user-defaults'; + +import I18n from '../i18n'; +import { themedHeader } from '../utils/navigation'; +import { withTheme } from '../theme'; +import { themes } from '../constants/colors'; +import sharedStyles from './Styles'; +import StatusBar from '../containers/StatusBar'; +import Separator from '../containers/Separator'; +import ListItem from '../containers/ListItem'; +import { CustomIcon } from '../lib/Icons'; +import { THEME_PREFERENCES_KEY } from '../lib/rocketchat'; +import { supportSystemTheme } from '../utils/deviceInfo'; + +const THEME_GROUP = 'THEME_GROUP'; +const DARK_GROUP = 'DARK_GROUP'; + +const SYSTEM_THEME = { + label: I18n.t('Automatic'), + value: 'automatic', + group: THEME_GROUP +}; + +const THEMES = [ + { + label: I18n.t('Light'), + value: 'light', + group: THEME_GROUP + }, { + label: I18n.t('Dark'), + value: 'dark', + group: THEME_GROUP + }, { + label: I18n.t('Dark'), + value: 'dark', + separator: true, + header: I18n.t('Dark_level'), + group: DARK_GROUP + }, { + label: I18n.t('Black'), + value: 'black', + group: DARK_GROUP + } +]; + +const styles = StyleSheet.create({ + list: { + paddingBottom: 18 + }, + info: { + paddingTop: 25, + paddingBottom: 18, + paddingHorizontal: 16 + }, + infoText: { + fontSize: 16, + ...sharedStyles.textRegular + } +}); + +class ThemeView extends React.Component { + static navigationOptions = ({ screenProps }) => ({ + title: I18n.t('Theme'), + ...themedHeader(screenProps.theme) + }) + + static propTypes = { + theme: PropTypes.string, + themePreferences: PropTypes.object, + setTheme: PropTypes.func + } + + constructor(props) { + super(props); + if (supportSystemTheme()) { + THEMES.unshift(SYSTEM_THEME); + } + } + + isSelected = (item) => { + const { themePreferences } = this.props; + const { group } = item; + const { darkLevel, currentTheme } = themePreferences; + if (group === THEME_GROUP) { + return item.value === currentTheme; + } + if (group === DARK_GROUP) { + return item.value === darkLevel; + } + } + + onClick = (item) => { + const { themePreferences } = this.props; + const { darkLevel, currentTheme } = themePreferences; + const { value, group } = item; + let changes = {}; + if (group === THEME_GROUP && currentTheme !== value) { + changes = { currentTheme: value }; + } + if (group === DARK_GROUP && darkLevel !== value) { + changes = { darkLevel: value }; + } + this.setTheme(changes); + } + + setTheme = async(theme) => { + const { setTheme, themePreferences } = this.props; + const newTheme = { ...themePreferences, ...theme }; + setTheme(newTheme); + await RNUserDefaults.setObjectForKey(THEME_PREFERENCES_KEY, newTheme); + }; + + renderSeparator = () => { + const { theme } = this.props; + return <Separator theme={theme} />; + } + + renderIcon = () => { + const { theme } = this.props; + return <CustomIcon name='check' size={20} color={themes[theme].tintColor} />; + } + + renderItem = ({ item, index }) => { + const { theme } = this.props; + const { label, value } = item; + const isFirst = index === 0; + return ( + <> + {item.separator || isFirst ? this.renderSectionHeader(item.header) : null} + <ListItem + title={label} + onPress={() => this.onClick(item)} + testID={`theme-view-${ value }`} + right={this.isSelected(item) ? this.renderIcon : null} + theme={theme} + /> + </> + ); + } + + renderSectionHeader = (header = I18n.t('Theme')) => { + const { theme } = this.props; + return ( + <> + <View style={styles.info}> + <Text style={[styles.infoText, { color: themes[theme].infoText }]}>{header}</Text> + </View> + {this.renderSeparator()} + </> + ); + } + + renderFooter = () => { + const { theme } = this.props; + return ( + <View style={[styles.info, sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]}> + <Text style={{ color: themes[theme].infoText }}> + {I18n.t('Applying_a_theme_will_change_how_the_app_looks')} + </Text> + </View> + ); + } + + render() { + const { theme } = this.props; + return ( + <SafeAreaView + style={[sharedStyles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} + forceInset={{ vertical: 'never' }} + testID='theme-view' + > + <StatusBar theme={theme} /> + <FlatList + data={THEMES} + keyExtractor={item => item.value} + contentContainerStyle={[ + styles.list, + { borderColor: themes[theme].separatorColor } + ]} + renderItem={this.renderItem} + ListHeaderComponent={this.renderHeader} + ListFooterComponent={this.renderFooter} + ItemSeparatorComponent={this.renderSeparator} + /> + </SafeAreaView> + ); + } +} + +export default withTheme(ThemeView); diff --git a/app/views/ThreadMessagesView/index.js b/app/views/ThreadMessagesView/index.js index 05e6c7821..0d3c84195 100644 --- a/app/views/ThreadMessagesView/index.js +++ b/app/views/ThreadMessagesView/index.js @@ -12,7 +12,7 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import styles from './styles'; import Message from '../../containers/message'; -import RCActivityIndicator from '../../containers/ActivityIndicator'; +import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import RocketChat from '../../lib/rocketchat'; import database from '../../lib/database'; @@ -21,21 +21,32 @@ import buildMessage from '../../lib/methods/helpers/buildMessage'; import log from '../../utils/log'; import debounce from '../../utils/debounce'; import protectedFunction from '../../lib/methods/helpers/protectedFunction'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; +import { themedHeader } from '../../utils/navigation'; +import ModalNavigation from '../../lib/ModalNavigation'; + +const Separator = React.memo(({ theme }) => <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />); +Separator.propTypes = { + theme: PropTypes.string +}; -const Separator = React.memo(() => <View style={styles.separator} />); const API_FETCH_COUNT = 50; class ThreadMessagesView extends React.Component { - static navigationOptions = { + static navigationOptions = ({ screenProps }) => ({ + ...themedHeader(screenProps.theme), title: I18n.t('Threads') - } + }); static propTypes = { user: PropTypes.object, navigation: PropTypes.object, baseUrl: PropTypes.string, useRealName: PropTypes.bool, - customEmojis: PropTypes.object + theme: PropTypes.string, + customEmojis: PropTypes.object, + screenProps: PropTypes.object } constructor(props) { @@ -249,13 +260,32 @@ class ThreadMessagesView extends React.Component { }); }, 1000, true) - renderSeparator = () => <Separator /> + renderSeparator = () => { + const { theme } = this.props; + return <Separator theme={theme} />; + } - renderEmpty = () => ( - <View style={styles.listEmptyContainer} testID='thread-messages-view'> - <Text style={styles.noDataFound}>{I18n.t('No_thread_messages')}</Text> - </View> - ) + renderEmpty = () => { + const { theme } = this.props; + return ( + <View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].backgroundColor }]} testID='thread-messages-view'> + <Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{I18n.t('No_thread_messages')}</Text> + </View> + ); + } + + navToRoomInfo = (navParam) => { + const { navigation, user, screenProps } = this.props; + if (navParam.rid === user.id) { + return; + } + if (screenProps && screenProps.split) { + navigation.navigate('RoomActionsView', { rid: this.rid, t: this.t }); + ModalNavigation.navigate('RoomInfoView', navParam); + } else { + navigation.navigate('RoomInfoView', navParam); + } + } renderItem = ({ item }) => { const { @@ -276,12 +306,14 @@ class ThreadMessagesView extends React.Component { baseUrl={baseUrl} useRealName={useRealName} getCustomEmoji={this.getCustomEmoji} + navToRoomInfo={this.navToRoomInfo} /> ); } render() { const { loading, messages } = this.state; + const { theme } = this.props; if (!loading && messages.length === 0) { return this.renderEmpty(); @@ -289,12 +321,12 @@ class ThreadMessagesView extends React.Component { return ( <SafeAreaView style={styles.list} testID='thread-messages-view' forceInset={{ vertical: 'never' }}> - <StatusBar /> + <StatusBar theme={theme} /> <FlatList data={messages} extraData={this.state} renderItem={this.renderItem} - style={styles.list} + style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} contentContainerStyle={styles.contentContainer} keyExtractor={item => item.id} onEndReached={this.load} @@ -302,7 +334,7 @@ class ThreadMessagesView extends React.Component { maxToRenderPerBatch={5} initialNumToRender={1} ItemSeparatorComponent={this.renderSeparator} - ListFooterComponent={loading ? <RCActivityIndicator /> : null} + ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null} /> </SafeAreaView> ); @@ -320,4 +352,4 @@ const mapStateToProps = state => ({ customEmojis: state.customEmojis }); -export default connect(mapStateToProps)(ThreadMessagesView); +export default connect(mapStateToProps)(withTheme(ThreadMessagesView)); diff --git a/app/views/ThreadMessagesView/styles.js b/app/views/ThreadMessagesView/styles.js index e1a77b974..1bb0b0e14 100644 --- a/app/views/ThreadMessagesView/styles.js +++ b/app/views/ThreadMessagesView/styles.js @@ -1,23 +1,19 @@ import { StyleSheet } from 'react-native'; import sharedStyles from '../Styles'; -import { COLOR_WHITE, COLOR_SEPARATOR } from '../../constants/colors'; export default StyleSheet.create({ list: { - flex: 1, - backgroundColor: COLOR_WHITE + flex: 1 }, listEmptyContainer: { flex: 1, alignItems: 'center', - justifyContent: 'center', - backgroundColor: COLOR_WHITE + justifyContent: 'center' }, noDataFound: { fontSize: 14, - ...sharedStyles.textRegular, - ...sharedStyles.textColorNormal + ...sharedStyles.textRegular }, contentContainer: { paddingBottom: 30 @@ -26,7 +22,6 @@ export default StyleSheet.create({ height: StyleSheet.hairlineWidth, width: '100%', marginLeft: 60, - marginTop: 10, - backgroundColor: COLOR_SEPARATOR + marginTop: 10 } }); diff --git a/app/views/WithoutServersView.js b/app/views/WithoutServersView.js index cd763cc1b..75b7968ed 100644 --- a/app/views/WithoutServersView.js +++ b/app/views/WithoutServersView.js @@ -2,36 +2,37 @@ import React from 'react'; import { StyleSheet, View, Text } from 'react-native'; +import PropTypes from 'prop-types'; import ShareExtension from 'rn-extensions-share'; import { CloseShareExtensionButton } from '../containers/HeaderButton'; import sharedStyles from './Styles'; import I18n from '../i18n'; -import { COLOR_WHITE } from '../constants/colors'; +import { themes } from '../constants/colors'; +import { themedHeader } from '../utils/navigation'; +import { withTheme } from '../theme'; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: COLOR_WHITE, justifyContent: 'center', alignItems: 'center', padding: 15 }, title: { fontSize: 18, - ...sharedStyles.textBold, - ...sharedStyles.textColorNormal + ...sharedStyles.textBold }, content: { fontSize: 14, ...sharedStyles.textAlignCenter, - ...sharedStyles.textColorNormal, ...sharedStyles.textRegular } }); -export default class WithoutServerView extends React.Component { - static navigationOptions = () => ({ +class WithoutServerView extends React.Component { + static navigationOptions = ({ screenProps }) => ({ + ...themedHeader(screenProps.theme), headerLeft: ( <CloseShareExtensionButton onPress={ShareExtension.close} @@ -40,12 +41,19 @@ export default class WithoutServerView extends React.Component { ) }) + static propTypes = { + theme: PropTypes.string + } + render() { + const { theme } = this.props; return ( - <View style={styles.container}> - <Text style={styles.title}>{I18n.t('Without_Servers')}</Text> - <Text style={styles.content}>{I18n.t('You_need_to_access_at_least_one_RocketChat_server_to_share_something')}</Text> + <View style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]}> + <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Without_Servers')}</Text> + <Text style={[styles.content, { color: themes[theme].titleText }]}>{I18n.t('You_need_to_access_at_least_one_RocketChat_server_to_share_something')}</Text> </View> ); } } + +export default withTheme(WithoutServerView); diff --git a/icons/20x20/at.svg b/icons/20x20/at.svg deleted file mode 100644 index 0af789c1a..000000000 --- a/icons/20x20/at.svg +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch --> - <title>20x20/at--dark - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/icons/20x20/image.svg b/icons/20x20/image.svg deleted file mode 100644 index 8a1efb5ed..000000000 --- a/icons/20x20/image.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 20x20/image--dark - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/icons/20x20/location.svg b/icons/20x20/location.svg deleted file mode 100644 index 15acd1511..000000000 --- a/icons/20x20/location.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - 20x20/locaiton--dark - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/icons/20x20/video.svg b/icons/20x20/video.svg deleted file mode 100644 index f40e7f246..000000000 --- a/icons/20x20/video.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - 20x20/video--dark - Created with Sketch. - - - - - - - - - - - \ No newline at end of file diff --git a/icons/20x20/volume.svg b/icons/20x20/volume.svg deleted file mode 100644 index f3d22ea4d..000000000 --- a/icons/20x20/volume.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - 20x20/volume--dark - Created with Sketch. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 176769997..bf26b8f45 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -1,12 +1,12 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.0) - addressable (2.6.0) - public_suffix (>= 2.0.2, < 4.0) + CFPropertyList (3.0.1) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) atomos (0.1.3) - babosa (1.0.2) - claide (1.0.2) + babosa (1.0.3) + claide (1.0.3) colored (1.2) colored2 (3.1.2) commander-fastlane (4.4.6) @@ -14,20 +14,20 @@ GEM declarative (0.0.10) declarative-option (0.1.0) digest-crc (0.4.1) - domain_name (0.5.20180417) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.4) + dotenv (2.7.5) emoji_regex (1.0.1) - excon (0.64.0) - faraday (0.15.4) + excon (0.68.0) + faraday (0.17.0) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) faraday (>= 0.7.4) http-cookie (~> 1.0.0) faraday_middleware (0.13.1) faraday (>= 0.7.4, < 1.0) - fastimage (2.1.5) - fastlane (2.126.0) + fastimage (2.1.7) + fastlane (2.134.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) @@ -37,9 +37,9 @@ GEM dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 2.0) excon (>= 0.45.0, < 1.0.0) - faraday (~> 0.9) + faraday (~> 0.17) faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 0.9) + faraday_middleware (~> 0.13.1) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-api-client (>= 0.21.2, < 0.24.0) @@ -47,12 +47,12 @@ GEM highline (>= 1.7.2, < 2.0.0) json (< 3.0.0) jwt (~> 2.1.0) - mini_magick (~> 4.5.1) + mini_magick (>= 4.9.4, < 5.0.0) multi_xml (~> 0.5) multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) public_suffix (~> 2.0.0) - rubyzip (>= 1.2.2, < 2.0.0) + rubyzip (>= 1.3.0, < 2.0.0) security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) @@ -73,9 +73,9 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.9) - google-cloud-core (1.3.0) + google-cloud-core (1.4.1) google-cloud-env (~> 1.0) - google-cloud-env (1.2.0) + google-cloud-env (1.3.0) faraday (~> 0.11) google-cloud-storage (1.16.0) digest-crc (~> 0.4) @@ -96,11 +96,11 @@ GEM json (2.2.0) jwt (2.1.0) memoist (0.16.0) - mime-types (3.2.2) + mime-types (3.3) mime-types-data (~> 3.2015) - mime-types-data (3.2019.0331) - mini_magick (4.5.1) - multi_json (1.13.1) + mime-types-data (3.2019.1009) + mini_magick (4.9.5) + multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.0.0) nanaimo (0.2.6) @@ -114,14 +114,14 @@ GEM uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) - rubyzip (1.2.3) + rubyzip (1.3.0) security (0.1.3) - signet (0.11.0) + signet (0.12.0) addressable (~> 2.3) faraday (~> 0.9) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.5) + simctl (1.6.6) CFPropertyList naturally slack-notifier (2.3.2) @@ -138,7 +138,7 @@ GEM unf_ext (0.0.7.6) unicode-display_width (1.6.0) word_wrap (1.0.0) - xcodeproj (1.10.0) + xcodeproj (1.13.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/ios/LaunchScreen.storyboard b/ios/LaunchScreen.storyboard new file mode 100644 index 000000000..e84781fe8 --- /dev/null +++ b/ios/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Podfile b/ios/Podfile index f9edbed91..cb86d4ae6 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -36,6 +36,7 @@ target 'RocketChatRN' do use_native_modules! use_unimodules! + end target 'ShareRocketChatRN' do diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e280640b6..e057c8ba9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - boost-for-react-native (1.63.0) - - BugsnagReactNative (2.22.4): - - BugsnagReactNative/Core (= 2.22.4) + - BugsnagReactNative (2.23.2): + - BugsnagReactNative/Core (= 2.23.2) - React - - BugsnagReactNative/Core (2.22.4): + - BugsnagReactNative/Core (2.23.2): - React - Crashlytics (3.14.0): - Fabric (~> 1.10.2) @@ -26,14 +26,14 @@ PODS: - EXWebBrowser (6.0.0): - UMCore - Fabric (1.10.2) - - FBLazyVector (0.61.1) - - FBReactNativeSpec (0.61.1): + - FBLazyVector (0.61.3) + - FBReactNativeSpec (0.61.3): - Folly (= 2018.10.22.00) - - RCTRequired (= 0.61.1) - - RCTTypeSafety (= 0.61.1) - - React-Core (= 0.61.1) - - React-jsi (= 0.61.1) - - ReactCommon/turbomodule/core (= 0.61.1) + - RCTRequired (= 0.61.3) + - RCTTypeSafety (= 0.61.3) + - React-Core (= 0.61.3) + - React-jsi (= 0.61.3) + - ReactCommon/turbomodule/core (= 0.61.3) - Firebase/Core (6.8.1): - Firebase/CoreOnly - FirebaseAnalytics (= 6.1.1) @@ -102,6 +102,8 @@ PODS: - GoogleUtilities/UserDefaults (6.3.0): - GoogleUtilities/Logger - JitsiMeetSDK (2.4.0) + - KeyCommands (2.0.3): + - React - libwebp (1.0.3): - libwebp/demux (= 1.0.3) - libwebp/mux (= 1.0.3) @@ -117,169 +119,171 @@ PODS: - nanopb/decode (0.3.901) - nanopb/encode (0.3.901) - QBImagePickerController (3.4.0) - - RCTRequired (0.61.1) - - RCTTypeSafety (0.61.1): - - FBLazyVector (= 0.61.1) + - RCTRequired (0.61.3) + - RCTTypeSafety (0.61.3): + - FBLazyVector (= 0.61.3) - Folly (= 2018.10.22.00) - - RCTRequired (= 0.61.1) - - React-Core (= 0.61.1) - - React (0.61.1): - - React-Core (= 0.61.1) - - React-Core/DevSupport (= 0.61.1) - - React-Core/RCTWebSocket (= 0.61.1) - - React-RCTActionSheet (= 0.61.1) - - React-RCTAnimation (= 0.61.1) - - React-RCTBlob (= 0.61.1) - - React-RCTImage (= 0.61.1) - - React-RCTLinking (= 0.61.1) - - React-RCTNetwork (= 0.61.1) - - React-RCTSettings (= 0.61.1) - - React-RCTText (= 0.61.1) - - React-RCTVibration (= 0.61.1) - - React-Core (0.61.1): + - RCTRequired (= 0.61.3) + - React-Core (= 0.61.3) + - React (0.61.3): + - React-Core (= 0.61.3) + - React-Core/DevSupport (= 0.61.3) + - React-Core/RCTWebSocket (= 0.61.3) + - React-RCTActionSheet (= 0.61.3) + - React-RCTAnimation (= 0.61.3) + - React-RCTBlob (= 0.61.3) + - React-RCTImage (= 0.61.3) + - React-RCTLinking (= 0.61.3) + - React-RCTNetwork (= 0.61.3) + - React-RCTSettings (= 0.61.3) + - React-RCTText (= 0.61.3) + - React-RCTVibration (= 0.61.3) + - React-Core (0.61.3): - Folly (= 2018.10.22.00) - glog - - React-Core/Default (= 0.61.1) - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-Core/Default (= 0.61.3) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/CoreModulesHeaders (0.61.1): + - React-Core/CoreModulesHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/Default (0.61.1): + - React-Core/Default (0.61.3): - Folly (= 2018.10.22.00) - glog - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/DevSupport (0.61.1): + - React-Core/DevSupport (0.61.3): - Folly (= 2018.10.22.00) - glog - - React-Core/Default (= 0.61.1) - - React-Core/RCTWebSocket (= 0.61.1) - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) - - React-jsinspector (= 0.61.1) + - React-Core/Default (= 0.61.3) + - React-Core/RCTWebSocket (= 0.61.3) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) + - React-jsinspector (= 0.61.3) - Yoga - - React-Core/RCTActionSheetHeaders (0.61.1): + - React-Core/RCTActionSheetHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTAnimationHeaders (0.61.1): + - React-Core/RCTAnimationHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTBlobHeaders (0.61.1): + - React-Core/RCTBlobHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTImageHeaders (0.61.1): + - React-Core/RCTImageHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTLinkingHeaders (0.61.1): + - React-Core/RCTLinkingHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTNetworkHeaders (0.61.1): + - React-Core/RCTNetworkHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTSettingsHeaders (0.61.1): + - React-Core/RCTSettingsHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTTextHeaders (0.61.1): + - React-Core/RCTTextHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTVibrationHeaders (0.61.1): + - React-Core/RCTVibrationHeaders (0.61.3): - Folly (= 2018.10.22.00) - glog - React-Core/Default - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-Core/RCTWebSocket (0.61.1): + - React-Core/RCTWebSocket (0.61.3): - Folly (= 2018.10.22.00) - glog - - React-Core/Default (= 0.61.1) - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsiexecutor (= 0.61.1) + - React-Core/Default (= 0.61.3) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsiexecutor (= 0.61.3) - Yoga - - React-CoreModules (0.61.1): - - FBReactNativeSpec (= 0.61.1) + - React-CoreModules (0.61.3): + - FBReactNativeSpec (= 0.61.3) - Folly (= 2018.10.22.00) - - RCTTypeSafety (= 0.61.1) - - React-Core/CoreModulesHeaders (= 0.61.1) - - React-RCTImage (= 0.61.1) - - ReactCommon/turbomodule/core (= 0.61.1) - - React-cxxreact (0.61.1): + - RCTTypeSafety (= 0.61.3) + - React-Core/CoreModulesHeaders (= 0.61.3) + - React-RCTImage (= 0.61.3) + - ReactCommon/turbomodule/core (= 0.61.3) + - React-cxxreact (0.61.3): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-jsinspector (= 0.61.1) - - React-jsi (0.61.1): + - React-jsinspector (= 0.61.3) + - React-jsi (0.61.3): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-jsi/Default (= 0.61.1) - - React-jsi/Default (0.61.1): + - React-jsi/Default (= 0.61.3) + - React-jsi/Default (0.61.3): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-jsiexecutor (0.61.1): + - React-jsiexecutor (0.61.3): - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - React-jsinspector (0.61.1) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - React-jsinspector (0.61.3) + - react-native-appearance (0.3.1): + - React - react-native-background-timer (2.1.1): - React - react-native-document-picker (3.2.4): @@ -295,55 +299,55 @@ PODS: - React - react-native-orientation-locker (1.1.6): - React - - react-native-slider (2.0.1): + - react-native-slider (2.0.5): - React - react-native-splash-screen (3.2.0): - React - - react-native-video (5.0.0): + - react-native-video (5.0.2): - React - - react-native-video/Video (= 5.0.0) - - react-native-video/Video (5.0.0): + - react-native-video/Video (= 5.0.2) + - react-native-video/Video (5.0.2): - React - - react-native-webview (6.8.0): + - react-native-webview (7.5.1): - React - - React-RCTActionSheet (0.61.1): - - React-Core/RCTActionSheetHeaders (= 0.61.1) - - React-RCTAnimation (0.61.1): - - React-Core/RCTAnimationHeaders (= 0.61.1) - - React-RCTBlob (0.61.1): - - React-Core/RCTBlobHeaders (= 0.61.1) - - React-Core/RCTWebSocket (= 0.61.1) - - React-jsi (= 0.61.1) - - React-RCTNetwork (= 0.61.1) - - React-RCTImage (0.61.1): - - React-Core/RCTImageHeaders (= 0.61.1) - - React-RCTNetwork (= 0.61.1) - - React-RCTLinking (0.61.1): - - React-Core/RCTLinkingHeaders (= 0.61.1) - - React-RCTNetwork (0.61.1): - - React-Core/RCTNetworkHeaders (= 0.61.1) - - React-RCTSettings (0.61.1): - - React-Core/RCTSettingsHeaders (= 0.61.1) - - React-RCTText (0.61.1): - - React-Core/RCTTextHeaders (= 0.61.1) - - React-RCTVibration (0.61.1): - - React-Core/RCTVibrationHeaders (= 0.61.1) - - ReactCommon/jscallinvoker (0.61.1): + - React-RCTActionSheet (0.61.3): + - React-Core/RCTActionSheetHeaders (= 0.61.3) + - React-RCTAnimation (0.61.3): + - React-Core/RCTAnimationHeaders (= 0.61.3) + - React-RCTBlob (0.61.3): + - React-Core/RCTBlobHeaders (= 0.61.3) + - React-Core/RCTWebSocket (= 0.61.3) + - React-jsi (= 0.61.3) + - React-RCTNetwork (= 0.61.3) + - React-RCTImage (0.61.3): + - React-Core/RCTImageHeaders (= 0.61.3) + - React-RCTNetwork (= 0.61.3) + - React-RCTLinking (0.61.3): + - React-Core/RCTLinkingHeaders (= 0.61.3) + - React-RCTNetwork (0.61.3): + - React-Core/RCTNetworkHeaders (= 0.61.3) + - React-RCTSettings (0.61.3): + - React-Core/RCTSettingsHeaders (= 0.61.3) + - React-RCTText (0.61.3): + - React-Core/RCTTextHeaders (= 0.61.3) + - React-RCTVibration (0.61.3): + - React-Core/RCTVibrationHeaders (= 0.61.3) + - ReactCommon/jscallinvoker (0.61.3): - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-cxxreact (= 0.61.1) - - ReactCommon/turbomodule/core (0.61.1): + - React-cxxreact (= 0.61.3) + - ReactCommon/turbomodule/core (0.61.3): - DoubleConversion - Folly (= 2018.10.22.00) - glog - - React-Core (= 0.61.1) - - React-cxxreact (= 0.61.1) - - React-jsi (= 0.61.1) - - ReactCommon/jscallinvoker (= 0.61.1) + - React-Core (= 0.61.3) + - React-cxxreact (= 0.61.3) + - React-jsi (= 0.61.3) + - ReactCommon/jscallinvoker (= 0.61.3) - rn-extensions-share (2.3.10): - React - - rn-fetch-blob (0.10.16): + - rn-fetch-blob (0.11.2): - React-Core - RNAudio (4.3.0): - React @@ -362,16 +366,18 @@ PODS: - Fabric - Firebase/Core - React - - RNGestureHandler (1.4.1): + - RNGestureHandler (1.5.0): - React - - RNImageCropPicker (0.25.0): + - RNImageCropPicker (0.26.1): - QBImagePickerController - React-Core - React-RCTImage - RSKImageCropper - - RNLocalize (1.1.4): + - RNLocalize (1.3.1): - React - - RNReanimated (1.3.0): + - RNReanimated (1.4.0): + - React + - RNRootView (1.0.3): - React - RNScreens (2.0.0-alpha.3): - React @@ -418,6 +424,7 @@ DEPENDENCIES: - Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - JitsiMeetSDK (from `https://github.com/RocketChat/jitsi-meet-ios-sdk-releases.git`) + - KeyCommands (from `../node_modules/react-native-keycommands`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - React (from `../node_modules/react-native/`) @@ -429,6 +436,7 @@ DEPENDENCIES: - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) + - react-native-appearance (from `../node_modules/react-native-appearance`) - react-native-background-timer (from `../node_modules/react-native-background-timer`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-jitsi-meet (from `../node_modules/react-native-jitsi-meet`) @@ -461,6 +469,7 @@ DEPENDENCIES: - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNLocalize (from `../node_modules/react-native-localize`) - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNRootView (from `../node_modules/rn-root-view`) - RNScreens (from `../node_modules/react-native-screens`) - RNUserDefaults (from `../node_modules/rn-user-defaults`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`) @@ -479,7 +488,7 @@ DEPENDENCIES: - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: - https://github.com/cocoapods/specs.git: + https://github.com/CocoaPods/Specs.git: - boost-for-react-native - Crashlytics - Fabric @@ -536,6 +545,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" JitsiMeetSDK: :git: https://github.com/RocketChat/jitsi-meet-ios-sdk-releases.git + KeyCommands: + :path: "../node_modules/react-native-keycommands" RCTRequired: :path: "../node_modules/react-native/Libraries/RCTRequired" RCTTypeSafety: @@ -554,6 +565,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: :path: "../node_modules/react-native/ReactCommon/jsinspector" + react-native-appearance: + :path: "../node_modules/react-native-appearance" react-native-background-timer: :path: "../node_modules/react-native-background-timer" react-native-document-picker: @@ -616,6 +629,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-localize" RNReanimated: :path: "../node_modules/react-native-reanimated" + RNRootView: + :path: "../node_modules/rn-root-view" RNScreens: :path: "../node_modules/react-native-screens" RNUserDefaults: @@ -668,7 +683,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c - BugsnagReactNative: 2114356c3acac0a71fb4b8962d3d1afdeb35f4d9 + BugsnagReactNative: 0a24a1dd2cac88862d67b938f809bec8274130a9 Crashlytics: 540b7e5f5da5a042647227a5e3ac51d85eed06df DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2 EXAppLoaderProvider: 7a8185228d8ba9e689a0e2d6d957fe9bdd49c8a0 @@ -679,8 +694,8 @@ SPEC CHECKSUMS: EXPermissions: 99e52dc3e5f8e55153f1958004f6df2a30a1f2f5 EXWebBrowser: def838b95aa9d396f9ce71ace4e614ee16e7ee30 Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 - FBLazyVector: 0846affdb2924b01093eb696766ecb0104e409e0 - FBReactNativeSpec: c4cf958af1b97799b524f63a26a1c509c0295b04 + FBLazyVector: 5bc5b1606fc9a7ac6956de049f6e30901ed31c49 + FBReactNativeSpec: f7be9bcc5ce259f7c39509f3f4caf59020d11d4c Firebase: 9cbe4e5b5eaafa05dc932be58b7c8c3820d71e88 FirebaseAnalytics: 843c7f64a8f9c79f0d03281197ebe7bb1d58d477 FirebaseCore: e9d9bd1dae61c1e82bc1e0e617a9d832392086a0 @@ -694,18 +709,20 @@ SPEC CHECKSUMS: GoogleDataTransportCCTSupport: 7455d07b98851aa63e4c05a34dad356ca588479e GoogleUtilities: 9c2c544202301110b29f7974a82e77fdcf12bf51 JitsiMeetSDK: d4a3aeed1a75fd57e6a78e5d202b6051dfcb9320 + KeyCommands: f66c535f698ed14b3d3a4e58859d79a827ea907e libwebp: 057912d6d0abfb6357d8bb05c0ea470301f5d61e nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48 QBImagePickerController: d54cf93db6decf26baf6ed3472f336ef35cae022 - RCTRequired: 53825815218847d3e9c7b6d92ad2d197a926d51e - RCTTypeSafety: d886540c518e53064dfa081bf7693fd650699b92 - React: 5dea58967c421bd1fdf6b94c18b9ed0f5134683c - React-Core: b381e65aa0da9b94b9dcdc4a99298075b1c3876c - React-CoreModules: 4ed224e29848ba76d26aacb8e3fe85712d3c4fe1 - React-cxxreact: 52c98f5c1fb4e4d9f4b588742718350a55f4f088 - React-jsi: 61ff417c95e6c3af50fb96399037e80752fb5ce7 - React-jsiexecutor: ee45274419eb95614bbbadb98e20684c5f29996e - React-jsinspector: 574d597112f9ea3d1b717f6fb62aef764c70dd6f + RCTRequired: a72523286ea3381f97b28d87529c265baad3ad7d + RCTTypeSafety: e3cc0537400222250f0be37bd69f4b339d3c0a0f + React: 3dc877fc32548b0c7108ca7f301466f4956cbff8 + React-Core: ca94e2e7d22cdcc266a405c4d2ad5e5675145776 + React-CoreModules: aa415458b5d7dacd10ac1b324d679f6e17cd8685 + React-cxxreact: bac5da3d62ee98abd3c1bf7338a7cc6205da7f69 + React-jsi: 8bcf5836caa8a759c135ab9ef97f3e023a7b94af + React-jsiexecutor: ae078e9df9c65bcdcf68f9a17656657932d95528 + React-jsinspector: a8939cc6909607eb5e8a5ecfff7c6226984e174d + react-native-appearance: 368f9d1160e3f1d7ecb5945e704affe018deef46 react-native-background-timer: 1b6e6b4e10f1b74c367a1fdc3c72b67c619b222b react-native-document-picker: c36bf5f067a581657ecaf7124dcd921a8be19061 react-native-jitsi-meet: 1ca89538b8ef3158bfd306d60325d7f11ca1451a @@ -713,30 +730,31 @@ SPEC CHECKSUMS: react-native-keyboard-tracking-view: 1ebd24a2b6ca2314549aa51775995678094bffa1 react-native-notifications: 163ddedac6fcc8d850ea15b06abdadcacdff00f1 react-native-orientation-locker: 23918c400376a7043e752c639c122fcf6bce8f1c - react-native-slider: ecc7f4cb0ccb9fa1346707ce76eb8618c9c2b598 + react-native-slider: 39208600e44f885e2d2c0510b5c6435a0f62d087 react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 - react-native-video: 6555881252c8ca039760e1cd6df28ac28ffb2baf - react-native-webview: 9f588ea09ede9bd1f5443f4aa4ddfadeb51fcd28 - React-RCTActionSheet: af4d951113b1e068bb30611f91b984a7a73597ff - React-RCTAnimation: 4f518d70bb6890b7c3d9d732f84786d6693ca297 - React-RCTBlob: 072a4888c08de0eef6d04eaa727d25e577e6ff26 - React-RCTImage: 78c5cdf1b2de6cd3cd650dd741868fad19a35528 - React-RCTLinking: 486ed1c9a659c7f9fea213868f8930b9a0a79f07 - React-RCTNetwork: e79599f3160b459da03447e32b8bcca1a0f0f797 - React-RCTSettings: 48b7c5a64ffe0c54c39d59eb7d9036e72305f95a - React-RCTText: 81b62b4e7f11531a5154e4daa5617670d5a2d5de - React-RCTVibration: 8be61459e3749d1fb02cf414edd05b3007622882 - ReactCommon: 4fba5be89efdf0b5720e0adb3d8d7edf6e532db0 + react-native-video: d01ed7ff1e38fa7dcc6c15c94cf505e661b7bfd0 + react-native-webview: 2aadbfef6b9eaa9e89b306ae3e31e6e870a6306d + React-RCTActionSheet: 94671eef55b01a93be735605822ef712d5ea208e + React-RCTAnimation: 524ae33e73de9c0fe6501a7a4bda8e01d26499d9 + React-RCTBlob: 5481c2db702f57207af7e7a9b32d90524b821b72 + React-RCTImage: b472cc0606f8a7c1ac270d6ccc57123a09439a32 + React-RCTLinking: 9cfc7bfdfda078489736695ac476de1f265b9f82 + React-RCTNetwork: 967547e4eeac92e55d41573a82da7fff4003052a + React-RCTSettings: 6ab7911172056b5077dacd9240f057eeeb1b121b + React-RCTText: b8f895b94aa0e7778fef28d13f3d71eed4a10c3d + React-RCTVibration: 262588c97551b0b1c675468cda857466ba5af18f + ReactCommon: c2c63d9290b422ca6ad5b3663073a015dd892ae9 rn-extensions-share: 4bfee75806ad54aadeff1dfa535697a6345a50b8 - rn-fetch-blob: 651b8d076b43d0d7aa294a3d9ec16c00aab8bef9 + rn-fetch-blob: f525a73a78df9ed5d35e67ea65e79d53c15255bc RNAudio: cae2991f2dccb75163f260b60da8051717b959fa RNDeviceInfo: 17e34f6dd902f08d88cbe2c0b7a01be948d43641 RNFastImage: 9b0c22643872bb7494c8d87bbbb66cc4c0d9e7a2 RNFirebase: ac0de8b24c6f91ae9459575491ed6a77327619c6 - RNGestureHandler: 4cb47a93019c1a201df2644413a0a1569a51c8aa - RNImageCropPicker: 0a731d984e64ee4c28bddaa7ce52262e4b80979f - RNLocalize: 62a949d2ec5bee0eb8f39a80a48f01e2f4f67080 - RNReanimated: 6abbbae2e5e72609d85aabd92a982a94566885f1 + RNGestureHandler: a4ddde1ffc6e590c8127b8b7eabfdade45475c74 + RNImageCropPicker: e1d8c3381e5b05a1bdcd13ea57a4f1c020a09cef + RNLocalize: 07eb7a91d10021cdf59d80061ebf3adb8a5b5688 + RNReanimated: b2ab0b693dddd2339bd2f300e770f6302d2e960c + RNRootView: 895a4813dedeaca82db2fa868ca1c333d790e494 RNScreens: 402a99b0a27c0c32f079cec12d3ccbd35e20cd7f RNUserDefaults: 8a4928443510aa99e4ccb3b53f1bf186593d690b RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4 @@ -755,8 +773,8 @@ SPEC CHECKSUMS: UMReactNativeAdapter: 131ea2b944ade8035f0b54c6570c405f6000548d UMSensorsInterface: 0ed023ce9b96f2ca6fada7bda05b7760da60b293 UMTaskManagerInterface: 8664abd37a00715727e60df9ecd65e42ba47b548 - Yoga: d8c572ddec8d05b7dba08e4e5f1924004a177078 + Yoga: 02036f6383c0008edb7ef0773a0e6beb6ce82bd1 -PODFILE CHECKSUM: 6f25ad58fac2bb13a90b79abad32d214a1bdb256 +PODFILE CHECKSUM: 18d0b080112c72e9cc76a381c1baba1172c6ca4d -COCOAPODS: 1.6.2 +COCOAPODS: 1.8.4 diff --git a/ios/Pods/Headers/Private/BugsnagReactNative/BSG_KSCrashIdentifier.h b/ios/Pods/Headers/Private/BugsnagReactNative/BSG_KSCrashIdentifier.h new file mode 120000 index 000000000..4ced5a652 --- /dev/null +++ b/ios/Pods/Headers/Private/BugsnagReactNative/BSG_KSCrashIdentifier.h @@ -0,0 +1 @@ +../../../../../node_modules/bugsnag-react-native/cocoa/vendor/bugsnag-cocoa/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashIdentifier.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/KeyCommands/RCTKeyCommandConstants.h b/ios/Pods/Headers/Private/KeyCommands/RCTKeyCommandConstants.h new file mode 120000 index 000000000..f413098ae --- /dev/null +++ b/ios/Pods/Headers/Private/KeyCommands/RCTKeyCommandConstants.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-keycommands/ios/KeyCommands/RCTKeyCommandConstants.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/KeyCommands/RCTKeyCommandsManager.h b/ios/Pods/Headers/Private/KeyCommands/RCTKeyCommandsManager.h new file mode 120000 index 000000000..3ddea8fb6 --- /dev/null +++ b/ios/Pods/Headers/Private/KeyCommands/RCTKeyCommandsManager.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-keycommands/ios/KeyCommands/RCTKeyCommandsManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/RNRootView/RootView.h b/ios/Pods/Headers/Private/RNRootView/RootView.h new file mode 120000 index 000000000..9ba63a4bb --- /dev/null +++ b/ios/Pods/Headers/Private/RNRootView/RootView.h @@ -0,0 +1 @@ +../../../../../node_modules/rn-root-view/ios/RootView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/Bitfield.h b/ios/Pods/Headers/Private/Yoga/yoga/Bitfield.h new file mode 120000 index 000000000..494037b5f --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/Bitfield.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/Bitfield.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/CompactValue.h b/ios/Pods/Headers/Private/Yoga/yoga/CompactValue.h new file mode 120000 index 000000000..5f9fcb501 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/CompactValue.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/CompactValue.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/Utils.h b/ios/Pods/Headers/Private/Yoga/yoga/Utils.h new file mode 120000 index 000000000..62eb08d8d --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/Utils.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/Utils.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGConfig.h b/ios/Pods/Headers/Private/Yoga/yoga/YGConfig.h new file mode 120000 index 000000000..e2ddc4b78 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGConfig.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGConfig.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGEnums.h b/ios/Pods/Headers/Private/Yoga/yoga/YGEnums.h new file mode 120000 index 000000000..fd4047d3c --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGEnums.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGEnums.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGFloatOptional.h b/ios/Pods/Headers/Private/Yoga/yoga/YGFloatOptional.h new file mode 120000 index 000000000..cb603e3e7 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGFloatOptional.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGFloatOptional.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGLayout.h b/ios/Pods/Headers/Private/Yoga/yoga/YGLayout.h new file mode 120000 index 000000000..f95b82a39 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGLayout.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGLayout.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGMacros.h b/ios/Pods/Headers/Private/Yoga/yoga/YGMacros.h new file mode 120000 index 000000000..b9f1940aa --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGMacros.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGMacros.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGNode.h b/ios/Pods/Headers/Private/Yoga/yoga/YGNode.h new file mode 120000 index 000000000..6dab868af --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGNode.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGNode.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGNodePrint.h b/ios/Pods/Headers/Private/Yoga/yoga/YGNodePrint.h new file mode 120000 index 000000000..fb4f6ebfa --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGNodePrint.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGNodePrint.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGStyle.h b/ios/Pods/Headers/Private/Yoga/yoga/YGStyle.h new file mode 120000 index 000000000..dfc50e60f --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGStyle.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGStyle.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/YGValue.h b/ios/Pods/Headers/Private/Yoga/yoga/YGValue.h new file mode 120000 index 000000000..868845773 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/YGValue.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGValue.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/Yoga-internal.h b/ios/Pods/Headers/Private/Yoga/yoga/Yoga-internal.h new file mode 120000 index 000000000..5b0d41ee2 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/Yoga-internal.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/Yoga-internal.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/Yoga.h b/ios/Pods/Headers/Private/Yoga/yoga/Yoga.h new file mode 120000 index 000000000..9ea8cc5f0 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/Yoga.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/Yoga.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/event.h b/ios/Pods/Headers/Private/Yoga/yoga/event.h new file mode 120000 index 000000000..eab97aef0 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/event.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/event/event.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/experiments-inl.h b/ios/Pods/Headers/Private/Yoga/yoga/experiments-inl.h new file mode 120000 index 000000000..c3d7802de --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/experiments-inl.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/internal/experiments-inl.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/experiments.h b/ios/Pods/Headers/Private/Yoga/yoga/experiments.h new file mode 120000 index 000000000..d255008f2 --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/experiments.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/internal/experiments.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/Yoga/yoga/log.h b/ios/Pods/Headers/Private/Yoga/yoga/log.h new file mode 120000 index 000000000..658bfcabd --- /dev/null +++ b/ios/Pods/Headers/Private/Yoga/yoga/log.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/log.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-appearance/RNCAppearance.h b/ios/Pods/Headers/Private/react-native-appearance/RNCAppearance.h new file mode 120000 index 000000000..c720d54c5 --- /dev/null +++ b/ios/Pods/Headers/Private/react-native-appearance/RNCAppearance.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-appearance/ios/Appearance/RNCAppearance.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-appearance/RNCAppearanceProvider.h b/ios/Pods/Headers/Private/react-native-appearance/RNCAppearanceProvider.h new file mode 120000 index 000000000..e20e9a22c --- /dev/null +++ b/ios/Pods/Headers/Private/react-native-appearance/RNCAppearanceProvider.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-appearance/ios/Appearance/RNCAppearanceProvider.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-appearance/RNCAppearanceProviderManager.h b/ios/Pods/Headers/Private/react-native-appearance/RNCAppearanceProviderManager.h new file mode 120000 index 000000000..5e0095d4a --- /dev/null +++ b/ios/Pods/Headers/Private/react-native-appearance/RNCAppearanceProviderManager.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-appearance/ios/Appearance/RNCAppearanceProviderManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-webview/RNCUIWebView.h b/ios/Pods/Headers/Private/react-native-webview/RNCUIWebView.h deleted file mode 120000 index 8cc7df66a..000000000 --- a/ios/Pods/Headers/Private/react-native-webview/RNCUIWebView.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCUIWebView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-webview/RNCUIWebViewManager.h b/ios/Pods/Headers/Private/react-native-webview/RNCUIWebViewManager.h deleted file mode 120000 index 842e4fdc6..000000000 --- a/ios/Pods/Headers/Private/react-native-webview/RNCUIWebViewManager.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCUIWebViewManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-webview/RNCWKWebView.h b/ios/Pods/Headers/Private/react-native-webview/RNCWKWebView.h deleted file mode 120000 index 939a42464..000000000 --- a/ios/Pods/Headers/Private/react-native-webview/RNCWKWebView.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCWKWebView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-webview/RNCWKWebViewManager.h b/ios/Pods/Headers/Private/react-native-webview/RNCWKWebViewManager.h deleted file mode 120000 index 03f019f16..000000000 --- a/ios/Pods/Headers/Private/react-native-webview/RNCWKWebViewManager.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCWKWebViewManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-webview/RNCWebView.h b/ios/Pods/Headers/Private/react-native-webview/RNCWebView.h new file mode 120000 index 000000000..c814fcebc --- /dev/null +++ b/ios/Pods/Headers/Private/react-native-webview/RNCWebView.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-webview/ios/RNCWebView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Private/react-native-webview/RNCWebViewManager.h b/ios/Pods/Headers/Private/react-native-webview/RNCWebViewManager.h new file mode 120000 index 000000000..c20dd096e --- /dev/null +++ b/ios/Pods/Headers/Private/react-native-webview/RNCWebViewManager.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-webview/ios/RNCWebViewManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/KeyCommands/RCTKeyCommandConstants.h b/ios/Pods/Headers/Public/KeyCommands/RCTKeyCommandConstants.h new file mode 120000 index 000000000..f413098ae --- /dev/null +++ b/ios/Pods/Headers/Public/KeyCommands/RCTKeyCommandConstants.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-keycommands/ios/KeyCommands/RCTKeyCommandConstants.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/KeyCommands/RCTKeyCommandsManager.h b/ios/Pods/Headers/Public/KeyCommands/RCTKeyCommandsManager.h new file mode 120000 index 000000000..3ddea8fb6 --- /dev/null +++ b/ios/Pods/Headers/Public/KeyCommands/RCTKeyCommandsManager.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-keycommands/ios/KeyCommands/RCTKeyCommandsManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/RNRootView/RootView.h b/ios/Pods/Headers/Public/RNRootView/RootView.h new file mode 120000 index 000000000..9ba63a4bb --- /dev/null +++ b/ios/Pods/Headers/Public/RNRootView/RootView.h @@ -0,0 +1 @@ +../../../../../node_modules/rn-root-view/ios/RootView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/Yoga/yoga/YGEnums.h b/ios/Pods/Headers/Public/Yoga/yoga/YGEnums.h new file mode 120000 index 000000000..fd4047d3c --- /dev/null +++ b/ios/Pods/Headers/Public/Yoga/yoga/YGEnums.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGEnums.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/Yoga/yoga/YGMacros.h b/ios/Pods/Headers/Public/Yoga/yoga/YGMacros.h new file mode 120000 index 000000000..b9f1940aa --- /dev/null +++ b/ios/Pods/Headers/Public/Yoga/yoga/YGMacros.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGMacros.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/Yoga/yoga/YGValue.h b/ios/Pods/Headers/Public/Yoga/yoga/YGValue.h new file mode 120000 index 000000000..868845773 --- /dev/null +++ b/ios/Pods/Headers/Public/Yoga/yoga/YGValue.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/YGValue.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/Yoga/yoga/Yoga.h b/ios/Pods/Headers/Public/Yoga/yoga/Yoga.h new file mode 120000 index 000000000..9ea8cc5f0 --- /dev/null +++ b/ios/Pods/Headers/Public/Yoga/yoga/Yoga.h @@ -0,0 +1 @@ +../../../../../../node_modules/react-native/ReactCommon/yoga/yoga/Yoga.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-appearance/RNCAppearance.h b/ios/Pods/Headers/Public/react-native-appearance/RNCAppearance.h new file mode 120000 index 000000000..c720d54c5 --- /dev/null +++ b/ios/Pods/Headers/Public/react-native-appearance/RNCAppearance.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-appearance/ios/Appearance/RNCAppearance.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-appearance/RNCAppearanceProvider.h b/ios/Pods/Headers/Public/react-native-appearance/RNCAppearanceProvider.h new file mode 120000 index 000000000..e20e9a22c --- /dev/null +++ b/ios/Pods/Headers/Public/react-native-appearance/RNCAppearanceProvider.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-appearance/ios/Appearance/RNCAppearanceProvider.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-appearance/RNCAppearanceProviderManager.h b/ios/Pods/Headers/Public/react-native-appearance/RNCAppearanceProviderManager.h new file mode 120000 index 000000000..5e0095d4a --- /dev/null +++ b/ios/Pods/Headers/Public/react-native-appearance/RNCAppearanceProviderManager.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-appearance/ios/Appearance/RNCAppearanceProviderManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-webview/RNCUIWebView.h b/ios/Pods/Headers/Public/react-native-webview/RNCUIWebView.h deleted file mode 120000 index 8cc7df66a..000000000 --- a/ios/Pods/Headers/Public/react-native-webview/RNCUIWebView.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCUIWebView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-webview/RNCUIWebViewManager.h b/ios/Pods/Headers/Public/react-native-webview/RNCUIWebViewManager.h deleted file mode 120000 index 842e4fdc6..000000000 --- a/ios/Pods/Headers/Public/react-native-webview/RNCUIWebViewManager.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCUIWebViewManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-webview/RNCWKWebView.h b/ios/Pods/Headers/Public/react-native-webview/RNCWKWebView.h deleted file mode 120000 index 939a42464..000000000 --- a/ios/Pods/Headers/Public/react-native-webview/RNCWKWebView.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCWKWebView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-webview/RNCWKWebViewManager.h b/ios/Pods/Headers/Public/react-native-webview/RNCWKWebViewManager.h deleted file mode 120000 index 03f019f16..000000000 --- a/ios/Pods/Headers/Public/react-native-webview/RNCWKWebViewManager.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../node_modules/react-native-webview/ios/RNCWKWebViewManager.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-webview/RNCWebView.h b/ios/Pods/Headers/Public/react-native-webview/RNCWebView.h new file mode 120000 index 000000000..c814fcebc --- /dev/null +++ b/ios/Pods/Headers/Public/react-native-webview/RNCWebView.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-webview/ios/RNCWebView.h \ No newline at end of file diff --git a/ios/Pods/Headers/Public/react-native-webview/RNCWebViewManager.h b/ios/Pods/Headers/Public/react-native-webview/RNCWebViewManager.h new file mode 120000 index 000000000..c20dd096e --- /dev/null +++ b/ios/Pods/Headers/Public/react-native-webview/RNCWebViewManager.h @@ -0,0 +1 @@ +../../../../../node_modules/react-native-webview/ios/RNCWebViewManager.h \ No newline at end of file diff --git a/ios/Pods/Local Podspecs/BugsnagReactNative.podspec.json b/ios/Pods/Local Podspecs/BugsnagReactNative.podspec.json index c91322299..5412ef459 100644 --- a/ios/Pods/Local Podspecs/BugsnagReactNative.podspec.json +++ b/ios/Pods/Local Podspecs/BugsnagReactNative.podspec.json @@ -1,6 +1,6 @@ { "name": "BugsnagReactNative", - "version": "2.22.4", + "version": "2.23.2", "license": "MIT", "summary": "Bugsnag crash and error reporting for React Native apps", "authors": { @@ -9,7 +9,7 @@ "homepage": "https://docs.bugsnag.com/platforms/react-native", "source": { "git": "https://github.com/bugsnag/bugsnag-react-native.git", - "tag": "v2.22.4" + "tag": "v2.23.2" }, "platforms": { "ios": "8.0" diff --git a/ios/Pods/Local Podspecs/FBLazyVector.podspec.json b/ios/Pods/Local Podspecs/FBLazyVector.podspec.json index af1743b69..8c140ca39 100644 --- a/ios/Pods/Local Podspecs/FBLazyVector.podspec.json +++ b/ios/Pods/Local Podspecs/FBLazyVector.podspec.json @@ -1,6 +1,6 @@ { "name": "FBLazyVector", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "**/*.{c,h,m,mm,cpp}", "header_dir": "FBLazyVector" diff --git a/ios/Pods/Local Podspecs/FBReactNativeSpec.podspec.json b/ios/Pods/Local Podspecs/FBReactNativeSpec.podspec.json index 3508615a6..01a90ecfa 100644 --- a/ios/Pods/Local Podspecs/FBReactNativeSpec.podspec.json +++ b/ios/Pods/Local Podspecs/FBReactNativeSpec.podspec.json @@ -1,6 +1,6 @@ { "name": "FBReactNativeSpec", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -12,7 +12,7 @@ "compiler_flags": "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32 -Wno-nullability-completeness", "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "**/*.{c,h,m,mm,cpp}", "header_dir": "FBReactNativeSpec", @@ -26,19 +26,19 @@ "2018.10.22.00" ], "RCTRequired": [ - "0.61.1" + "0.61.3" ], "RCTTypeSafety": [ - "0.61.1" + "0.61.3" ], "React-Core": [ - "0.61.1" + "0.61.3" ], "React-jsi": [ - "0.61.1" + "0.61.3" ], "ReactCommon/turbomodule/core": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/KeyCommands.podspec.json b/ios/Pods/Local Podspecs/KeyCommands.podspec.json new file mode 100644 index 000000000..8c64f2a50 --- /dev/null +++ b/ios/Pods/Local Podspecs/KeyCommands.podspec.json @@ -0,0 +1,21 @@ +{ + "name": "KeyCommands", + "version": "2.0.3", + "summary": "iOS UIKeyCommands.", + "description": "iOS UIKeyCommands.", + "license": "MIT", + "authors": "djorkaeffalexandre", + "homepage": "https://github.com/RocketChat/react-native-keycommands", + "source": { + "git": "https://github.com/RocketChat/react-native-keycommands.git" + }, + "platforms": { + "ios": "7.0" + }, + "source_files": "ios/**/*.{h,m}", + "dependencies": { + "React": [ + + ] + } +} diff --git a/ios/Pods/Local Podspecs/RCTRequired.podspec.json b/ios/Pods/Local Podspecs/RCTRequired.podspec.json index 4fb7ebbd1..5c3f65c46 100644 --- a/ios/Pods/Local Podspecs/RCTRequired.podspec.json +++ b/ios/Pods/Local Podspecs/RCTRequired.podspec.json @@ -1,6 +1,6 @@ { "name": "RCTRequired", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "**/*.{c,h,m,mm,cpp}", "header_dir": "RCTRequired" diff --git a/ios/Pods/Local Podspecs/RCTTypeSafety.podspec.json b/ios/Pods/Local Podspecs/RCTTypeSafety.podspec.json index d8bb57473..1022f2407 100644 --- a/ios/Pods/Local Podspecs/RCTTypeSafety.podspec.json +++ b/ios/Pods/Local Podspecs/RCTTypeSafety.podspec.json @@ -1,6 +1,6 @@ { "name": "RCTTypeSafety", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -12,7 +12,7 @@ "compiler_flags": "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32", "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "**/*.{c,h,m,mm,cpp}", "header_dir": "RCTTypeSafety", @@ -23,16 +23,16 @@ }, "dependencies": { "FBLazyVector": [ - "0.61.1" + "0.61.3" ], "Folly": [ "2018.10.22.00" ], "RCTRequired": [ - "0.61.1" + "0.61.3" ], "React-Core": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/RNGestureHandler.podspec.json b/ios/Pods/Local Podspecs/RNGestureHandler.podspec.json index 43d1d84f4..28266ad46 100644 --- a/ios/Pods/Local Podspecs/RNGestureHandler.podspec.json +++ b/ios/Pods/Local Podspecs/RNGestureHandler.podspec.json @@ -1,6 +1,6 @@ { "name": "RNGestureHandler", - "version": "1.4.1", + "version": "1.5.0", "summary": "Experimental implementation of a new declarative API for gesture handling in react-native", "homepage": "https://github.com/kmagiera/react-native-gesture-handler", "license": "MIT", @@ -13,7 +13,7 @@ }, "source": { "git": "https://github.com/kmagiera/react-native-gesture-handler", - "tag": "1.4.1" + "tag": "1.5.0" }, "source_files": "ios/**/*.{h,m}", "dependencies": { diff --git a/ios/Pods/Local Podspecs/RNImageCropPicker.podspec.json b/ios/Pods/Local Podspecs/RNImageCropPicker.podspec.json index 1a7f4e913..7927ba420 100644 --- a/ios/Pods/Local Podspecs/RNImageCropPicker.podspec.json +++ b/ios/Pods/Local Podspecs/RNImageCropPicker.podspec.json @@ -1,6 +1,6 @@ { "name": "RNImageCropPicker", - "version": "0.25.0", + "version": "0.26.1", "summary": "Select single or multiple images, with cropping option", "requires_arc": true, "license": "MIT", diff --git a/ios/Pods/Local Podspecs/RNLocalize.podspec.json b/ios/Pods/Local Podspecs/RNLocalize.podspec.json index b0d6fd726..c9bb9f2ad 100644 --- a/ios/Pods/Local Podspecs/RNLocalize.podspec.json +++ b/ios/Pods/Local Podspecs/RNLocalize.podspec.json @@ -5,18 +5,19 @@ ] }, - "version": "1.1.4", + "version": "1.3.1", "license": "MIT", - "description": "A toolbox for your React Native app localization.", "summary": "A toolbox for your React Native app localization.", "authors": "Mathieu Acthernoene ", "homepage": "https://github.com/react-native-community/react-native-localize", "platforms": { - "ios": "9.0" + "ios": "9.0", + "tvos": "11.0" }, + "requires_arc": true, "source": { "git": "https://github.com/react-native-community/react-native-localize.git", - "tag": "1.1.4" + "tag": "1.3.1" }, "source_files": "ios/*.{h,m}" } diff --git a/ios/Pods/Local Podspecs/RNReanimated.podspec.json b/ios/Pods/Local Podspecs/RNReanimated.podspec.json index 9e510bf32..5f7a608b8 100644 --- a/ios/Pods/Local Podspecs/RNReanimated.podspec.json +++ b/ios/Pods/Local Podspecs/RNReanimated.podspec.json @@ -1,6 +1,6 @@ { "name": "RNReanimated", - "version": "1.3.0", + "version": "1.4.0", "summary": "More powerful alternative to Animated library for React Native.", "description": "RNReanimated", "homepage": "https://github.com/kmagiera/react-native-reanimated", @@ -9,11 +9,12 @@ "author": "author@domain.cn" }, "platforms": { - "ios": "9.0" + "ios": "9.0", + "tvos": "9.0" }, "source": { "git": "https://github.com/kmagiera/react-native-reanimated.git", - "tag": "1.3.0" + "tag": "1.4.0" }, "source_files": "ios/**/*.{h,m}", "requires_arc": true, diff --git a/ios/Pods/Local Podspecs/RNRootView.podspec.json b/ios/Pods/Local Podspecs/RNRootView.podspec.json new file mode 100644 index 000000000..72c71753a --- /dev/null +++ b/ios/Pods/Local Podspecs/RNRootView.podspec.json @@ -0,0 +1,31 @@ +{ + "name": "RNRootView", + "version": "1.0.3", + "summary": "React Native Root View", + "description": "Change Root View Background color on React Native", + "license": "MIT", + "authors": { + "name": "Djorkaeff Alexandre", + "email": "djorkaeff.alexandre@rocket.chat" + }, + "homepage": "https://github.com/RocketChat/rn-root-view", + "source": { + "git": "https://github.com/RocketChat/rn-root-view.git", + "tag": "master" + }, + "requires_arc": true, + "platforms": { + "ios": "7.0" + }, + "preserve_paths": [ + "README.md", + "package.json", + "index.js" + ], + "source_files": "iOS/*.{h,m}", + "dependencies": { + "React": [ + + ] + } +} diff --git a/ios/Pods/Local Podspecs/React-Core.podspec.json b/ios/Pods/Local Podspecs/React-Core.podspec.json index 02d8cda1d..d3a5130ce 100644 --- a/ios/Pods/Local Podspecs/React-Core.podspec.json +++ b/ios/Pods/Local Podspecs/React-Core.podspec.json @@ -1,6 +1,6 @@ { "name": "React-Core", - "version": "0.61.1", + "version": "0.61.3", "summary": "The core of React Native.", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "compiler_flags": "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32 -Wno-documentation", "header_dir": "React", @@ -29,13 +29,13 @@ "2018.10.22.00" ], "React-cxxreact": [ - "0.61.1" + "0.61.3" ], "React-jsi": [ - "0.61.1" + "0.61.3" ], "React-jsiexecutor": [ - "0.61.1" + "0.61.3" ], "Yoga": [ @@ -78,13 +78,13 @@ ], "dependencies": { "React-Core/Default": [ - "0.61.1" + "0.61.3" ], "React-Core/RCTWebSocket": [ - "0.61.1" + "0.61.3" ], "React-jsinspector": [ - "0.61.1" + "0.61.3" ] } }, @@ -93,7 +93,7 @@ "source_files": "Libraries/WebSocket/*.{h,m}", "dependencies": { "React-Core/Default": [ - "0.61.1" + "0.61.3" ] } }, diff --git a/ios/Pods/Local Podspecs/React-CoreModules.podspec.json b/ios/Pods/Local Podspecs/React-CoreModules.podspec.json index 1e8948d2f..b26f314b5 100644 --- a/ios/Pods/Local Podspecs/React-CoreModules.podspec.json +++ b/ios/Pods/Local Podspecs/React-CoreModules.podspec.json @@ -1,6 +1,6 @@ { "name": "React-CoreModules", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -12,7 +12,7 @@ "compiler_flags": "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32 -Wno-nullability-completeness", "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "**/*.{c,m,mm,cpp}", "header_dir": "CoreModules", @@ -23,22 +23,22 @@ }, "dependencies": { "FBReactNativeSpec": [ - "0.61.1" + "0.61.3" ], "RCTTypeSafety": [ - "0.61.1" + "0.61.3" ], "React-RCTImage": [ - "0.61.1" + "0.61.3" ], "Folly": [ "2018.10.22.00" ], "React-Core/CoreModulesHeaders": [ - "0.61.1" + "0.61.3" ], "ReactCommon/turbomodule/core": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTActionSheet.podspec.json b/ios/Pods/Local Podspecs/React-RCTActionSheet.podspec.json index e3a2cd645..07ca9da9b 100644 --- a/ios/Pods/Local Podspecs/React-RCTActionSheet.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTActionSheet.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTActionSheet", - "version": "0.61.1", + "version": "0.61.3", "summary": "An API for displaying iOS action sheets and share sheets.", "homepage": "http://facebook.github.io/react-native/", "documentation_url": "https://facebook.github.io/react-native/docs/actionsheetios", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{m}", "preserve_paths": [ @@ -23,7 +23,7 @@ "header_dir": "RCTActionSheet", "dependencies": { "React-Core/RCTActionSheetHeaders": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTAnimation.podspec.json b/ios/Pods/Local Podspecs/React-RCTAnimation.podspec.json index 40a673757..f0d5f8afb 100644 --- a/ios/Pods/Local Podspecs/React-RCTAnimation.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTAnimation.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTAnimation", - "version": "0.61.1", + "version": "0.61.3", "summary": "A native driver for the Animated API.", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "{Drivers/*,Nodes/*,*}.{m}", "preserve_paths": [ @@ -22,7 +22,7 @@ "header_dir": "RCTAnimation", "dependencies": { "React-Core/RCTAnimationHeaders": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTBlob.podspec.json b/ios/Pods/Local Podspecs/React-RCTBlob.podspec.json index d36190d25..aaf9800e2 100644 --- a/ios/Pods/Local Podspecs/React-RCTBlob.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTBlob.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTBlob", - "version": "0.61.1", + "version": "0.61.3", "summary": "An API for displaying iOS action sheets and share sheets.", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": [ "*.{m,mm}", @@ -25,16 +25,16 @@ "header_dir": "RCTBlob", "dependencies": { "React-Core/RCTBlobHeaders": [ - "0.61.1" + "0.61.3" ], "React-Core/RCTWebSocket": [ - "0.61.1" + "0.61.3" ], "React-RCTNetwork": [ - "0.61.1" + "0.61.3" ], "React-jsi": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTImage.podspec.json b/ios/Pods/Local Podspecs/React-RCTImage.podspec.json index f13557b19..48d0c64e0 100644 --- a/ios/Pods/Local Podspecs/React-RCTImage.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTImage.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTImage", - "version": "0.61.1", + "version": "0.61.3", "summary": "A React component for displaying different types of images.", "homepage": "http://facebook.github.io/react-native/", "documentation_url": "https://facebook.github.io/react-native/docs/image", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{m}", "preserve_paths": [ @@ -23,10 +23,10 @@ "header_dir": "RCTImage", "dependencies": { "React-Core/RCTImageHeaders": [ - "0.61.1" + "0.61.3" ], "React-RCTNetwork": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTLinking.podspec.json b/ios/Pods/Local Podspecs/React-RCTLinking.podspec.json index 5a08f67f9..0541f5ba5 100644 --- a/ios/Pods/Local Podspecs/React-RCTLinking.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTLinking.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTLinking", - "version": "0.61.1", + "version": "0.61.3", "summary": "A general interface to interact with both incoming and outgoing app links.", "homepage": "http://facebook.github.io/react-native/", "documentation_url": "https://facebook.github.io/react-native/docs/linking", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{m}", "preserve_paths": [ @@ -23,7 +23,7 @@ "header_dir": "RCTLinking", "dependencies": { "React-Core/RCTLinkingHeaders": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTNetwork.podspec.json b/ios/Pods/Local Podspecs/React-RCTNetwork.podspec.json index 045331296..7cb7b473e 100644 --- a/ios/Pods/Local Podspecs/React-RCTNetwork.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTNetwork.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTNetwork", - "version": "0.61.1", + "version": "0.61.3", "summary": "The networking library of React Native.", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{m,mm}", "preserve_paths": [ @@ -22,7 +22,7 @@ "header_dir": "RCTNetwork", "dependencies": { "React-Core/RCTNetworkHeaders": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTSettings.podspec.json b/ios/Pods/Local Podspecs/React-RCTSettings.podspec.json index eff674554..58107e855 100644 --- a/ios/Pods/Local Podspecs/React-RCTSettings.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTSettings.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTSettings", - "version": "0.61.1", + "version": "0.61.3", "summary": "A wrapper for NSUserDefaults, a persistent key-value store available only on iOS.", "homepage": "http://facebook.github.io/react-native/", "documentation_url": "https://facebook.github.io/react-native/docs/settings", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{m}", "preserve_paths": [ @@ -23,7 +23,7 @@ "header_dir": "RCTSettings", "dependencies": { "React-Core/RCTSettingsHeaders": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTText.podspec.json b/ios/Pods/Local Podspecs/React-RCTText.podspec.json index 38196fff9..327b66b32 100644 --- a/ios/Pods/Local Podspecs/React-RCTText.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTText.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTText", - "version": "0.61.1", + "version": "0.61.3", "summary": "A React component for displaying text.", "homepage": "http://facebook.github.io/react-native/", "documentation_url": "https://facebook.github.io/react-native/docs/text", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "**/*.{h,m}", "preserve_paths": [ @@ -23,7 +23,7 @@ "header_dir": "RCTText", "dependencies": { "React-Core/RCTTextHeaders": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-RCTVibration.podspec.json b/ios/Pods/Local Podspecs/React-RCTVibration.podspec.json index dce4cff4a..baf39c38f 100644 --- a/ios/Pods/Local Podspecs/React-RCTVibration.podspec.json +++ b/ios/Pods/Local Podspecs/React-RCTVibration.podspec.json @@ -1,6 +1,6 @@ { "name": "React-RCTVibration", - "version": "0.61.1", + "version": "0.61.3", "summary": "An API for controlling the vibration hardware of the device.", "homepage": "http://facebook.github.io/react-native/", "documentation_url": "https://facebook.github.io/react-native/docs/vibration", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{m}", "preserve_paths": [ @@ -23,7 +23,7 @@ "header_dir": "RCTVibration", "dependencies": { "React-Core/RCTVibrationHeaders": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-cxxreact.podspec.json b/ios/Pods/Local Podspecs/React-cxxreact.podspec.json index 10de8adf2..472fa710a 100644 --- a/ios/Pods/Local Podspecs/React-cxxreact.podspec.json +++ b/ios/Pods/Local Podspecs/React-cxxreact.podspec.json @@ -1,6 +1,6 @@ { "name": "React-cxxreact", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{cpp,h}", "exclude_files": "SampleCxxModule.*", @@ -34,7 +34,7 @@ ], "React-jsinspector": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/React-jsi.podspec.json b/ios/Pods/Local Podspecs/React-jsi.podspec.json index 3853cae04..a436429e2 100644 --- a/ios/Pods/Local Podspecs/React-jsi.podspec.json +++ b/ios/Pods/Local Podspecs/React-jsi.podspec.json @@ -1,6 +1,6 @@ { "name": "React-jsi", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "**/*.{cpp,h}", "exclude_files": "**/test/*", diff --git a/ios/Pods/Local Podspecs/React-jsiexecutor.podspec.json b/ios/Pods/Local Podspecs/React-jsiexecutor.podspec.json index e9630daa4..b9b98da5a 100644 --- a/ios/Pods/Local Podspecs/React-jsiexecutor.podspec.json +++ b/ios/Pods/Local Podspecs/React-jsiexecutor.podspec.json @@ -1,6 +1,6 @@ { "name": "React-jsiexecutor", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "jsireact/*.{cpp,h}", "compiler_flags": "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32 -Wno-documentation", @@ -21,10 +21,10 @@ "header_dir": "jsireact", "dependencies": { "React-cxxreact": [ - "0.61.1" + "0.61.3" ], "React-jsi": [ - "0.61.1" + "0.61.3" ], "Folly": [ "2018.10.22.00" diff --git a/ios/Pods/Local Podspecs/React-jsinspector.podspec.json b/ios/Pods/Local Podspecs/React-jsinspector.podspec.json index b5c4c05e8..a72764c90 100644 --- a/ios/Pods/Local Podspecs/React-jsinspector.podspec.json +++ b/ios/Pods/Local Podspecs/React-jsinspector.podspec.json @@ -1,6 +1,6 @@ { "name": "React-jsinspector", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -11,7 +11,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "source_files": "*.{cpp,h}", "header_dir": "jsinspector" diff --git a/ios/Pods/Local Podspecs/React.podspec.json b/ios/Pods/Local Podspecs/React.podspec.json index 97bfc8257..94a31602e 100644 --- a/ios/Pods/Local Podspecs/React.podspec.json +++ b/ios/Pods/Local Podspecs/React.podspec.json @@ -1,6 +1,6 @@ { "name": "React", - "version": "0.61.1", + "version": "0.61.3", "summary": "A framework for building native apps using React", "description": "React Native apps are built using the React JS\nframework, and render directly to native UIKit\nelements using a fully asynchronous architecture.\nThere is no browser and no HTML. We have picked what\nwe think is the best set of features from these and\nother technologies to build what we hope to become\nthe best product development framework available,\nwith an emphasis on iteration speed, developer\ndelight, continuity of technology, and absolutely\nbeautiful and fast products with no compromises in\nquality or capability.", "homepage": "http://facebook.github.io/react-native/", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "preserve_paths": [ "package.json", @@ -22,40 +22,40 @@ "cocoapods_version": ">= 1.2.0", "dependencies": { "React-Core": [ - "0.61.1" + "0.61.3" ], "React-Core/DevSupport": [ - "0.61.1" + "0.61.3" ], "React-Core/RCTWebSocket": [ - "0.61.1" + "0.61.3" ], "React-RCTActionSheet": [ - "0.61.1" + "0.61.3" ], "React-RCTAnimation": [ - "0.61.1" + "0.61.3" ], "React-RCTBlob": [ - "0.61.1" + "0.61.3" ], "React-RCTImage": [ - "0.61.1" + "0.61.3" ], "React-RCTLinking": [ - "0.61.1" + "0.61.3" ], "React-RCTNetwork": [ - "0.61.1" + "0.61.3" ], "React-RCTSettings": [ - "0.61.1" + "0.61.3" ], "React-RCTText": [ - "0.61.1" + "0.61.3" ], "React-RCTVibration": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/ReactCommon.podspec.json b/ios/Pods/Local Podspecs/ReactCommon.podspec.json index a2887c013..d4450ee0e 100644 --- a/ios/Pods/Local Podspecs/ReactCommon.podspec.json +++ b/ios/Pods/Local Podspecs/ReactCommon.podspec.json @@ -1,7 +1,7 @@ { "name": "ReactCommon", "module_name": "ReactCommon", - "version": "0.61.1", + "version": "0.61.3", "summary": "-", "homepage": "http://facebook.github.io/react-native/", "license": "MIT", @@ -12,7 +12,7 @@ }, "source": { "git": "https://github.com/facebook/react-native.git", - "tag": "v0.61.1" + "tag": "v0.61.3" }, "header_dir": "ReactCommon", "compiler_flags": "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32 -Wno-documentation", @@ -27,7 +27,7 @@ "source_files": "jscallinvoker/**/*.{cpp,h}", "dependencies": { "React-cxxreact": [ - "0.61.1" + "0.61.3" ], "DoubleConversion": [ @@ -44,16 +44,16 @@ "name": "turbomodule", "dependencies": { "ReactCommon/jscallinvoker": [ - "0.61.1" + "0.61.3" ], "React-Core": [ - "0.61.1" + "0.61.3" ], "React-cxxreact": [ - "0.61.1" + "0.61.3" ], "React-jsi": [ - "0.61.1" + "0.61.3" ], "Folly": [ "2018.10.22.00" @@ -81,7 +81,7 @@ ], "dependencies": { "ReactCommon/turbomodule/core": [ - "0.61.1" + "0.61.3" ] } } diff --git a/ios/Pods/Local Podspecs/Yoga.podspec.json b/ios/Pods/Local Podspecs/Yoga.podspec.json new file mode 100644 index 000000000..eea9e63a5 --- /dev/null +++ b/ios/Pods/Local Podspecs/Yoga.podspec.json @@ -0,0 +1,33 @@ +{ + "name": "Yoga", + "version": "1.14.0", + "license": { + "type": "MIT" + }, + "homepage": "https://yogalayout.com", + "documentation_url": "https://yogalayout.com/docs/", + "summary": "Yoga is a cross-platform layout engine which implements Flexbox.", + "description": "Yoga is a cross-platform layout engine enabling maximum collaboration within your team by implementing an API many designers are familiar with, and opening it up to developers across different platforms.", + "authors": "Facebook", + "source": { + "git": "https://github.com/facebook/react-native.git", + "tag": "v0.61.3" + }, + "module_name": "yoga", + "header_dir": "yoga", + "requires_arc": false, + "compiler_flags": [ + "-fno-omit-frame-pointer", + "-fexceptions", + "-Wall", + "-Werror", + "-std=c++1y", + "-fPIC" + ], + "platforms": { + "ios": "9.0", + "tvos": "9.2" + }, + "source_files": "yoga/**/*.{cpp,h}", + "public_header_files": "yoga/{Yoga,YGEnums,YGMacros,YGValue}.h" +} diff --git a/ios/Pods/Local Podspecs/react-native-appearance.podspec.json b/ios/Pods/Local Podspecs/react-native-appearance.podspec.json new file mode 100644 index 000000000..29e78eef7 --- /dev/null +++ b/ios/Pods/Local Podspecs/react-native-appearance.podspec.json @@ -0,0 +1,22 @@ +{ + "name": "react-native-appearance", + "version": "0.3.1", + "summary": "Polyfill for `Appearance` API which will be available in `react-native@0.62`.", + "license": "MIT", + "authors": "Brent Vatne ", + "homepage": "https://github.com/expo/react-native-appearance#readme", + "platforms": { + "ios": "9.0", + "tvos": "9.2" + }, + "source": { + "git": "https://github.com/expo/react-native-appearance.git", + "tag": "v0.3.1" + }, + "source_files": "ios/**/*.{h,m}", + "dependencies": { + "React": [ + + ] + } +} diff --git a/ios/Pods/Local Podspecs/react-native-slider.podspec.json b/ios/Pods/Local Podspecs/react-native-slider.podspec.json index f8c7140f0..511495dd4 100644 --- a/ios/Pods/Local Podspecs/react-native-slider.podspec.json +++ b/ios/Pods/Local Podspecs/react-native-slider.podspec.json @@ -1,6 +1,6 @@ { "name": "react-native-slider", - "version": "2.0.1", + "version": "2.0.5", "summary": "React Native component used to select a single value from a range of values.", "license": "MIT", "authors": "react-native-community", @@ -10,7 +10,7 @@ }, "source": { "git": "https://github.com/react-native-community/react-native-slider.git", - "tag": "v2.0.1" + "tag": "v2.0.5" }, "source_files": "ios/**/*.{h,m}", "dependencies": { diff --git a/ios/Pods/Local Podspecs/react-native-video.podspec.json b/ios/Pods/Local Podspecs/react-native-video.podspec.json index 65d476f8d..a30e87e7c 100644 --- a/ios/Pods/Local Podspecs/react-native-video.podspec.json +++ b/ios/Pods/Local Podspecs/react-native-video.podspec.json @@ -1,6 +1,6 @@ { "name": "react-native-video", - "version": "5.0.0", + "version": "5.0.2", "summary": "A