Merge branch 'develop' into single-server

# Conflicts:
#	README.md
#	android/app/src/debug/AndroidManifest.xml
#	android/gradle.properties
#	app/views/RoomsListView/Header/Header.js
#	ios/RocketChatRN.xcodeproj/project.pbxproj
#	ios/RocketChatRN/RocketChatRN.entitlements
This commit is contained in:
Diego Mello 2020-08-10 13:38:50 -03:00
commit 4e437e3269
1603 changed files with 81328 additions and 44771 deletions

View File

@ -75,29 +75,6 @@ restore_cache: &restore-gradle-cache
name: Restore gradle cache
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
restore-brew-cache: &restore-brew-cache
name: Restore Brew cache
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
save-brew-cache: &save-brew-cache
name: Save brew cache
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
paths:
- /usr/local/Homebrew
install-apple-sim-utils: &install-apple-sim-utils
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
rebuild-detox: &rebuild-detox
name: Rebuild Detox framework cache
command: |
npx detox clean-framework-cache
npx detox build-framework-cache
version: 2.1
# EXECUTORS
@ -107,35 +84,6 @@ executors:
environment:
<<: *bash-env
# COMMANDS
commands:
detox-test:
parameters:
folder:
type: string
steps:
- checkout
- attach_workspace:
at: .
- restore_cache: *restore-npm-cache-mac
- restore_cache: *restore-brew-cache
- run: *install-node
- run: *install-apple-sim-utils
- run: *install-npm-modules
- run: *rebuild-detox
- run:
name: Test
command: |
npx detox test << parameters.folder >> --configuration ios.sim.release --cleanup
# JOBS
jobs:
lint-testunit:
@ -170,57 +118,6 @@ jobs:
- save_cache: *save-npm-cache-linux
# E2E
e2e-build:
executor: mac-env
steps:
- checkout
- restore_cache: *restore-npm-cache-mac
- restore_cache: *restore-brew-cache
- run: *install-node
- run: *install-apple-sim-utils
- run: *install-npm-modules
- run: *rebuild-detox
- run:
name: Build
command: |
npx detox build --configuration ios.sim.release
- persist_to_workspace:
root: .
paths:
- ios/build/Build/Products/Release-iphonesimulator/RocketChatRN.app
- save_cache: *save-npm-cache-mac
- save_cache: *save-brew-cache
e2e-test-onboarding:
executor: mac-env
steps:
- detox-test:
folder: "./e2e/tests/onboarding"
e2e-test-room:
executor: mac-env
steps:
- detox-test:
folder: "./e2e/tests/room"
e2e-test-assorted:
executor: mac-env
steps:
- detox-test:
folder: "./e2e/tests/assorted"
# Android builds
android-build:
<<: *defaults
@ -429,23 +326,6 @@ workflows:
jobs:
- lint-testunit
- e2e-hold:
type: approval
requires:
- lint-testunit
- e2e-build:
requires:
- e2e-hold
- e2e-test-onboarding:
requires:
- e2e-build
- e2e-test-room:
requires:
- e2e-build
- e2e-test-assorted:
requires:
- e2e-build
- ios-build:
requires:
- lint-testunit

View File

@ -1,7 +1,34 @@
<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative
<!-- This is a pull request template, you do not need to uncomment or remove the comments, they won't show up in the PR text. -->
<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #ISSUE_NUMBER
## Proposed changes
<!-- Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue below. -->
<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
## Issue(s)
<!-- Link the issues being closed by or related to this PR. For example, you can use #594 if this PR closes issue number 594 -->
## How to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->
## Screenshots
## Types of changes
<!-- What types of changes does your code introduce to Rocket.Chat? -->
<!-- Put an `x` in the boxes that apply -->
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] Improvement (non-breaking change which improves a current function)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Documentation update (if none of the other choices apply)
## Checklist
<!-- Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. -->
- [ ] I have read the [CONTRIBUTING](https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/CONTRIBUTING.md#contributing-to-rocketchat) doc
- [ ] I have signed the [CLA](https://cla-assistant.io/RocketChat/Rocket.Chat.ReactNative)
- [ ] Lint and unit tests pass locally with my changes
- [ ] I have added tests that prove my fix is effective or that my feature works (if applicable)
- [ ] I have added necessary documentation (if applicable)
- [ ] Any dependent changes have been merged and published in downstream modules
## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->

221
.github/workflows/ios_detox.yml vendored Normal file
View File

@ -0,0 +1,221 @@
name: iOS Detox
on: [pull_request]
jobs:
detox-build:
runs-on: macos-latest
timeout-minutes: 60
env:
DEVELOPER_DIR: /Applications/Xcode_11.5.app
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Generate Detox app cache key
run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt"
- name: Cache Detox app
uses: actions/cache@v1
id: detoxappcache
with:
path: ios/build/Build/Products/Release-iphonesimulator
key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }}
- name: Node
if: steps.detoxappcache.outputs.cache-hit != 'true'
uses: actions/setup-node@v1
- name: Cache node modules
if: steps.detoxappcache.outputs.cache-hit != 'true'
uses: actions/cache@v1
id: npmcache
with:
path: node_modules
key: node-modules-${{ hashFiles('**/yarn.lock') }}
- name: Rebuild detox
if: steps.detoxappcache.outputs.cache-hit != 'true' && steps.npmcache.outputs.cache-hit == 'true'
run: yarn detox clean-framework-cache && yarn detox build-framework-cache
- name: Install Dependencies
if: steps.detoxappcache.outputs.cache-hit != 'true' && steps.npmcache.outputs.cache-hit != 'true'
run: yarn install
- run: yarn detox build e2e --configuration ios.sim.release
if: steps.detoxappcache.outputs.cache-hit != 'true'
detox-test-rooms:
needs: detox-build
runs-on: macos-latest
timeout-minutes: 60
env:
DEVELOPER_DIR: /Applications/Xcode_11.5.app
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Generate Detox app cache key
run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt"
- name: Cache Detox app
uses: actions/cache@v1
id: detoxappcache
with:
path: ios/build/Build/Products/Release-iphonesimulator
key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }}
- name: Check for Detox app
if: steps.detoxappcache.outputs.cache-hit != 'true'
run: exit 1
- name: Node
uses: actions/setup-node@v1
- name: Cache node modules
uses: actions/cache@v1
id: npmcache
with:
path: node_modules
key: node-modules-${{ hashFiles('**/yarn.lock') }}
- name: Rebuild detox
if: steps.npmcache.outputs.cache-hit == 'true'
run: yarn detox clean-framework-cache && yarn detox build-framework-cache
- name: Install Dependencies
if: steps.npmcache.outputs.cache-hit != 'true'
run: yarn install
- run: brew tap wix/brew
- run: brew install applesimutils
- run: yarn detox test e2e/tests/room --configuration ios.sim.release --cleanup
- name: Upload test artifacts
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: artifacts
path: artifacts
detox-test-assorted:
needs: detox-build
runs-on: macos-latest
timeout-minutes: 60
env:
DEVELOPER_DIR: /Applications/Xcode_11.5.app
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Generate Detox app cache key
run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt"
- name: Cache Detox app
uses: actions/cache@v1
id: detoxappcache
with:
path: ios/build/Build/Products/Release-iphonesimulator
key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }}
- name: Check for Detox app
if: steps.detoxappcache.outputs.cache-hit != 'true'
run: exit 1
- name: Node
uses: actions/setup-node@v1
- name: Cache node modules
uses: actions/cache@v1
id: npmcache
with:
path: node_modules
key: node-modules-${{ hashFiles('**/yarn.lock') }}
- name: Rebuild detox
if: steps.npmcache.outputs.cache-hit == 'true'
run: yarn detox clean-framework-cache && yarn detox build-framework-cache
- name: Install Dependencies
if: steps.npmcache.outputs.cache-hit != 'true'
run: yarn install
- run: brew tap wix/brew
- run: brew install applesimutils
- run: yarn detox test e2e/tests/assorted --configuration ios.sim.release --cleanup
- name: Upload test artifacts
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: artifacts
path: artifacts
detox-test-onboarding:
needs: detox-build
runs-on: macos-latest
timeout-minutes: 60
env:
DEVELOPER_DIR: /Applications/Xcode_11.5.app
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Generate Detox app cache key
run: echo $(git rev-parse HEAD:app) > "./app-git-revision.txt"
- name: Cache Detox app
uses: actions/cache@v1
id: detoxappcache
with:
path: ios/build/Build/Products/Release-iphonesimulator
key: iOSDetoxRelease-v4-${{ hashFiles('yarn.lock') }}-${{ hashFiles('ios/Podfile.lock') }}-${{ hashFiles('./app-git-revision.txt') }}
- name: Check for Detox app
if: steps.detoxappcache.outputs.cache-hit != 'true'
run: exit 1
- name: Node
uses: actions/setup-node@v1
- name: Cache node modules
uses: actions/cache@v1
id: npmcache
with:
path: node_modules
key: node-modules-${{ hashFiles('**/yarn.lock') }}
- name: Rebuild detox
if: steps.npmcache.outputs.cache-hit == 'true'
run: yarn detox clean-framework-cache && yarn detox build-framework-cache
- name: Install Dependencies
if: steps.npmcache.outputs.cache-hit != 'true'
run: yarn install
- run: brew tap wix/brew
- run: brew install applesimutils
- run: yarn detox test e2e/tests/onboarding --configuration ios.sim.release --cleanup
- name: Upload test artifacts
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: artifacts
path: artifacts

1
.gitignore vendored
View File

@ -58,6 +58,7 @@ buck-out/
coverage
artifacts
.vscode/
e2e/docker/rc_test_env/docker-compose.yml
e2e/docker/data/db

93
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,93 @@
# Contributing Guidelines
Great to have you here! Here are a few ways you can help make this project better!
## Setting up a development environment
Refer to [React Native environment setup](https://reactnative.dev/docs/environment-setup) to make sure everything is up and running.
Follow the `React Native CLI Quickstart` section as we don't support Expo managed flow.
*Note: you'll need a MacOS to run iOS apps*
### How to run
Clone repository and install dependencies:
```sh
git clone git@github.com:RocketChat/Rocket.Chat.ReactNative.git
cd Rocket.Chat.ReactNative
yarn
```
Run the app:
```sh
npx react-native run-ios
```
or
```sh
npx react-native run-android
```
At this point, the app should be running on the simulator or on your device!
*Note: npm won't work on this project*
## Issues needing help
Didn't find a bug or want a new feature not already reported? Check out the [help wanted](https://github.com/RocketChat/Rocket.Chat.ReactNative/issues?q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%91%8B+help+wanted%22) or the [good first issue](https://github.com/RocketChat/Rocket.Chat.ReactNative/issues?q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%8D%AD+good+first+issue%22) labels.
Can't help coding? Triaging issues is a **great** way of helping.
## Code style
We use [ESLint](https://eslint.org/) to enforce code style and best practices. We have a pre-commit hook enforcing commits to follow our lint rules.
To check for lint issues on your code, run this on your terminal:
```sh
yarn lint
```
## Tests
It's always important to ensure everything is working properly and that's why tests are great. We have unit and e2e tests on this project.
### Unit tests
We use [Jest](https://jestjs.io/) and [Storybook](https://storybook.js.org/) on our tests.
#### Storybook
Storybook is a tool for developing UI Components and has some plugins to make Jest generate snapshots of them.
[On the root of the project](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/index.js#L24), comment everything leaving only the last import to Storybook left and refresh your project.
You'll see some tests like this:
<img src="https://user-images.githubusercontent.com/804994/89677725-56393200-d8c4-11ea-84b0-213be1d24e98.png" width="350" />
#### Jest
We use Jest for our unit tests and to generate Storybook snapshots. We have a pre-commit hook enforcing preventing commits that breaks any test.
To check for test issues on your code, run this on your terminal:
```sh
yarn test
```
### E2E tests
We use [Detox](https://github.com/wix/Detox) framework to end-to-end test our app and ensure everything is working properly.
[Follow this documentation to learn how to run it](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/e2e).
### Pull request
As soon as your changes are ready, you can open a Pull Request.
The title of your PR should be descriptive, including either [NEW], [IMPROVEMENT] or [FIX] at the beginning, e.g. [FIX] App crashing on startup.
You may share working results prior to finishing, please include [WIP] in the title. This way anyone can look at your code: you can ask for help within the PR if you don't know how to solve a problem.
Your PR is automatically inspected by various tools, check their response and try to improve your code accordingly. Requests that fail to build or have wrong coding style won't be merged.

225
README.md
View File

@ -5,11 +5,12 @@
[![codecov](https://codecov.io/gh/RocketChat/Rocket.Chat.ReactNative/branch/master/graph/badge.svg)](https://codecov.io/gh/RocketChat/Rocket.Chat.ReactNative)
[![CodeFactor](https://www.codefactor.io/repository/github/rocketchat/rocket.chat.reactnative/badge)](https://www.codefactor.io/repository/github/rocketchat/rocket.chat.reactnative)
**Supported Server Versions:** 0.70.0+
- **Supported server versions:** 0.70.0+
- **Supported iOS versions**: 11+
- **Supported Android versions**: 5.0+
## Download
### Official apps
<a href="https://play.google.com/store/apps/details?id=chat.rocket.android">
<img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>
</a>
@ -17,29 +18,7 @@
<img alt="Download on App Store" src="https://user-images.githubusercontent.com/7317008/43209852-4ca39622-904b-11e8-8ce1-cdc3aee76ae9.png" height=43>
</a>
### Experimental apps
<a href="https://play.google.com/store/apps/details?id=chat.rocket.reactnative">
<img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>
</a>
<a href="https://itunes.apple.com/us/app/rocket-chat-experimental/id1272915472">
<img alt="Download on App Store" src="https://user-images.githubusercontent.com/7317008/43209852-4ca39622-904b-11e8-8ce1-cdc3aee76ae9.png" height=43>
</a>
## Beta Access
### TestFlight
You can signup to our TestFlight builds by accessing these links:
- Official: https://testflight.apple.com/join/3gcYeoMr
- Experimental: https://testflight.apple.com/join/7I3dLCNT.
### Google Play beta
You can subscribe to Google Play Beta program and download latest versions:
- Official: https://play.google.com/store/apps/details?id=chat.rocket.android
- Experimental: https://play.google.com/store/apps/details?id=chat.rocket.reactnative
Check [our docs](https://docs.rocket.chat/installation/mobile-and-desktop-apps#mobile-apps) for beta and Experimental versions.
## Reporting an Issue
@ -47,194 +26,16 @@ You can subscribe to Google Play Beta program and download latest versions:
Also check the [#react-native](https://open.rocket.chat/channel/react-native) community on [open.rocket.chat](https://open.rocket.chat). We'd like to help.
## Installing dependencies
## Contributing
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
Are you a dev and would like to help? Found a bug that you would like to report or a missing feature that you would like to work on? Great! We have written down a [Contribution guide](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/CONTRIBUTING.md) so you can start easily.
## How to run
- Clone repository and install dependencies:
```bash
$ git clone git@github.com:RocketChat/Rocket.Chat.ReactNative.git
$ cd Rocket.Chat.ReactNative
$ yarn
```
## Whitelabel
Do you want to make the app run on your own server only? [Follow our whitelabel documentation.](https://docs.rocket.chat/guides/developer/mobile-apps/whitelabeling-mobile-apps)
- Run application
```bash
$ npx react-native run-ios
```
```bash
$ npx react-native run-android
```
## Engage with us
### Share your story
Wed love to hear about [your experience](https://survey.zohopublic.com/zs/e4BUFG) and potentially feature it on our [Blog](https://rocket.chat/case-studies/?utm_source=github&utm_medium=readme&utm_campaign=community).
### Whitelabel
Follow our docs: https://docs.rocket.chat/guides/developer/mobile-apps/whitelabeling-mobile-apps
## Current priorities
1) Omnichannel support
2) E2E encryption
## Features
| Feature | Status |
|--------------------------------------------------------------- |-------- |
| Jitsi Integration | ✅ |
| Federation (Directory) | ✅ |
| Discussions | ✅ |
| Omnichannel | ❌ |
| Threads | ✅ |
| Record Audio | ✅ |
| Record Video | ✅ |
| Commands | ✅ |
| Draft message per room | ✅ |
| Share Extension | ✅ |
| Notifications Preferences | ✅ |
| Edited status | ✅ |
| Upload video | ✅ |
| Grouped messages | ✅ |
| Mark room as read | ✅ |
| Mark room as unread | ✅ |
| Tablet Support | ✅ |
| Read receipt | ✅ |
| Broadbast Channel | ✅ |
| Authentication via SAML | ✅ |
| Authentication via CAS | ✅ |
| Custom Fields on Signup | ✅ |
| Report message | ✅ |
| Theming | ✅ |
| Settings -> Review the App | ✅ |
| Settings -> Default Browser | ✅ |
| Admin panel | ✅ |
| Reply message from notification | ✅ |
| Unread counter banner on message list | ✅ |
| E2E Encryption | ❌ |
| Join a Protected Room | ❌ |
| Optional Analytics | ✅ |
| Settings -> About us | ❌ |
| Settings -> Contact us | ✅ |
| Settings -> Update App Icon | ❌ |
| Settings -> Share | ✅ |
| Accessibility (Medium) | ❌ |
| Accessibility (Advanced) | ❌ |
| Authentication via Meteor | ❌ |
| Authentication via Wordpress | ✅ |
| Authentication via Custom OAuth | ✅ |
| Add user to the room | ✅ |
| Send message | ✅ |
| Authentication via Email | ✅ |
| Authentication via Username | ✅ |
| Authentication via LDAP | ✅ |
| Message format: Markdown | ✅ |
| Message format: System messages (Welcome, Message removed...) | ✅ |
| Message format: links | ✅ |
| Message format: images | ✅ |
| Message format: replies | ✅ |
| Message format: alias with custom message (title & text) | ✅ |
| Messages list: day separation | ✅ |
| Messages list: load more on scroll | ✅ |
| Messages list: receive new messages via subscription | ✅ |
| Subscriptions list | ✅ |
| Segmented subscriptions list: Favorites | ✅ |
| Segmented subscriptions list: Unreads | ✅ |
| Segmented subscriptions list: DMs | ✅ |
| Segmented subscriptions list: Channels | ✅ |
| Subscriptions list: update user status via subscription | ✅ |
| Numbers os messages unread in the Subscriptions list | ✅ |
| Status change | ✅ |
| Upload image | ✅ |
| Take picture & upload it | ✅ |
| 2FA | ✅ |
| Signup | ✅ |
| Autocomplete with usernames | ✅ |
| Autocomplete with @all & @here | ✅ |
| Autocomplete room/channel name | ✅ |
| Upload audio | ✅ |
| Forgot your password | ✅ |
| Login screen: terms of service | ✅ |
| Login screen: privacy policy | ✅ |
| Authentication via Google | ✅ |
| Authentication via Facebook | ✅ |
| Authentication via Twitter | ✅ |
| Authentication via GitHub | ✅ |
| Authentication via GitLab | ✅ |
| Authentication via LinkedIn | ✅ |
| Create channel | ✅ |
| Search Local | ✅ |
| Search in the API | ✅ |
| Settings -> License | ✅ |
| Settings -> App version | ✅ |
| Autocomplete emoji | ✅ |
| Upload file (documents, PDFs, spreadsheets, zip files, etc) | ✅ |
| Copy message | ✅ |
| Pin message | ✅ |
| Unpin message | ✅ |
| Channel Info screen -> Members | ✅ |
| Channel Info screen -> Pinned | ✅ |
| Channel Info screen -> Starred | ✅ |
| Channel Info screen -> Uploads | ✅ |
| Star message | ✅ |
| Unstar message | ✅ |
| Channel Info screen -> Topic | ✅ |
| Channel Info screen -> Description | ✅ |
| Star a channel | ✅ |
| Message format: videos | ✅ |
| Message format: audios | ✅ |
| Edit message | ✅ |
| Delete a message | ✅ |
| Reply message | ✅ |
| Quote message | ✅ |
| Muted state | ✅ |
| Offline reading | ✅ |
| Offline writing | ✅ |
| Edit profile | ✅ |
| Reactions | ✅ |
| Custom emojis | ✅ |
| Accessibility (Basic) | ✅ |
| Tap notification, go to the channel | ✅ |
| Deep links: Authentication | ✅ |
| Deep links: Rooms | ✅ |
| Full name setting | ✅ |
| Read only rooms | ✅ |
| Typing status | ✅ |
| Create channel/group | ✅ |
| Disable registration setting | ✅ |
| Unread red line indicator on message list | ✅ |
| Search Messages in Channel | ✅ |
| Mentions List | ✅ |
| Attachment List | ✅ |
| Join a Room | ✅ |
## Detox (end-to-end tests)
- Build your app
```bash
$ npx detox build --configuration ios.sim.release
```
- Run tests
```bash
$ npx detox test ./e2e/tests/onboarding --configuration ios.sim.release
$ npx detox test ./e2e/tests/room --configuration ios.sim.release
$ npx detox test ./e2e/tests/assorted --configuration ios.sim.release
```
## Storybook
- Open index.js
- Uncomment following line
```bash
import './storybook';
```
- Comment out following lines
```bash
import './app/ReactotronConfig';
import { AppRegistry } from 'react-native';
import App from './app/index';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
```
- Start your application again
### Subscribe for Updates
Once a month our marketing team releases an email update with news about product releases, company related topics, events and use cases. [Sign Up!](https://rocket.chat/newsletter/?utm_source=github&utm_medium=readme&utm_campaign=community)

View File

@ -0,0 +1,3 @@
export default {
crashlytics: null
};

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
apply plugin: "com.android.application"
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'kotlin-android'
apply plugin: "io.fabric"
apply plugin: "com.google.firebase.firebase-perf"
apply plugin: 'com.bugsnag.android.gradle'
import com.android.build.OutputFile
@ -168,15 +168,18 @@ android {
minifyEnabled enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
signingConfig signingConfigs.release
firebaseCrashlytics {
nativeSymbolUploadEnabled true
}
}
}
packagingOptions {
pickFirst '**/armeabi-v7a/libc++_shared.so'
pickFirst '**/x86/libc++_shared.so'
pickFirst '**/arm64-v8a/libc++_shared.so'
pickFirst '**/x86_64/libc++_shared.so'
}
// packagingOptions {
// pickFirst '**/armeabi-v7a/libc++_shared.so'
// pickFirst '**/x86/libc++_shared.so'
// pickFirst '**/arm64-v8a/libc++_shared.so'
// pickFirst '**/x86_64/libc++_shared.so'
// }
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
@ -215,11 +218,6 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "com.google.firebase:firebase-messaging:18.0.0"
implementation "com.google.firebase:firebase-core:16.0.9"
implementation "com.google.firebase:firebase-perf:17.0.2"
implementation('com.crashlytics.sdk.android:crashlytics:2.9.9@aar') {
transitive = true
}
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
@ -251,5 +249,4 @@ task copyDownloadableDepsToLibs(type: Copy) {
into 'libs'
}
apply plugin: 'com.google.gms.google-services'
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

View File

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

View File

@ -0,0 +1,25 @@
package chat.rocket.reactnative;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
public class MainDebugApplication extends MainApplication {
@Override
public void onCreate() {
super.onCreate();
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*
* @param context
* @param reactInstanceManager
*/
private static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
ReactNativeFlipper.initializeFlipper(context, reactInstanceManager);
}
}

View File

@ -4,7 +4,7 @@
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.rndiffapp;
package chat.rocket.reactnative;
import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.android.utils.FlipperUtils;

View File

@ -37,8 +37,10 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="go.rocket.chat" />
<data android:scheme="https" android:host="jitsi.rocket.chat" />
<data android:scheme="rocketchat" android:host="room" />
<data android:scheme="rocketchat" android:host="auth" />
<data android:scheme="rocketchat" android:host="jitsi.rocket.chat" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

View File

@ -0,0 +1,10 @@
package chat.rocket.reactnative;
import android.os.Bundle;
import androidx.annotation.Nullable;
public class Callback {
public void call(@Nullable Bundle bundle) {
}
}

View File

@ -14,8 +14,9 @@ import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.app.Person;
import androidx.annotation.Nullable;
import com.google.gson.*;
import com.google.gson.Gson;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
@ -33,15 +34,18 @@ import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
public class CustomPushNotification extends PushNotification {
public static ReactApplicationContext reactApplicationContext;
final NotificationManager notificationManager;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
reactApplicationContext = new ReactApplicationContext(context);
notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
private static Map<String, List<Bundle>> notificationMessages = new HashMap<String, List<Bundle>>();
@ -54,29 +58,39 @@ public class CustomPushNotification extends PushNotification {
@Override
public void onReceived() throws InvalidNotificationException {
final Bundle bundle = mNotificationProps.asBundle();
Bundle received = mNotificationProps.asBundle();
Ejson receivedEjson = new Gson().fromJson(received.getString("ejson", "{}"), Ejson.class);
if (receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) {
notificationLoad(receivedEjson, new Callback() {
@Override
public void call(@Nullable Bundle bundle) {
if (bundle != null) {
mNotificationProps = createProps(bundle);
}
}
});
}
// We should re-read these values since that can be changed by notificationLoad
Bundle bundle = mNotificationProps.asBundle();
Ejson loadedEjson = new Gson().fromJson(bundle.getString("ejson", "{}"), Ejson.class);
String notId = bundle.getString("notId", "1");
String title = bundle.getString("title");
if (notificationMessages.get(notId) == null) {
notificationMessages.put(notId, new ArrayList<Bundle>());
}
Gson gson = new Gson();
Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class);
boolean hasSender = ejson.sender != null;
boolean hasSender = loadedEjson.sender != null;
String title = bundle.getString("title");
bundle.putLong("time", new Date().getTime());
bundle.putString("username", hasSender ? ejson.sender.username : title);
bundle.putString("senderId", hasSender ? ejson.sender._id : "1");
bundle.putString("avatarUri", ejson.getAvatarUri());
bundle.putString("username", hasSender ? loadedEjson.sender.username : title);
bundle.putString("senderId", hasSender ? loadedEjson.sender._id : "1");
bundle.putString("avatarUri", loadedEjson.getAvatarUri());
notificationMessages.get(notId).add(bundle);
super.postNotification(Integer.parseInt(notId));
postNotification(Integer.parseInt(notId));
notifyReceivedToJS();
}
@ -96,9 +110,11 @@ public class CustomPushNotification extends PushNotification {
String notId = bundle.getString("notId", "1");
String title = bundle.getString("title");
String message = bundle.getString("message");
Boolean notificationLoaded = bundle.getBoolean("notificationLoaded", false);
Ejson ejson = new Gson().fromJson(bundle.getString("ejson", "{}"), Ejson.class);
notification
.setContentTitle(title)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(intent)
.setPriority(Notification.PRIORITY_HIGH)
@ -109,10 +125,34 @@ public class CustomPushNotification extends PushNotification {
notificationColor(notification);
notificationChannel(notification);
notificationIcons(notification, bundle);
notificationStyle(notification, notificationId, bundle);
notificationReply(notification, notificationId, bundle);
notificationDismiss(notification, notificationId);
// if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully
if (ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") || notificationLoaded) {
notificationStyle(notification, notificationId, bundle);
notificationReply(notification, notificationId, bundle);
// message couldn't be loaded from server (Fallback notification)
} else {
Gson gson = new Gson();
// iterate over the current notification ids to dismiss fallback notifications from same server
for (Map.Entry<String, List<Bundle>> bundleList : notificationMessages.entrySet()) {
// iterate over the notifications with this id (same host + rid)
Iterator iterator = bundleList.getValue().iterator();
while (iterator.hasNext()) {
Bundle not = (Bundle) iterator.next();
// get the notification info
Ejson notEjson = gson.fromJson(not.getString("ejson", "{}"), Ejson.class);
// if already has a notification from same server
if (ejson.serverURL().equals(notEjson.serverURL())) {
String id = not.getString("notId");
// cancel this notification
notificationManager.cancel(Integer.parseInt(id));
}
}
}
}
return notification;
}
@ -300,4 +340,7 @@ public class CustomPushNotification extends PushNotification {
notification.setDeleteIntent(dismissPendingIntent);
}
private void notificationLoad(Ejson ejson, Callback callback) {
LoadNotification.load(reactApplicationContext, ejson, callback);
}
}

View File

@ -9,6 +9,8 @@ public class Ejson {
String rid;
String type;
Sender sender;
String messageId;
String notificationType;
private String TOKEN_KEY = "reactnativemeteor_usertoken-";
private SharedPreferences sharedPreferences = RNUserDefaultsModule.getPreferences(CustomPushNotification.reactApplicationContext);

View File

@ -0,0 +1,101 @@
package chat.rocket.reactnative;
import android.os.Bundle;
import android.content.Context;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Interceptor;
import com.google.gson.Gson;
import java.io.IOException;
import com.facebook.react.bridge.ReactApplicationContext;
import chat.rocket.userdefaults.RNUserDefaultsModule;
class JsonResponse {
Data data;
class Data {
Notification notification;
class Notification {
String notId;
String title;
String text;
Payload payload;
class Payload {
String host;
String rid;
String type;
Sender sender;
String messageId;
String notificationType;
String name;
String messageType;
class Sender {
String _id;
String username;
String name;
}
}
}
}
}
public class LoadNotification {
private static int RETRY_COUNT = 0;
private static int[] TIMEOUT = new int[]{ 0, 1, 3, 5, 10 };
private static String TOKEN_KEY = "reactnativemeteor_usertoken-";
public static void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
final OkHttpClient client = new OkHttpClient();
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
Request request = new Request.Builder()
.header("x-user-id", ejson.userId())
.header("x-auth-token", ejson.token())
.url(url.addQueryParameter("id", ejson.messageId).build())
.build();
runRequest(client, request, callback);
}
private static void runRequest(OkHttpClient client, Request request, Callback callback) {
try {
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);
Response response = client.newCall(request).execute();
String body = response.body().string();
if (!response.isSuccessful()) {
throw new Exception("Error");
}
Gson gson = new Gson();
JsonResponse json = gson.fromJson(body, JsonResponse.class);
Bundle bundle = new Bundle();
bundle.putString("notId", json.data.notification.notId);
bundle.putString("title", json.data.notification.title);
bundle.putString("message", json.data.notification.text);
bundle.putString("ejson", gson.toJson(json.data.notification.payload));
bundle.putBoolean("notificationLoaded", true);
callback.call(bundle);
} catch (Exception e) {
if (RETRY_COUNT <= TIMEOUT.length) {
RETRY_COUNT++;
runRequest(client, request, callback);
} else {
callback.call(null);
}
}
}
}

View File

@ -29,10 +29,6 @@ import com.wix.reactnativenotifications.core.notification.INotificationsApplicat
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage;
import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage;
import io.invertase.firebase.perf.RNFirebasePerformancePackage;
import com.nozbe.watermelondb.WatermelonDBPackage;
import com.reactnativecommunity.viewpager.RNCViewPagerPackage;
@ -53,9 +49,6 @@ public class MainApplication extends Application implements ReactApplication, IN
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new RNFirebaseCrashlyticsPackage());
packages.add(new RNFirebaseAnalyticsPackage());
packages.add(new RNFirebasePerformancePackage());
packages.add(new KeyboardInputPackage(MainApplication.this));
packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new WatermelonDBPackage());
@ -88,38 +81,6 @@ public class MainApplication extends Application implements ReactApplication, IN
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*
* @param context
* @param reactInstanceManager
*/
private static void initializeFlipper(
Context context, ReactInstanceManager reactInstanceManager) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("chat.rocket.reactnative");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
@Override

View File

@ -1,10 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "28.0.3"
buildToolsVersion = "29.0.2"
minSdkVersion = 21
compileSdkVersion = 28
targetSdkVersion = 28
compileSdkVersion = 29
targetSdkVersion = 29
glideVersion = "4.9.0"
kotlin_version = "1.3.50"
supportLibVersion = "28.0.0"
@ -18,10 +18,9 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.firebase:perf-plugin:1.2.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+'
@ -57,10 +56,10 @@ subprojects { subproject ->
afterEvaluate {
if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
targetSdkVersion 28
targetSdkVersion 29
}
variantFilter { variant ->
def names = variant.flavors*.name

View File

@ -28,11 +28,11 @@ android.enableJetifier=true
APPLICATIONID=chat.rocket.reactnative
VERSIONNAME=4.9.0
VERSIONCODE=1
BugsnagAPIKey=""
BugsnagAPIKey=
KEYSTORE=my-upload-key.keystore
KEY_ALIAS=my-key-alias
KEYSTORE_PASSWORD=
KEY_PASSWORD=
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.33.1
FLIPPER_VERSION=0.51.0

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

29
android/gradlew vendored
View File

@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@ -175,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

5
android/gradlew.bat vendored
View File

@ -5,7 +5,7 @@
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

View File

@ -33,6 +33,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'CLOSE_SEARCH_HEADER'
]);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
export const INQUIRY = createRequestTypes('INQUIRY', [...defaultTypes, 'SET_ENABLED', 'RESET', 'QUEUE_ADD', 'QUEUE_UPDATE', 'QUEUE_REMOVE']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);

55
app/actions/inquiry.js Normal file
View File

@ -0,0 +1,55 @@
import * as types from './actionsTypes';
export function inquirySetEnabled(enabled) {
return {
type: types.INQUIRY.SET_ENABLED,
enabled
};
}
export function inquiryReset() {
return {
type: types.INQUIRY.RESET
};
}
export function inquiryQueueAdd(inquiry) {
return {
type: types.INQUIRY.QUEUE_ADD,
inquiry
};
}
export function inquiryQueueUpdate(inquiry) {
return {
type: types.INQUIRY.QUEUE_UPDATE,
inquiry
};
}
export function inquiryQueueRemove(inquiryId) {
return {
type: types.INQUIRY.QUEUE_REMOVE,
inquiryId
};
}
export function inquiryRequest() {
return {
type: types.INQUIRY.REQUEST
};
}
export function inquirySuccess(inquiries) {
return {
type: types.INQUIRY.SUCCESS,
inquiries
};
}
export function inquiryFailure(error) {
return {
type: types.INQUIRY.FAILURE,
error
};
}

View File

@ -10,6 +10,16 @@ export const SWITCH_TRACK_COLOR = {
true: '#2de0a5'
};
const mentions = {
unreadBackground: '#414852',
mentionMeColor: '#f5455c',
mentionMeBackground: '#ffe9ec',
mentionGroupColor: '#f38c39',
mentionGroupBackground: '#fde8d7',
mentionOtherColor: '#b68d00',
mentionOtherBackground: '#fff6d6'
};
export const themes = {
light: {
backgroundColor: '#ffffff',
@ -53,7 +63,8 @@ export const themes = {
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A',
previewBackground: '#1F2329',
previewTintColor: '#ffffff'
previewTintColor: '#ffffff',
...mentions
},
dark: {
backgroundColor: '#030b1b',
@ -97,7 +108,8 @@ export const themes = {
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A',
previewBackground: '#030b1b',
previewTintColor: '#ffffff'
previewTintColor: '#ffffff',
...mentions
},
black: {
backgroundColor: '#000000',
@ -141,6 +153,7 @@ export const themes = {
passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A',
previewBackground: '#000000',
previewTintColor: '#ffffff'
previewTintColor: '#ffffff',
...mentions
}
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';

View File

@ -1,5 +1,5 @@
import React from 'react';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import PropTypes from 'prop-types';
const CustomEmoji = React.memo(({ baseUrl, emoji, style }) => (

View File

@ -28,7 +28,7 @@ export const CustomHeaderButtons = React.memo(props => (
export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => (
<CustomHeaderButtons left>
<Item title='drawer' iconName='menu_hamburguer' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
<Item title='drawer' iconName='hamburguer' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
</CustomHeaderButtons>
));
@ -36,7 +36,7 @@ export const CloseModalButton = React.memo(({
navigation, testID, onPress = () => navigation.pop(), ...props
}) => (
<CustomHeaderButtons left>
<Item title='close' iconName='Cross' onPress={onPress} testID={testID} {...props} />
<Item title='close' iconName='close' onPress={onPress} testID={testID} {...props} />
</CustomHeaderButtons>
));
@ -44,14 +44,14 @@ export const CancelModalButton = React.memo(({ onPress, testID }) => (
<CustomHeaderButtons left>
{isIOS
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
: <Item title='close' iconName='Cross' onPress={onPress} testID={testID} />
: <Item title='close' iconName='close' onPress={onPress} testID={testID} />
}
</CustomHeaderButtons>
));
export const MoreButton = React.memo(({ onPress, testID }) => (
<CustomHeaderButtons>
<Item title='more' iconName='menu' onPress={onPress} testID={testID} />
<Item title='more' iconName='kebab' onPress={onPress} testID={testID} />
</CustomHeaderButtons>
));

View File

@ -90,6 +90,8 @@ const NotifierComponent = React.memo(({
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
goRoom({ item, isMasterDetail });
hideNotification();
@ -125,7 +127,7 @@ const NotifierComponent = React.memo(({
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name='Cross' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
<CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
</Touchable>
</View>
);

View File

@ -14,7 +14,7 @@ const InAppNotification = memo(() => {
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload.rid) {
if (route?.name === 'RoomView' && route.params?.rid === payload.rid) {
if ((route?.name === 'RoomView' && route.params?.rid === payload.rid) || route?.name === 'JitsiMeetView') {
return;
}
Notifier.showNotification({

View File

@ -15,6 +15,7 @@ import OrSeparator from './OrSeparator';
import Touch from '../utils/touch';
import I18n from '../i18n';
import random from '../utils/random';
import { logEvent, events } from '../utils/log';
import RocketChat from '../lib/rocketchat';
const BUTTON_HEIGHT = 48;
@ -77,6 +78,7 @@ class LoginServices extends React.PureComponent {
}
onPressFacebook = () => {
logEvent(events.ENTER_WITH_FACEBOOK);
const { services, server } = this.props;
const { clientId } = services.facebook;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
@ -88,6 +90,7 @@ class LoginServices extends React.PureComponent {
}
onPressGithub = () => {
logEvent(events.ENTER_WITH_GITHUB);
const { services, server } = this.props;
const { clientId } = services.github;
const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`;
@ -99,6 +102,7 @@ class LoginServices extends React.PureComponent {
}
onPressGitlab = () => {
logEvent(events.ENTER_WITH_GITLAB);
const { services, server, Gitlab_URL } = this.props;
const { clientId } = services.gitlab;
const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com';
@ -111,6 +115,7 @@ class LoginServices extends React.PureComponent {
}
onPressGoogle = () => {
logEvent(events.ENTER_WITH_GOOGLE);
const { services, server } = this.props;
const { clientId } = services.google;
const endpoint = 'https://accounts.google.com/o/oauth2/auth';
@ -122,6 +127,7 @@ class LoginServices extends React.PureComponent {
}
onPressLinkedin = () => {
logEvent(events.ENTER_WITH_LINKEDIN);
const { services, server } = this.props;
const { clientId } = services.linkedin;
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
@ -133,6 +139,7 @@ class LoginServices extends React.PureComponent {
}
onPressMeteor = () => {
logEvent(events.ENTER_WITH_METEOR);
const { services, server } = this.props;
const { clientId } = services['meteor-developer'];
const endpoint = 'https://www.meteor.com/oauth2/authorize';
@ -143,6 +150,7 @@ class LoginServices extends React.PureComponent {
}
onPressTwitter = () => {
logEvent(events.ENTER_WITH_TWITTER);
const { server } = this.props;
const state = this.getOAuthState();
const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`;
@ -150,6 +158,7 @@ class LoginServices extends React.PureComponent {
}
onPressWordpress = () => {
logEvent(events.ENTER_WITH_WORDPRESS);
const { services, server } = this.props;
const { clientId, serverURL } = services.wordpress;
const endpoint = `${ serverURL }/oauth/authorize`;
@ -161,6 +170,7 @@ class LoginServices extends React.PureComponent {
}
onPressCustomOAuth = (loginService) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { server } = this.props;
const {
serverURL, authorizePath, clientId, scope, service
@ -175,6 +185,7 @@ class LoginServices extends React.PureComponent {
}
onPressSaml = (loginService) => {
logEvent(events.ENTER_WITH_SAML);
const { server } = this.props;
const { clientConfig } = loginService;
const { provider } = clientConfig;
@ -184,6 +195,7 @@ class LoginServices extends React.PureComponent {
}
onPressCas = () => {
logEvent(events.ENTER_WITH_CAS);
const { server, CAS_login_url } = this.props;
const ssoToken = random(17);
const url = `${ CAS_login_url }?service=${ server }/_cas/${ ssoToken }`;
@ -191,6 +203,7 @@ class LoginServices extends React.PureComponent {
}
onPressAppleLogin = async() => {
logEvent(events.ENTER_WITH_APPLE);
try {
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
requestedScopes: [
@ -201,7 +214,7 @@ class LoginServices extends React.PureComponent {
await RocketChat.loginOAuthOrSso({ fullName, email, identityToken });
} catch {
// Do nothing
logEvent(events.ENTER_WITH_APPLE_F);
}
}

View File

@ -78,7 +78,7 @@ const HeaderFooter = React.memo(({ onReaction, theme }) => (
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
<CustomIcon name='add-reaction' size={24} color={themes[theme].bodyText} />
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
</Button>
));
HeaderFooter.propTypes = {

View File

@ -7,7 +7,7 @@ import moment from 'moment';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n';
import log from '../../utils/log';
import log, { logEvent } from '../../utils/log';
import Navigation from '../../lib/Navigation';
import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
@ -15,6 +15,7 @@ import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events';
const MessageActions = React.memo(forwardRef(({
room,
@ -112,11 +113,18 @@ const MessageActions = React.memo(forwardRef(({
const getPermalink = message => RocketChat.getPermalinkMessage(message);
const handleReply = message => replyInit(message, true);
const handleReply = (message) => {
logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true);
};
const handleEdit = message => editInit(message);
const handleEdit = (message) => {
logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message);
};
const handleCreateDiscussion = (message) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
@ -126,6 +134,7 @@ const MessageActions = React.memo(forwardRef(({
};
const handleUnread = async(message) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message;
const { rid } = room;
try {
@ -144,54 +153,66 @@ const MessageActions = React.memo(forwardRef(({
Navigation.navigate('RoomsListView');
}
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_UNREAD_F);
log(e);
}
};
const handlePermalink = async(message) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try {
const permalink = await getPermalink(message);
Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch {
// Do nothing
logEvent(events.ROOM_MSG_ACTION_PERMALINK_F);
}
};
const handleCopy = async(message) => {
logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString(message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async(message) => {
logEvent(events.ROOM_MSG_ACTION_SHARE);
try {
const permalink = await getPermalink(message);
Share.share({ message: permalink });
} catch {
// Do nothing
logEvent(events.ROOM_MSG_ACTION_SHARE_F);
}
};
const handleQuote = message => replyInit(message, false);
const handleQuote = (message) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false);
};
const handleStar = async(message) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_STAR_F);
log(e);
}
};
const handlePin = async(message) => {
logEvent(events.ROOM_MSG_ACTION_PIN);
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_PIN_F);
log(e);
}
};
const handleReaction = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) {
onReactionPress(shortname, message.id);
} else {
@ -201,7 +222,13 @@ const MessageActions = React.memo(forwardRef(({
hideActionSheet();
};
const handleReadReceipt = message => Navigation.navigate('ReadReceiptsView', { messageId: message.id });
const handleReadReceipt = (message) => {
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else {
Navigation.navigate('ReadReceiptsView', { messageId: message.id });
}
};
const handleToggleTranslation = async(message) => {
try {
@ -228,10 +255,12 @@ const MessageActions = React.memo(forwardRef(({
};
const handleReport = async(message) => {
logEvent(events.ROOM_MSG_ACTION_REPORT);
try {
await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported'));
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_REPORT_F);
log(e);
}
};
@ -242,8 +271,10 @@ const MessageActions = React.memo(forwardRef(({
callToAction: I18n.t('Delete'),
onPress: async() => {
try {
logEvent(events.ROOM_MSG_ACTION_DELETE);
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e);
}
}
@ -290,7 +321,7 @@ const MessageActions = React.memo(forwardRef(({
// Create Discussion
options.push({
title: I18n.t('Start_a_Discussion'),
icon: 'chat',
icon: 'discussions',
onPress: () => handleCreateDiscussion(message)
});
@ -365,7 +396,7 @@ const MessageActions = React.memo(forwardRef(({
if (allowDelete(message)) {
options.push({
title: I18n.t('Delete'),
icon: 'trash',
icon: 'delete',
danger: true,
onPress: () => handleDelete(message)
});
@ -375,6 +406,7 @@ const MessageActions = React.memo(forwardRef(({
};
const showMessageActions = async(message) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions();
showActionSheet({
options: getOptions(message),

View File

@ -1,7 +1,7 @@
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import styles from '../styles';
import { CustomIcon } from '../../../lib/Icons';
@ -32,7 +32,7 @@ const Item = ({ item, theme }) => {
{ loading ? <ActivityIndicator theme={theme} /> : null }
</FastImage>
)
: <CustomIcon name='clip' size={36} color={themes[theme].actionTintColor} />
: <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
}
</TouchableOpacity>
);

View File

@ -10,6 +10,7 @@ import styles from './styles';
import I18n from '../../i18n';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { logEvent, events } from '../../utils/log';
const RECORDING_EXTENSION = '.aac';
const RECORDING_SETTINGS = {
@ -103,6 +104,7 @@ export default class RecordAudio extends React.PureComponent {
}
startRecordingAudio = async() => {
logEvent(events.ROOM_AUDIO_RECORD);
if (!this.isRecorderBusy) {
this.isRecorderBusy = true;
try {
@ -120,13 +122,14 @@ export default class RecordAudio extends React.PureComponent {
await Audio.requestPermissionsAsync();
}
} catch (error) {
// Do nothing
logEvent(events.ROOM_AUDIO_RECORD_F);
}
this.isRecorderBusy = false;
}
};
finishRecordingAudio = async() => {
logEvent(events.ROOM_AUDIO_FINISH);
if (!this.isRecorderBusy) {
const { onFinish } = this.props;
@ -147,7 +150,7 @@ export default class RecordAudio extends React.PureComponent {
onFinish(fileInfo);
} catch (error) {
// Do nothing
logEvent(events.ROOM_AUDIO_FINISH_F);
}
this.setState({ isRecording: false, recordingDurationMillis: 0 });
deactivateKeepAwake();
@ -156,12 +159,13 @@ export default class RecordAudio extends React.PureComponent {
};
cancelRecordingAudio = async() => {
logEvent(events.ROOM_AUDIO_CANCEL);
if (!this.isRecorderBusy) {
this.isRecorderBusy = true;
try {
await this.recording.stopAndUnloadAsync();
} catch (error) {
// Do nothing
logEvent(events.ROOM_AUDIO_CANCEL_F);
}
this.setState({ isRecording: false, recordingDurationMillis: 0 });
deactivateKeepAwake();
@ -182,7 +186,7 @@ export default class RecordAudio extends React.PureComponent {
accessibilityLabel={I18n.t('Send_audio_message')}
accessibilityTraits='button'
>
<CustomIcon name='mic' size={23} color={themes[theme].tintColor} />
<CustomIcon name='microphone' size={23} color={themes[theme].tintColor} />
</BorderlessButton>
);
}
@ -199,7 +203,7 @@ export default class RecordAudio extends React.PureComponent {
<CustomIcon
size={22}
color={themes[theme].dangerColor}
name='Cross'
name='close'
/>
</BorderlessButton>
<Text

View File

@ -72,7 +72,7 @@ const ReplyPreview = React.memo(({
theme={theme}
/>
</View>
<CustomIcon name='Cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
<CustomIcon name='close' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
</View>
);
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme && isEqual(prevProps.message, nextProps.message));

View File

@ -8,7 +8,7 @@ const ActionsButton = React.memo(({ theme, onPress }) => (
onPress={onPress}
testID='messagebox-actions'
accessibilityLabel='Message_actions'
icon='plus'
icon='add'
theme={theme}
/>
));

View File

@ -8,7 +8,7 @@ const CancelEditingButton = React.memo(({ theme, onPress }) => (
onPress={onPress}
testID='messagebox-cancel-editing'
accessibilityLabel='Cancel_editing'
icon='Cross'
icon='close'
theme={theme}
/>
));

View File

@ -8,7 +8,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
onPress={onPress}
testID='messagebox-send-message'
accessibilityLabel='Send_message'
icon='send-active'
icon='send-filled'
theme={theme}
/>
));

View File

@ -17,8 +17,8 @@ import RocketChat from '../../lib/rocketchat';
import styles from './styles';
import database from '../../lib/database';
import { emojis } from '../../emojis';
import log, { logEvent, events } from '../../utils/log';
import RecordAudio from './RecordAudio';
import log from '../../utils/log';
import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview';
import debounce from '../../utils/debounce';
@ -122,6 +122,7 @@ class MessageBox extends Component {
command: {}
};
this.text = '';
this.selection = { start: 0, end: 0 };
this.focused = false;
// MessageBox Actions
@ -133,7 +134,7 @@ class MessageBox extends Component {
},
{
title: I18n.t('Take_a_video'),
icon: 'video-1',
icon: 'camera',
onPress: this.takeVideo
},
{
@ -143,12 +144,12 @@ class MessageBox extends Component {
},
{
title: I18n.t('Choose_file'),
icon: 'folder',
icon: 'attach',
onPress: this.chooseFile
},
{
title: I18n.t('Create_Discussion'),
icon: 'chat',
icon: 'discussions',
onPress: this.createDiscussion
}
];
@ -331,6 +332,10 @@ class MessageBox extends Component {
this.setInput(text);
}
onSelectionChange = (e) => {
this.selection = e.nativeEvent.selection;
}
// eslint-disable-next-line react/sort-comp
debouncedOnChangeText = debounce(async(text) => {
const { sharing } = this.props;
@ -358,9 +363,9 @@ class MessageBox extends Component {
if (!isTextEmpty) {
try {
const { start, end } = this.component?.lastNativeSelection;
const { start, end } = this.selection;
const cursor = Math.max(start, end);
const lastNativeText = this.component?.lastNativeText || '';
const lastNativeText = this.text;
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
let regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
@ -399,7 +404,7 @@ class MessageBox extends Component {
}
const { trackingType } = this.state;
const msg = this.text;
const { start, end } = this.component?.lastNativeSelection;
const { start, end } = this.selection;
const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im;
const result = msg.substr(0, cursor).replace(regexp, '');
@ -410,7 +415,8 @@ class MessageBox extends Component {
if ((trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) && item.providesPreview) {
this.setState({ showCommandPreview: true });
}
this.setInput(text);
const newCursor = cursor + mentionName.length;
this.setInput(text, { start: newCursor, end: newCursor });
this.focus();
requestAnimationFrame(() => this.stopTrackingMention());
}
@ -443,15 +449,11 @@ class MessageBox extends Component {
let newText = '';
// if messagebox has an active cursor
if (this.component?.lastNativeSelection) {
const { start, end } = this.component.lastNativeSelection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else {
// if messagebox doesn't have a cursor, just append selected emoji
newText = `${ text }${ emoji }`;
}
this.setInput(newText);
const { start, end } = this.selection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
const newCursor = cursor + emoji.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
}
@ -551,11 +553,12 @@ class MessageBox extends Component {
this.setState({ commandPreview: [], showCommandPreview: true, command: {} });
}
setInput = (text) => {
setInput = (text, selection) => {
this.text = text;
if (this.component && this.component.setNativeProps) {
this.component.setNativeProps({ text });
if (selection) {
return this.component.setTextAndSelection(text, selection);
}
this.component.setNativeProps({ text });
}
setShowSend = (showSend) => {
@ -582,37 +585,41 @@ class MessageBox extends Component {
}
takePhoto = async() => {
logEvent(events.ROOM_BOX_ACTION_PHOTO);
try {
const image = await ImagePicker.openCamera(this.imagePickerConfig);
if (this.canUploadFile(image)) {
this.openShareView([image]);
}
} catch (e) {
// Do nothing
logEvent(events.ROOM_BOX_ACTION_PHOTO_F);
}
}
takeVideo = async() => {
logEvent(events.ROOM_BOX_ACTION_VIDEO);
try {
const video = await ImagePicker.openCamera(this.videoPickerConfig);
if (this.canUploadFile(video)) {
this.openShareView([video]);
}
} catch (e) {
// Do nothing
logEvent(events.ROOM_BOX_ACTION_VIDEO_F);
}
}
chooseFromLibrary = async() => {
logEvent(events.ROOM_BOX_ACTION_LIBRARY);
try {
const attachments = await ImagePicker.openPicker(this.libraryPickerConfig);
this.openShareView(attachments);
} catch (e) {
// Do nothing
logEvent(events.ROOM_BOX_ACTION_LIBRARY_F);
}
}
chooseFile = async() => {
logEvent(events.ROOM_BOX_ACTION_FILE);
try {
const res = await DocumentPicker.pick({
type: [DocumentPicker.types.allFiles]
@ -628,6 +635,7 @@ class MessageBox extends Component {
}
} catch (e) {
if (!DocumentPicker.isCancel(e)) {
logEvent(events.ROOM_BOX_ACTION_FILE_F);
log(e);
}
}
@ -645,6 +653,7 @@ class MessageBox extends Component {
}
createDiscussion = () => {
logEvent(events.ROOM_BOX_ACTION_DISCUSSION);
const { isMasterDetail } = this.props;
const params = { channel: this.room, showCloseModal: true };
if (isMasterDetail) {
@ -655,6 +664,7 @@ class MessageBox extends Component {
}
showMessageBoxActions = () => {
logEvent(events.ROOM_SHOW_BOX_ACTIONS);
const { showActionSheet } = this.props;
showActionSheet({ options: this.options });
}
@ -665,10 +675,9 @@ class MessageBox extends Component {
this.clearInput();
}
openEmoji = async() => {
await this.setState({
showEmojiKeyboard: true
});
openEmoji = () => {
logEvent(events.ROOM_OPEN_EMOJI);
this.setState({ showEmojiKeyboard: true });
}
recordingCallback = (recording) => {
@ -729,6 +738,7 @@ class MessageBox extends Component {
Q.where('id', Q.like(`${ Q.sanitizeLikeString(command) }%`))
).fetch();
if (slashCommand.length > 0) {
logEvent(events.COMMAND_RUN);
try {
const messageWithoutCommand = message.replace(/([^\s]+)/, '').trim();
const [{ appId }] = slashCommand;
@ -736,6 +746,7 @@ class MessageBox extends Component {
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand, triggerId, tmid || messageTmid);
replyCancel();
} catch (e) {
logEvent(events.COMMAND_RUN_F);
log(e);
}
this.clearInput();
@ -888,6 +899,7 @@ class MessageBox extends Component {
blurOnSubmit={false}
placeholder={I18n.t('New_Message')}
onChangeText={this.onChangeText}
onSelectionChange={this.onSelectionChange}
underlineColorAndroid='transparent'
defaultValue=''
multiline

View File

@ -81,7 +81,7 @@ const MessageErrorActions = forwardRef(({ tmid }, ref) => {
},
{
title: I18n.t('Delete'),
icon: 'trash',
icon: 'delete',
danger: true,
onPress: () => handleDelete(message)
}

View File

@ -10,7 +10,7 @@ import { CustomIcon } from '../../../lib/Icons';
const LockIcon = React.memo(({ theme }) => (
<Row style={styles.row}>
<View style={styles.iconView}>
<CustomIcon name='lock' size={40} color={themes[theme].passcodeLockIcon} />
<CustomIcon name='auth' size={40} color={themes[theme].passcodeLockIcon} />
</View>
</Row>
));

View File

@ -104,7 +104,7 @@ const ModalContent = React.memo(({
<View style={styles.titleContainer}>
<CustomIcon
style={[styles.closeButton, { color: themes[props.theme].buttonText }]}
name='Cross'
name='close'
size={20}
/>
<Text style={[styles.title, { color: themes[props.theme].buttonText }]}>{I18n.t('Reactions')}</Text>

View File

@ -20,19 +20,19 @@ const RoomTypeIcon = React.memo(({
const color = themes[theme].auxiliaryText;
let icon = 'lock';
let icon = 'channel-private';
if (type === 'discussion') {
icon = 'chat';
icon = 'discussions';
} else if (type === 'c') {
icon = 'hash';
icon = 'channel-public';
} else if (type === 'd') {
if (isGroupChat) {
icon = 'team';
} else {
icon = 'at';
icon = 'mention';
}
} else if (type === 'l') {
icon = 'livechat';
icon = 'omnichannel';
}
return (

View File

@ -61,7 +61,7 @@ const SearchBox = ({
]}
>
<View style={[styles.searchBox, { backgroundColor: themes[theme].searchboxBackground }]}>
<CustomIcon name='magnifier' size={14} color={themes[theme].auxiliaryText} />
<CustomIcon name='search' size={14} color={themes[theme].auxiliaryText} />
<TextInput
ref={inputRef}
autoCapitalize='none'

View File

@ -14,7 +14,7 @@ const Status = React.memo(({
borderRadius: size,
width: size,
height: size,
backgroundColor: STATUS_COLORS[status],
backgroundColor: STATUS_COLORS[status] ?? STATUS_COLORS.offline,
borderColor: themes[theme].backgroundColor
}
]}

View File

@ -111,7 +111,7 @@ export default class RCTextInput extends React.PureComponent {
return (
<BorderlessButton onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon
name={showPassword ? 'eye' : 'eye-off'}
name={showPassword ? 'unread-on-top' : 'unread-on-top-disabled'}
testID={testID ? `${ testID }-icon-right` : null}
style={{ color: themes[theme].auxiliaryText }}
size={20}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
import { View, Text, InteractionManager } from 'react-native';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { sha256 } from 'js-sha256';
@ -99,6 +99,7 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }) => {
<TextInput
value={code}
theme={theme}
inputRef={e => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
returnKeyType='send'
autoCapitalize='none'
onChangeText={setCode}

View File

@ -14,9 +14,10 @@ export default StyleSheet.create({
borderRadius: 4
},
title: {
fontSize: 14,
fontSize: 16,
paddingBottom: 8,
...sharedStyles.textBold
...sharedStyles.textBold,
...sharedStyles.textAlignCenter
},
subtitle: {
fontSize: 14,

View File

@ -1,6 +1,6 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import { themes } from '../../../constants/colors';
import { textParser } from '../utils';
@ -24,7 +24,7 @@ const Chip = ({
<>
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
<Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text>
<CustomIcon name='Cross' size={16} color={themes[theme].auxiliaryText} />
<CustomIcon name='close' size={16} color={themes[theme].auxiliaryText} />
</>
</Touchable>
);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Text, FlatList } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import Separator from '../../Separator';
import Check from '../../Check';

View File

@ -82,7 +82,7 @@ export const Overflow = ({
hitSlop={BUTTON_HIT_SLOP}
style={styles.menu}
>
{!loading ? <CustomIcon size={18} name='menu' color={themes[theme].bodyText} /> : <ActivityIndicator style={styles.loading} theme={theme} />}
{!loading ? <CustomIcon size={18} name='kebab' color={themes[theme].bodyText} /> : <ActivityIndicator style={styles.loading} theme={theme} />}
</Touchable>
<Popover
isVisible={show}

View File

@ -5,30 +5,44 @@ import { Text } from 'react-native';
import { themes } from '../../constants/colors';
import styles from './styles';
import { logEvent, events } from '../../utils/log';
const AtMention = React.memo(({
mention, mentions, username, navToRoomInfo, style = [], useRealName, theme
}) => {
let mentionStyle = { ...styles.mention, color: themes[theme].buttonText };
if (mention === 'all' || mention === 'here') {
return <Text style={[mentionStyle, styles.mentionAll, ...style]}>{mention}</Text>;
return (
<Text
style={[
styles.mention,
{
color: themes[theme].mentionGroupColor,
backgroundColor: themes[theme].mentionGroupBackground
},
...style
]}
>{mention}
</Text>
);
}
let mentionStyle = {};
if (mention === username) {
mentionStyle = {
...mentionStyle,
backgroundColor: themes[theme].actionTintColor
color: themes[theme].mentionMeColor,
backgroundColor: themes[theme].mentionMeBackground
};
} else {
mentionStyle = {
...mentionStyle,
color: themes[theme].actionTintColor
color: themes[theme].mentionOtherColor,
backgroundColor: themes[theme].mentionOtherBackground
};
}
const user = mentions && mentions.length && mentions.find(m => m.username === mention);
const user = mentions?.find?.(m => m && m.username === mention);
const handlePress = () => {
logEvent(events.ROOM_MENTION_GO_USER_INFO);
const navParam = {
t: 'd',
rid: user && user._id
@ -39,7 +53,7 @@ const AtMention = React.memo(({
if (user) {
return (
<Text
style={[mentionStyle, ...style]}
style={[styles.mention, mentionStyle, ...style]}
onPress={handlePress}
>
{useRealName && user.name ? user.name : user.username}

View File

@ -21,10 +21,16 @@ const Hashtag = React.memo(({
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {
return (
<Text
style={[styles.mention, ...style]}
style={[
styles.mention,
{
color: themes[theme].mentionOtherColor,
backgroundColor: themes[theme].mentionOtherBackground
},
...style]}
onPress={handlePress}
>
{hashtag}
{`#${ hashtag }`}
</Text>
);
}

View File

@ -383,9 +383,9 @@ class Markdown extends PureComponent {
m = m.replace(/^\[([\s]]*)\]\(([^)]*)\)\s/, '').trim();
if (preview) {
m = m.replace(/\n+/g, ' ');
m = shortnameToUnicode(m);
m = removeMarkdown(m);
m = m.replace(/\n+/g, ' ');
return (
<Text accessibilityLabel={m} style={[styles.text, { color: themes[theme].bodyText }, ...style]} numberOfLines={numberOfLines} testID={testID}>
{m}

View File

@ -54,13 +54,9 @@ export default StyleSheet.create({
temp: { opacity: 0.3 },
mention: {
fontSize: 16,
color: '#0072FE',
padding: 5,
...sharedStyles.textMedium,
backgroundColor: '#E8F2FF'
},
mentionAll: {
backgroundColor: '#FF5B5A'
letterSpacing: 0.5
},
paragraph: {
marginTop: 0,

View File

@ -79,7 +79,7 @@ const Button = React.memo(({
{
loading
? <ActivityIndicator style={[styles.playPauseButton, styles.audioLoading]} theme={theme} />
: <CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
: <CustomIcon name={paused ? 'play-filled' : 'pause-filled'} size={36} color={themes[theme].tintColor} />
}
</Touchable>
));

View File

@ -26,7 +26,7 @@ const Broadcast = React.memo(({
testID='message-broadcast-reply'
>
<>
<CustomIcon name='back' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<CustomIcon name='arrow-back' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Reply')}</Text>
</>
</Touchable>

View File

@ -22,7 +22,7 @@ const CallButton = React.memo(({
hitSlop={BUTTON_HIT_SLOP}
>
<>
<CustomIcon name='video-1' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<CustomIcon name='camera' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
</>
</Touchable>

View File

@ -29,7 +29,7 @@ const Discussion = React.memo(({
hitSlop={BUTTON_HIT_SLOP}
>
<>
<CustomIcon name='chat' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<CustomIcon name='discussions' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
</>
</Touchable>

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import equal from 'deep-equal';
import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress';

View File

@ -23,7 +23,7 @@ const AddReaction = React.memo(({ theme }) => {
hitSlop={BUTTON_HIT_SLOP}
>
<View style={[styles.reactionContainer, { borderColor: themes[theme].borderColor }]}>
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
<CustomIcon name='reaction-add' size={21} color={themes[theme].tintColor} />
</View>
</Touchable>
);

View File

@ -3,7 +3,7 @@ import {
View, Text, StyleSheet, Clipboard
} from 'react-native';
import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import isEqual from 'lodash/isEqual';
import Touchable from './Touchable';

View File

@ -49,7 +49,7 @@ const Video = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)}
>
<CustomIcon
name='play'
name='play-filled'
size={54}
color={themes[theme].buttonText}
/>

View File

@ -483,6 +483,7 @@ export default {
Tags: 'Tags',
Take_a_photo: 'Take a photo',
Take_a_video: 'Take a video',
Take_it: 'Take it!',
tap_to_change_status: 'tap to change status',
Tap_to_view_servers_list: 'Tap to view servers list',
Terms_of_Service: ' Terms of Service ',
@ -621,5 +622,7 @@ export default {
Passcode_app_locked_title: 'App locked',
Passcode_app_locked_subtitle: 'Try again in {{timeLeft}} seconds',
After_seconds_set_by_admin: 'After {{seconds}} seconds (set by admin)',
Dont_activate: 'Don\'t activate now'
Dont_activate: 'Don\'t activate now',
Queued_chats: 'Queued chats',
Queue_is_empty: 'Queue is empty'
};

View File

@ -108,8 +108,10 @@ export default {
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}}?',
Audio: 'Áudio',
Authenticating: 'Autenticando',
Automatic: 'Automático',
Auto_Translate: 'Tradução automática',
Avatar_changed_successfully: 'Avatar alterado com sucesso!',
Avatar_Url: 'Avatar URL',
Away: 'Ausente',
@ -172,6 +174,7 @@ export default {
DELETE: 'EXCLUIR',
deleting_room: 'excluindo sala',
Direct_Messages: 'Mensagens Diretas',
DESKTOP_OPTIONS: 'OPÇÕES DE ÁREA DE TRABALHO',
Directory: 'Diretório',
description: 'descrição',
Description: 'Descrição',
@ -192,6 +195,7 @@ export default {
Email: 'Email',
email: 'e-mail',
Empty_title: 'Título vazio',
Enable_Auto_Translate: 'Ativar a tradução automática',
Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo',
@ -234,6 +238,7 @@ export default {
Message_HideType_room_unarchived: 'Sala desarquivada',
IP: 'IP',
In_app: 'No app',
In_App_and_Desktop_Alert_info: 'Exibe um banner na parte superior da tela quando o aplicativo é aberto e exibe uma notificação na área de trabalho',
Invisible: 'Invisível',
Invite: 'Convidar',
is_typing: 'está digitando',
@ -299,6 +304,9 @@ 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',
Notifications: 'Notificações',
Notification_Duration: 'Duração da notificação',
Notification_Preferences: 'Preferências de notificação',
Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}',
No_available_agents_to_transfer: 'Nenhum agente disponível para transferência',
Offline: 'Offline',
@ -340,6 +348,7 @@ export default {
Profile: 'Perfil',
Public_Channel: 'Canal Público',
Public: 'Público',
Push_Notifications_Alert_Info: 'Essas notificações são entregues a você quando o aplicativo não está aberto',
Quote: 'Citar',
Reactions_are_disabled: 'Reagir está desabilitado',
Reactions_are_enabled: 'Reagir está habilitado',
@ -348,6 +357,8 @@ export default {
Read_External_Permission: 'Permissão de acesso à arquivos',
Read_Only_Channel: 'Canal Somente Leitura',
Read_Only: 'Somente Leitura',
Receive_Group_Mentions: 'Receber menções de grupo',
Receive_Group_Mentions_Info: 'Receber menções @all e @here',
Register: 'Registrar',
Read_Receipt: 'Lida por',
Repeat_Password: 'Repetir Senha',
@ -355,6 +366,8 @@ export default {
replies: 'respostas',
reply: 'resposta',
Reply: 'Responder',
Receive_Notification: 'Receber Notificação',
Receive_notifications_from: 'Receber notificação de {{name}}',
Resend: 'Reenviar',
Reset_password: 'Resetar senha',
resetting_password: 'redefinindo senha',
@ -411,10 +424,13 @@ export default {
Share: 'Compartilhar',
Share_Link: 'Share Link',
Show_more: 'Mostrar mais..',
Show_Unread_Counter: 'Mostrar contador não lido',
Show_Unread_Counter_Info: 'O contador não lido é exibido como um emblema à direita do canal, na lista',
Sign_in_your_server: 'Entrar no seu servidor',
Sign_Up: 'Registrar',
Some_field_is_invalid_or_empty: 'Algum campo está inválido ou vazio',
Sorting_by: 'Ordenando por {{key}}',
Sound: 'Som da notificação',
Star_room: 'Favoritar sala',
Star: 'Favorito',
Starred_Messages: 'Mensagens Favoritas',
@ -551,5 +567,7 @@ export default {
Passcode_app_locked_title: 'Aplicativo bloqueado',
Passcode_app_locked_subtitle: 'Tente novamente em {{timeLeft}} segundos',
After_seconds_set_by_admin: 'Após {{seconds}} segundos (Configurado pelo adm)',
Dont_activate: 'Não ativar agora'
Dont_activate: 'Não ativar agora',
Queued_chats: 'Bate-papos na fila',
Queue_is_empty: 'A fila está vazia'
};

View File

@ -50,6 +50,13 @@ const parseDeepLinking = (url) => {
return parseQuery(url);
}
}
const call = /^(https:\/\/)?jitsi.rocket.chat\//;
if (url.match(call)) {
url = url.replace(call, '').trim();
if (url) {
return { path: url, isCall: true };
}
}
}
return null;
};

View File

@ -25,4 +25,6 @@ export default class Server extends Model {
@field('auto_lock_time') autoLockTime;
@field('biometry') biometry;
@field('unique_id') uniqueID;
}

View File

@ -40,6 +40,8 @@ export default class Subscription extends Model {
@field('user_mentions') userMentions;
@field('group_mentions') groupMentions;
@date('room_updated_at') roomUpdatedAt;
@field('ro') ro;

View File

@ -118,6 +118,17 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 9,
steps: [
addColumns({
table: 'subscriptions',
columns: [
{ name: 'group_mentions', type: 'number', isOptional: true }
]
})
]
}
]
});

View File

@ -26,6 +26,17 @@ export default schemaMigrations({
]
})
]
},
{
toVersion: 5,
steps: [
addColumns({
table: 'servers',
columns: [
{ name: 'unique_id', type: 'string', isOptional: true }
]
})
]
}
]
});

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 8,
version: 9,
tables: [
tableSchema({
name: 'subscriptions',
@ -19,6 +19,7 @@ export default appSchema({
{ name: 'roles', type: 'string', isOptional: true },
{ name: 'unread', type: 'number' },
{ name: 'user_mentions', type: 'number' },
{ name: 'group_mentions', type: 'number' },
{ name: 'room_updated_at', type: 'number' },
{ name: 'ro', type: 'boolean' },
{ name: 'last_open', type: 'number', isOptional: true },

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 4,
version: 5,
tables: [
tableSchema({
name: 'users',
@ -28,7 +28,8 @@ export default appSchema({
{ name: 'last_local_authenticated_session', type: 'number', isOptional: true },
{ name: 'auto_lock', type: 'boolean', isOptional: true },
{ name: 'auto_lock_time', type: 'number', isOptional: true },
{ name: 'biometry', type: 'boolean', isOptional: true }
{ name: 'biometry', type: 'boolean', isOptional: true },
{ name: 'unique_id', type: 'string', isOptional: true }
]
})
]

View File

@ -1,41 +1,41 @@
import reduxStore from '../createStore';
import Navigation from '../Navigation';
import { logEvent, events } from '../../utils/log';
async function jitsiURL({ rid }) {
const { settings } = reduxStore.getState();
const { Jitsi_Enabled } = settings;
const jitsiBaseUrl = ({
Jitsi_Enabled, Jitsi_SSL, Jitsi_Domain, Jitsi_URL_Room_Prefix, uniqueID
}) => {
if (!Jitsi_Enabled) {
return '';
}
const uniqueIdentifier = uniqueID || 'undefined';
const domain = Jitsi_Domain;
const {
Jitsi_Domain, Jitsi_URL_Room_Prefix, Jitsi_SSL, Jitsi_Enabled_TokenAuth, uniqueID
} = settings;
const domain = `${ Jitsi_Domain }/`;
const prefix = Jitsi_URL_Room_Prefix;
const uniqueIdentifier = uniqueID || 'undefined';
const protocol = Jitsi_SSL ? 'https://' : 'http://';
const urlProtocol = Jitsi_SSL ? 'https://' : 'http://';
const urlDomain = `${ domain }/`;
return `${ urlProtocol }${ urlDomain }${ prefix }${ uniqueIdentifier }`;
};
async function callJitsi(rid, onlyAudio = false) {
let accessToken;
let queryString = '';
const { settings } = reduxStore.getState();
const { Jitsi_Enabled_TokenAuth } = settings;
if (Jitsi_Enabled_TokenAuth) {
try {
accessToken = await this.methodCallWrapper('jitsi:generateAccessToken', rid);
} catch (e) {
// do nothing
const accessToken = await this.methodCallWrapper('jitsi:generateAccessToken', rid);
queryString = `?jwt=${ accessToken }`;
} catch {
logEvent(events.RA_JITSI_F);
}
}
if (accessToken) {
queryString = `?jwt=${ accessToken }`;
}
return `${ protocol }${ domain }${ prefix }${ uniqueIdentifier }${ rid }${ queryString }`;
}
Navigation.navigate('JitsiMeetView', { url: `${ jitsiBaseUrl(settings) }${ rid }${ queryString }`, onlyAudio, rid });
async function callJitsi(rid, onlyAudio = false) {
logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
const url = await jitsiURL.call(this, { rid });
Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid });
}
export default callJitsi;

View File

@ -1,4 +1,5 @@
import database from '../database';
import store from '../createStore';
const restTypes = {
channel: 'channels', direct: 'im', group: 'groups'
@ -53,11 +54,17 @@ async function open({ type, rid, name }) {
}
}
export default async function canOpenRoom({ rid, path }) {
export default async function canOpenRoom({ rid, path, isCall }) {
try {
const db = database.active;
const subsCollection = db.collections.get('subscriptions');
const [type, name] = path.split('/');
if (isCall && !rid) {
// Extract rid from a Jitsi URL
// Eg.: [Jitsi_URL_Room_Prefix][uniqueID][rid][?jwt]
const { Jitsi_URL_Room_Prefix, uniqueID } = store.getState().settings;
rid = path.replace(`${ Jitsi_URL_Room_Prefix }${ uniqueID }`, '').replace(/\?(.*)/g, '');
}
if (rid) {
try {
@ -75,8 +82,10 @@ export default async function canOpenRoom({ rid, path }) {
}
}
const [type, name] = path.split('/');
try {
return await open.call(this, { type, rid, name });
const result = await open.call(this, { type, rid, name });
return result;
} catch (e) {
return false;
}

View File

@ -11,7 +11,7 @@ import protectedFunction from './helpers/protectedFunction';
import fetch from '../../utils/fetch';
import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication';
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After'];
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After', 'uniqueID'];
// these settings are used only on onboarding process
const loginSettings = [
@ -68,6 +68,9 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
return { ...allSettings, autoLockTime: setting.valueAsNumber };
}
}
if (setting._id === 'uniqueID') {
return { ...allSettings, uniqueID: setting.valueAsString };
}
return allSettings;
}, {});

View File

@ -0,0 +1,95 @@
import log from '../../../utils/log';
import store from '../../createStore';
import RocketChat from '../../rocketchat';
import {
inquiryRequest,
inquiryQueueAdd,
inquiryQueueUpdate,
inquiryQueueRemove
} from '../../../actions/inquiry';
const removeListener = listener => listener.stop();
let connectedListener;
let disconnectedListener;
let queueListener;
const streamTopic = 'stream-livechat-inquiry-queue-observer';
export default function subscribeInquiry() {
const handleConnection = () => {
store.dispatch(inquiryRequest());
};
const handleQueueMessageReceived = (ddpMessage) => {
const [{ type, ...sub }] = ddpMessage.fields.args;
// added can be ignored, since it is handled by 'changed' event
if (/added/.test(type)) {
return;
}
// if the sub isn't on the queue anymore
if (sub.status !== 'queued') {
// remove it from the queue
store.dispatch(inquiryQueueRemove(sub._id));
return;
}
const { queued } = store.getState().inquiry;
// check if this sub is on the current queue
const idx = queued.findIndex(item => item._id === sub._id);
if (idx >= 0) {
// if it is on the queue let's update
store.dispatch(inquiryQueueUpdate(sub));
} else {
// if it is not on the queue let's add
store.dispatch(inquiryQueueAdd(sub));
}
};
const stop = () => {
if (connectedListener) {
connectedListener.then(removeListener);
connectedListener = false;
}
if (disconnectedListener) {
disconnectedListener.then(removeListener);
disconnectedListener = false;
}
if (queueListener) {
queueListener.then(removeListener);
queueListener = false;
}
};
connectedListener = this.sdk.onStreamData('connected', handleConnection);
disconnectedListener = this.sdk.onStreamData('close', handleConnection);
queueListener = this.sdk.onStreamData(streamTopic, handleQueueMessageReceived);
try {
const { user } = store.getState().login;
RocketChat.getAgentDepartments(user.id).then((result) => {
if (result.success) {
const { departments } = result;
if (!departments.length || RocketChat.hasRole('livechat-manager')) {
this.sdk.subscribe(streamTopic, 'public').catch(e => console.log(e));
}
const departmentIds = departments.map(({ departmentId }) => departmentId);
departmentIds.forEach((departmentId) => {
// subscribe to all departments of the agent
this.sdk.subscribe(streamTopic, `department/${ departmentId }`).catch(e => console.log(e));
});
}
});
return {
stop: () => stop()
};
} catch (e) {
log(e);
return Promise.reject();
}
}

View File

@ -20,6 +20,7 @@ import {
} from '../actions/share';
import subscribeRooms from './methods/subscriptions/rooms';
import subscribeInquiry from './methods/subscriptions/inquiry';
import getUsersPresence, { getUserPresence, subscribeUsersPresence } from './methods/getUsersPresence';
import protectedFunction from './methods/helpers/protectedFunction';
@ -72,6 +73,15 @@ const RocketChat = {
}
}
},
async subscribeInquiry() {
if (!this.inquirySub) {
try {
this.inquirySub = await subscribeInquiry.call(this);
} catch (e) {
log(e);
}
}
},
canOpenRoom,
createChannel({
name, users, type, readOnly, broadcast
@ -203,6 +213,11 @@ const RocketChat = {
this.roomsSub = null;
}
if (this.inquirySub) {
this.inquirySub.stop();
this.inquirySub = null;
}
if (this.sdk) {
this.sdk.disconnect();
this.sdk = null;
@ -680,7 +695,7 @@ const RocketChat = {
c: 'channel',
d: 'direct'
}[room.t];
return `${ server }/${ roomType }/${ room.name }?msg=${ message.id }`;
return `${ server }/${ roomType }/${ this.isGroupChat(room) ? room.rid : room.name }?msg=${ message.id }`;
},
getPermalinkChannel(channel) {
const { server } = reduxStore.getState().server;
@ -816,7 +831,7 @@ const RocketChat = {
},
getAgentDepartments(uid) {
// RC 2.4.0
return this.sdk.get(`livechat/agents/${ uid }/departments`);
return this.sdk.get(`livechat/agents/${ uid }/departments?enabledDepartmentsOnly=true`);
},
getCustomFields() {
// RC 2.2.0
@ -826,6 +841,16 @@ const RocketChat = {
// RC 0.26.0
return this.methodCallWrapper('livechat:changeLivechatStatus');
},
getInquiriesQueued() {
// RC 2.4.0
return this.sdk.get('livechat/inquiries.queued');
},
takeInquiry(inquiryId) {
// this inquiry is added to the db by the subscriptions stream
// and will be removed by the queue stream
// RC 2.4.0
return this.methodCallWrapper('livechat:takeInquiry', inquiryId);
},
getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user;
@ -845,6 +870,12 @@ const RocketChat = {
return other && other.length ? other[0] : me;
},
isRead(item) {
let isUnread = item.archived !== true && item.open === true; // item is not archived and not opened
isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert
return !isUnread;
},
isGroupChat(room) {
return (room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2);
},
@ -957,6 +988,14 @@ const RocketChat = {
// RC 0.47.0
return this.sdk.get('chat.getMessage', { msgId });
},
hasRole(role) {
const shareUser = reduxStore.getState().share.user;
const loginUser = reduxStore.getState().login.user;
// get user roles on the server from redux
const userRoles = (shareUser?.roles || loginUser?.roles) || [];
return userRoles.indexOf(r => r === role) > -1;
},
async hasPermission(permissions, rid) {
const db = database.active;
const subsCollection = db.collections.get('subscriptions');

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@ export const onNotification = (notification) => {
if (data) {
try {
const {
rid, name, sender, type, host
rid, name, sender, type, host, messageType
} = EJSON.parse(data.ejson);
const types = {
@ -24,7 +24,8 @@ export const onNotification = (notification) => {
const params = {
host,
rid,
path: `${ types[type] }/${ roomName }`
path: `${ types[type] }/${ roomName }`,
isCall: messageType === 'jitsi_call_started'
};
store.dispatch(deepLinkingOpen(params));
} catch (e) {

View File

@ -6,7 +6,7 @@ export const ImageComponent = (type) => {
const { Image } = require('react-native');
Component = Image;
} else {
const FastImage = require('react-native-fast-image').default;
const FastImage = require('@rocket.chat/react-native-fast-image').default;
Component = FastImage;
}
return Component;

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view';
import scrollPersistTaps from '../utils/scrollPersistTaps';
export default class KeyboardView extends React.PureComponent {

View File

@ -107,7 +107,7 @@ export const RightActions = React.memo(({
>
<RectButton style={[styles.actionButton, { backgroundColor: themes[theme].hideBackground }]} onPress={onHidePress}>
<>
<CustomIcon size={20} name='eye-off' color={themes[theme].buttonText} />
<CustomIcon size={20} name='unread-on-top-disabled' color={themes[theme].buttonText} />
<Text style={[styles.actionText, { color: themes[theme].buttonText }]}>{I18n.t('Hide')}</Text>
</>
</RectButton>

View File

@ -0,0 +1,190 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import styles from './styles';
import Wrapper from './Wrapper';
import UnreadBadge from '../UnreadBadge';
import TypeIcon from './TypeIcon';
import LastMessage from './LastMessage';
import Title from './Title';
import UpdatedAt from './UpdatedAt';
import Touchable from './Touchable';
const RoomItem = ({
rid,
type,
prid,
name,
avatar,
width,
avatarSize,
baseUrl,
userId,
username,
token,
showLastMessage,
status,
useRealName,
theme,
isFocused,
isGroupChat,
isRead,
date,
accessibilityLabel,
favorite,
lastMessage,
alert,
hideUnreadStatus,
unread,
userMentions,
groupMentions,
roomUpdatedAt,
testID,
swipeEnabled,
onPress,
toggleFav,
toggleRead,
hideChannel
}) => (
<Touchable
onPress={onPress}
width={width}
favorite={favorite}
toggleFav={toggleFav}
isRead={isRead}
rid={rid}
toggleRead={toggleRead}
hideChannel={hideChannel}
testID={testID}
type={type}
theme={theme}
isFocused={isFocused}
swipeEnabled={swipeEnabled}
>
<Wrapper
accessibilityLabel={accessibilityLabel}
avatar={avatar}
avatarSize={avatarSize}
type={type}
baseUrl={baseUrl}
userId={userId}
token={token}
theme={theme}
>
{showLastMessage
? (
<>
<View style={styles.titleContainer}>
<TypeIcon
type={type}
prid={prid}
status={status}
isGroupChat={isGroupChat}
theme={theme}
/>
<Title
name={name}
theme={theme}
hideUnreadStatus={hideUnreadStatus}
alert={alert}
/>
<UpdatedAt
roomUpdatedAt={roomUpdatedAt}
date={date}
theme={theme}
hideUnreadStatus={hideUnreadStatus}
alert={alert}
/>
</View>
<View style={styles.row}>
<LastMessage
lastMessage={lastMessage}
type={type}
showLastMessage={showLastMessage}
username={username}
alert={alert && !hideUnreadStatus}
useRealName={useRealName}
theme={theme}
/>
<UnreadBadge
unread={unread}
userMentions={userMentions}
groupMentions={groupMentions}
theme={theme}
/>
</View>
</>
)
: (
<View style={[styles.titleContainer, styles.flex]}>
<TypeIcon
type={type}
prid={prid}
status={status}
isGroupChat={isGroupChat}
theme={theme}
/>
<Title
name={name}
theme={theme}
hideUnreadStatus={hideUnreadStatus}
alert={alert}
/>
<UnreadBadge
unread={unread}
userMentions={userMentions}
groupMentions={groupMentions}
theme={theme}
/>
</View>
)
}
</Wrapper>
</Touchable>
);
RoomItem.propTypes = {
rid: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
prid: PropTypes.string,
name: PropTypes.string.isRequired,
avatar: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool,
userId: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string,
avatarSize: PropTypes.number,
testID: PropTypes.string,
width: PropTypes.number,
status: PropTypes.string,
useRealName: PropTypes.bool,
theme: PropTypes.string,
isFocused: PropTypes.bool,
isGroupChat: PropTypes.bool,
isRead: PropTypes.bool,
date: PropTypes.string,
accessibilityLabel: PropTypes.string,
lastMessage: PropTypes.object,
favorite: PropTypes.bool,
alert: PropTypes.bool,
hideUnreadStatus: PropTypes.bool,
unread: PropTypes.number,
userMentions: PropTypes.number,
groupMentions: PropTypes.number,
roomUpdatedAt: PropTypes.instanceOf(Date),
swipeEnabled: PropTypes.bool,
toggleFav: PropTypes.func,
toggleRead: PropTypes.func,
onPress: PropTypes.func,
hideChannel: PropTypes.func
};
RoomItem.defaultProps = {
avatarSize: 48,
status: 'offline',
swipeEnabled: true
};
export default RoomItem;

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../constants/colors';
const Title = React.memo(({
name, theme, hideUnreadStatus, alert
}) => (
<Text
style={[
styles.title,
alert && !hideUnreadStatus && styles.alert,
{ color: themes[theme].titleText }
]}
ellipsizeMode='tail'
numberOfLines={1}
>
{name}
</Text>
));
Title.propTypes = {
name: PropTypes.string,
theme: PropTypes.string,
hideUnreadStatus: PropTypes.bool,
alert: PropTypes.bool
};
export default Title;

View File

@ -26,7 +26,8 @@ class Touchable extends React.Component {
hideChannel: PropTypes.func,
children: PropTypes.element,
theme: PropTypes.string,
isFocused: PropTypes.bool
isFocused: PropTypes.bool,
swipeEnabled: PropTypes.bool
}
constructor(props) {
@ -168,7 +169,7 @@ class Touchable extends React.Component {
render() {
const {
testID, isRead, width, favorite, children, theme, isFocused
testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled
} = this.props;
return (
@ -177,6 +178,7 @@ class Touchable extends React.Component {
minDeltaX={20}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange}
enabled={swipeEnabled}
>
<Animated.View>
<LeftActions

View File

@ -1,44 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
const UnreadBadge = React.memo(({
theme, unread, userMentions, type
}) => {
if (!unread || unread <= 0) {
return;
}
if (unread >= 1000) {
unread = '999+';
}
const mentioned = userMentions > 0 && type !== 'd';
return (
<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
};
export default UnreadBadge;

View File

@ -0,0 +1,49 @@
import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../constants/colors';
import { capitalize } from '../../utils/room';
const UpdatedAt = React.memo(({
roomUpdatedAt, date, theme, hideUnreadStatus, alert
}) => {
if (!roomUpdatedAt) {
return null;
}
return (
<Text
style={[
styles.date,
{
color:
themes[theme]
.auxiliaryText
},
alert && !hideUnreadStatus && [
styles.updateAlert,
{
color:
themes[theme]
.tintColor
}
]
]}
ellipsizeMode='tail'
numberOfLines={1}
>
{capitalize(date)}
</Text>
);
});
UpdatedAt.propTypes = {
roomUpdatedAt: PropTypes.instanceOf(Date),
date: PropTypes.string,
theme: PropTypes.string,
hideUnreadStatus: PropTypes.bool,
alert: PropTypes.bool
};
export default UpdatedAt;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../constants/colors';
import Avatar from '../../containers/Avatar';
const RoomItemInner = ({
accessibilityLabel,
avatar,
avatarSize,
type,
baseUrl,
userId,
token,
theme,
children
}) => (
<View
style={styles.container}
accessibilityLabel={accessibilityLabel}
>
<Avatar
text={avatar}
size={avatarSize}
type={type}
baseUrl={baseUrl}
style={styles.avatar}
userId={userId}
token={token}
/>
<View
style={[
styles.centerContainer,
{
borderColor: themes[theme].separatorColor
}
]}
>
{children}
</View>
</View>
);
RoomItemInner.propTypes = {
accessibilityLabel: PropTypes.string,
avatar: PropTypes.string,
avatarSize: PropTypes.number,
type: PropTypes.string,
baseUrl: PropTypes.string,
userId: PropTypes.string,
token: PropTypes.string,
theme: PropTypes.string,
children: PropTypes.element
};
export default RoomItemInner;

View File

@ -1,97 +1,89 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import { connect } from 'react-redux';
import Avatar from '../../containers/Avatar';
import I18n from '../../i18n';
import styles, { ROW_HEIGHT } from './styles';
import UnreadBadge from './UnreadBadge';
import TypeIcon from './TypeIcon';
import LastMessage from './LastMessage';
import { capitalize, formatDate } from '../../utils/room';
import Touchable from './Touchable';
import { themes } from '../../constants/colors';
import { ROW_HEIGHT } from './styles';
import { formatDate } from '../../utils/room';
import RoomItem from './RoomItem';
export { ROW_HEIGHT };
const attrs = [
'name',
'unread',
'userMentions',
'showLastMessage',
'useRealName',
'alert',
'type',
'width',
'isRead',
'favorite',
'status',
'connected',
'theme',
'isFocused'
'isFocused',
'forceUpdate',
'showLastMessage'
];
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 arePropsEqual = (oldProps, newProps) => attrs.every(key => oldProps[key] === newProps[key]);
const RoomItem = React.memo(({
const RoomItemContainer = React.memo(({
item,
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,
useRealName,
getUserPresence,
isGroupChat,
connected,
theme,
isFocused
isFocused,
getRoomTitle,
getRoomAvatar,
getIsGroupChat,
getIsRead,
swipeEnabled
}) => {
const [, setForceUpdate] = useState(1);
useEffect(() => {
if (connected && type === 'd' && id) {
if (connected && item.t === 'd' && id) {
getUserPresence(id);
}
}, [connected]);
const date = lastMessage && formatDate(lastMessage.ts);
useEffect(() => {
if (item?.observe) {
const observable = item.observe();
const subscription = observable?.subscribe?.(() => {
setForceUpdate(prevForceUpdate => prevForceUpdate + 1);
});
return () => {
subscription?.unsubscribe?.();
};
}
}, []);
const name = getRoomTitle(item);
const avatar = getRoomAvatar(item);
const isGroupChat = getIsGroupChat(item);
const isRead = getIsRead(item);
const _onPress = () => onPress(item);
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
let accessibilityLabel = name;
if (unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
} else if (unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
if (item.unread === 1) {
accessibilityLabel += `, ${ item.unread } ${ I18n.t('alert') }`;
} else if (item.unread > 1) {
accessibilityLabel += `, ${ item.unread } ${ I18n.t('alerts') }`;
}
if (userMentions > 0) {
if (item.userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
}
@ -100,120 +92,50 @@ const RoomItem = React.memo(({
}
return (
<Touchable
onPress={onPress}
width={width}
favorite={favorite}
toggleFav={toggleFav}
<RoomItem
name={name}
avatar={avatar}
isGroupChat={isGroupChat}
isRead={isRead}
rid={rid}
onPress={_onPress}
date={date}
accessibilityLabel={accessibilityLabel}
userMentions={item.userMentions}
width={width}
favorite={item.f}
toggleFav={toggleFav}
rid={item.rid}
toggleRead={toggleRead}
hideChannel={hideChannel}
testID={testID}
type={type}
type={item.t}
theme={theme}
isFocused={isFocused}
>
<View
style={styles.container}
accessibilityLabel={accessibilityLabel}
>
<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}
prid={prid}
status={status}
isGroupChat={isGroupChat}
theme={theme}
/>
<Text
style={[
styles.title,
alert && !hideUnreadStatus && styles.alert,
{ color: themes[theme].titleText }
]}
ellipsizeMode='tail'
numberOfLines={1}
>
{name}
</Text>
{_updatedAt ? (
<Text
style={[
styles.date,
{
color:
themes[theme]
.auxiliaryText
},
alert && !hideUnreadStatus && [
styles.updateAlert,
{
color:
themes[theme]
.tintColor
}
]
]}
ellipsizeMode='tail'
numberOfLines={1}
>
{capitalize(date)}
</Text>
) : null}
</View>
<View style={styles.row}>
<LastMessage
lastMessage={lastMessage}
type={type}
showLastMessage={showLastMessage}
username={username}
alert={alert && !hideUnreadStatus}
useRealName={useRealName}
theme={theme}
/>
<UnreadBadge
unread={unread}
userMentions={userMentions}
type={type}
theme={theme}
/>
</View>
</View>
</View>
</Touchable>
size={avatarSize}
baseUrl={baseUrl}
userId={userId}
token={token}
prid={item.prid}
status={status}
hideUnreadStatus={item.hideUnreadStatus}
alert={item.alert}
roomUpdatedAt={item.roomUpdatedAt}
lastMessage={item.lastMessage}
showLastMessage={showLastMessage}
username={username}
useRealName={useRealName}
unread={item.unread}
groupMentions={item.groupMentions}
swipeEnabled={swipeEnabled}
/>
);
}, arePropsEqual);
RoomItem.propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
RoomItemContainer.propTypes = {
item: PropTypes.object.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,
@ -221,27 +143,31 @@ RoomItem.propTypes = {
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,
useRealName: PropTypes.bool,
getUserPresence: PropTypes.func,
connected: PropTypes.bool,
isGroupChat: PropTypes.bool,
theme: PropTypes.string,
isFocused: PropTypes.bool
isFocused: PropTypes.bool,
getRoomTitle: PropTypes.func,
getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func,
getIsRead: PropTypes.func,
swipeEnabled: PropTypes.bool
};
RoomItem.defaultProps = {
RoomItemContainer.defaultProps = {
avatarSize: 48,
status: 'offline',
getUserPresence: () => {}
getUserPresence: () => {},
getRoomTitle: () => 'title',
getRoomAvatar: () => '',
getIsGroupChat: () => false,
getIsRead: () => false,
swipeEnabled: true
};
const mapStateToProps = (state, ownProps) => {
@ -260,4 +186,4 @@ const mapStateToProps = (state, ownProps) => {
};
};
export default connect(mapStateToProps)(RoomItem);
export default connect(mapStateToProps)(RoomItemContainer);

View File

@ -8,6 +8,9 @@ export const SMALL_SWIPE = ACTION_WIDTH / 2;
export const LONG_SWIPE = ACTION_WIDTH * 3;
export default StyleSheet.create({
flex: {
flex: 1
},
container: {
flexDirection: 'row',
alignItems: 'center',
@ -48,23 +51,6 @@ export default StyleSheet.create({
updateAlert: {
...sharedStyles.textSemibold
},
unreadNumberContainer: {
minWidth: 21,
height: 21,
paddingVertical: 3,
paddingHorizontal: 5,
borderRadius: 10.5,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 10
},
unreadText: {
overflow: 'hidden',
fontSize: 13,
...sharedStyles.textMedium,
letterSpacing: 0.56,
textAlign: 'center'
},
status: {
marginLeft: 4,
marginRight: 7,

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import FastImage from 'react-native-fast-image';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touch from '../../utils/touch';
import Check from '../../containers/Check';

View File

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
const styles = StyleSheet.create({
unreadNumberContainer: {
minWidth: 21,
height: 21,
paddingVertical: 3,
paddingHorizontal: 5,
borderRadius: 10.5,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 10
},
unreadText: {
overflow: 'hidden',
fontSize: 13,
...sharedStyles.textMedium,
letterSpacing: 0.56,
textAlign: 'center'
}
});
const UnreadBadge = React.memo(({
theme, unread, userMentions, groupMentions, style
}) => {
if (!unread || unread <= 0) {
return;
}
if (unread >= 1000) {
unread = '999+';
}
let backgroundColor = themes[theme].unreadBackground;
const color = themes[theme].buttonText;
if (userMentions > 0) {
backgroundColor = themes[theme].mentionMeColor;
} else if (groupMentions > 0) {
backgroundColor = themes[theme].mentionGroupColor;
}
return (
<View
style={[
styles.unreadNumberContainer,
{ backgroundColor },
style
]}
>
<Text
style={[
styles.unreadText,
{ color }
]}
>{unread}
</Text>
</View>
);
});
UnreadBadge.propTypes = {
theme: PropTypes.string,
unread: PropTypes.number,
userMentions: PropTypes.number,
groupMentions: PropTypes.number,
style: PropTypes.object
};
export default UnreadBadge;

View File

@ -1,13 +1,14 @@
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import {
Text, View, StyleSheet, Pressable
} from 'react-native';
import PropTypes from 'prop-types';
import Avatar from '../containers/Avatar';
import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
import Touch from '../utils/touch';
import LongPress from '../utils/longPress';
import { isIOS } from '../utils/deviceInfo';
const styles = StyleSheet.create({
button: {
@ -43,23 +44,28 @@ const styles = StyleSheet.create({
const UserItem = ({
name, username, onPress, testID, onLongPress, style, icon, baseUrl, user, theme
}) => (
<LongPress onLongPress={onLongPress}>
<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 }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
</View>
{icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
<Pressable
onPress={onPress}
onLongPress={onLongPress}
testID={testID}
android_ripple={{
color: themes[theme].bannerBackground
}}
style={({ pressed }) => ({
backgroundColor: isIOS && pressed
? themes[theme].bannerBackground
: 'transparent'
})}
>
<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 }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
</View>
</Touch>
</LongPress>
{icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
</View>
</Pressable>
);
UserItem.propTypes = {

View File

@ -16,6 +16,7 @@ import activeUsers from './activeUsers';
import usersTyping from './usersTyping';
import inviteLinks from './inviteLinks';
import createDiscussion from './createDiscussion';
import inquiry from './inquiry';
export default combineReducers({
settings,
@ -34,5 +35,6 @@ export default combineReducers({
activeUsers,
usersTyping,
inviteLinks,
createDiscussion
createDiscussion,
inquiry
});

51
app/reducers/inquiry.js Normal file
View File

@ -0,0 +1,51 @@
import { INQUIRY } from '../actions/actionsTypes';
const initialState = {
enabled: false,
queued: [],
error: {}
};
export default function inquiry(state = initialState, action) {
switch (action.type) {
case INQUIRY.SUCCESS:
return {
...state,
queued: action.inquiries
};
case INQUIRY.FAILURE:
return {
...state,
error: action.error
};
case INQUIRY.SET_ENABLED:
return {
...state,
enabled: action.enabled
};
case INQUIRY.QUEUE_ADD:
return {
...state,
queued: [...state.queued, action.inquiry]
};
case INQUIRY.QUEUE_UPDATE:
return {
...state,
queued: state.queued.map((item) => {
if (item._id === action.inquiry._id) {
return action.inquiry;
}
return item;
})
};
case INQUIRY.QUEUE_REMOVE:
return {
...state,
queued: state.queued.filter(({ _id }) => _id !== action.inquiryId)
};
case INQUIRY.RESET:
return initialState;
default:
return state;
}
}

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