Compare commits

..

26 Commits

Author SHA1 Message Date
Danish 2c73429ff8 Refactoring 2022-08-26 19:51:09 +05:30
Danish f1658c146a Use react-native-tab-view in ReactionsList 2022-08-26 19:51:09 +05:30
Danish 359171ed03 Migrate to react-native-tab-view 2022-08-26 19:51:09 +05:30
Danish 0f51a3c5df Refactoring and style fixes 2022-08-26 19:51:09 +05:30
Danish 8390a4c582 Add detox tests 2022-08-26 19:51:09 +05:30
Danish 83f0edefcc Tweaks 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza d9fa16977e Add prop enableContentPanningGesture to action sheet 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 7c43029bf0 Fix blank keyboard on switching back from emoji keyboard 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 37ac131618 Tweaks and optimization 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 086b98f8fb Fix FlatList not working properly in ActionSheet 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 7d64b262cf Fix hardware backpress behavior and remove redundant code 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 10f074eb02 Remove wrong use of memo and some requested changes 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 64afe08fe6 Update tabBar icons 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza e014777c9e useMemo performance optamizations and other requested changes 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 4c130c0b0b Fix searchbar height in reaction picker 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza d4bc6a078f Fix emoji search return type 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 28f869a80c Fix reaction picker not opening 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza b05d876946 New reaction picker as bottom action sheet 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza de9036edeb Close EmojiSearchbar on message input focus 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 036066cd9a Remove EmojiPicker footer from Reaction Picker 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza f9164cf0f1 Add ListEmptyComponent 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza e44c3d1ffd Remove redundant code 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 99db6d824d Fix keyboard dismiss on emoji press 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 8ea5755345 Add searchbar for Emojis 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 349c56ba45 Add backspace button and same number of columns as category tabs 2022-08-26 19:51:09 +05:30
Danish Ahmed Mirza 597a6836e6 Migrate EmojiPicker to hooks 2022-08-26 19:51:09 +05:30
468 changed files with 11243 additions and 14696 deletions

View File

@ -1,12 +1,9 @@
defaults: &defaults defaults: &defaults
working_directory: ~/repo working_directory: ~/repo
orbs:
android: circleci/android@2.1.2
macos: &macos macos: &macos
macos: macos:
xcode: "14.2.0" xcode: "13.3.0"
resource_class: large resource_class: large
bash-env: &bash-env bash-env: &bash-env
@ -54,14 +51,14 @@ save-gems-cache: &save-gems-cache
update-fastlane-ios: &update-fastlane-ios update-fastlane-ios: &update-fastlane-ios
name: Update Fastlane name: Update Fastlane
command: | command: |
echo "ruby-2.7.7" > ~/.ruby-version echo "ruby-2.6.4" > ~/.ruby-version
bundle install bundle install
working_directory: ios working_directory: ios
update-fastlane-android: &update-fastlane-android update-fastlane-android: &update-fastlane-android
name: Update Fastlane name: Update Fastlane
command: | command: |
echo "ruby-2.7.7" > ~/.ruby-version echo "ruby-2.6.4" > ~/.ruby-version
bundle install bundle install
working_directory: android working_directory: android
@ -121,26 +118,26 @@ commands:
if [[ $CIRCLE_JOB == "android-build-official" ]]; then if [[ $CIRCLE_JOB == "android-build-official" ]]; then
echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY_OFFICIAL" >> ./gradle.properties echo -e "BugsnagAPIKey=$BUGSNAG_KEY_OFFICIAL" >> ./gradle.properties
echo $KEYSTORE_OFFICIAL_BASE64 | base64 --decode > ./app/$KEYSTORE_OFFICIAL echo $CHAT_ROCKET_ANDROID_STORE_FILE_BASE64_JKS | base64 --decode > ./app/$KEYSTORE_OFFICIAL
echo -e "KEYSTORE=$KEYSTORE_OFFICIAL" >> ./gradle.properties echo -e "KEYSTORE=$KEYSTORE_OFFICIAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties echo -e "KEYSTORE_PASSWORD=$CHAT_ROCKET_ANDROID_STORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_OFFICIAL_ALIAS" >> ./gradle.properties echo -e "KEY_ALIAS=$CHAT_ROCKET_ANDROID_KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties echo -e "KEY_PASSWORD=$CHAT_ROCKET_ANDROID_KEY_PASSWORD" >> ./gradle.properties
else else
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
echo $KEYSTORE_EXPERIMENTAL_BASE64 | base64 --decode > ./app/$KEYSTORE_EXPERIMENTAL echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
echo -e "KEYSTORE=$KEYSTORE_EXPERIMENTAL" >> ./gradle.properties echo -e "KEYSTORE=$KEYSTORE" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties echo -e "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_EXPERIMENTAL_ALIAS" >> ./gradle.properties echo -e "KEY_ALIAS=$KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties echo -e "KEY_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
fi fi
working_directory: android working_directory: android
- run: - run:
name: Set Google Services name: Set Google Services
command: | command: |
if [[ $GOOGLE_SERVICES_ANDROID ]]; then if [[ $KEYSTORE ]]; then
echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json
fi fi
working_directory: android/app working_directory: android/app
@ -154,7 +151,7 @@ commands:
if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then
./gradlew bundleExperimentalPlayRelease ./gradlew bundleExperimentalPlayRelease
fi fi
if [[ ! $GOOGLE_SERVICES_ANDROID ]]; then if [[ ! $KEYSTORE ]]; then
./gradlew assembleExperimentalPlayDebug ./gradlew assembleExperimentalPlayDebug
fi fi
working_directory: android working_directory: android
@ -203,12 +200,8 @@ commands:
- run: - run:
name: Set Google Services name: Set Google Services
command: | command: |
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then if [[ $KEYSTORE ]]; then
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
echo $GOOGLE_SERVICES_IOS | base64 --decode > GoogleService-Info.plist echo $GOOGLE_SERVICES_IOS | base64 --decode > GoogleService-Info.plist
else
echo $GOOGLE_SERVICES_IOS_EXPERIMENTAL | base64 --decode > GoogleService-Info.plist
fi
fi fi
working_directory: ios working_directory: ios
- run: - run:
@ -230,12 +223,12 @@ commands:
/usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./NotificationService/Info.plist /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./NotificationService/Info.plist
fi fi
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then if [[ $APP_STORE_CONNECT_API_BASE64 ]]; then
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8 echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
bundle exec fastlane ios build_official bundle exec fastlane ios build_official
else else
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then if [[ $KEYSTORE ]]; then
bundle exec fastlane ios build_experimental bundle exec fastlane ios build_experimental
else else
bundle exec fastlane ios build_fork bundle exec fastlane ios build_fork
@ -325,19 +318,11 @@ commands:
- run: - run:
name: Fastlane Tesflight Upload name: Fastlane Tesflight Upload
command: | command: |
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8 echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
bundle exec fastlane ios beta official:<< parameters.official >> bundle exec fastlane ios beta official:<< parameters.official >>
working_directory: ios working_directory: ios
- save_cache: *save-gems-cache - save_cache: *save-gems-cache
create-e2e-account-file:
description: "Create e2e account file"
steps:
- run:
command: |
echo $E2E_ACCOUNT | base64 --decode > ./e2e_account.ts
working_directory: e2e
version: 2.1 version: 2.1
# EXECUTORS # EXECUTORS
@ -449,94 +434,6 @@ jobs:
- upload-to-google-play-beta: - upload-to-google-play-beta:
official: true official: true
e2e-build-android:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
tag: 2022.12.1
environment:
<<: *android-env
steps:
- checkout
- restore_cache: *restore-npm-cache-linux
- run: *install-npm-modules
- save_cache: *save-npm-cache-linux
- restore_cache: *restore-gradle-cache
- run:
name: Configure Gradle
command: |
echo -e "" > ./gradle.properties
# echo -e "android.enableAapt2=false" >> ./gradle.properties
echo -e "android.useAndroidX=true" >> ./gradle.properties
echo -e "android.enableJetifier=true" >> ./gradle.properties
echo -e "newArchEnabled=false" >> ./gradle.properties
echo -e "FLIPPER_VERSION=0.125.0" >> ./gradle.properties
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
echo $KEYSTORE_EXPERIMENTAL_BASE64 | base64 --decode > ./app/$KEYSTORE_EXPERIMENTAL
echo -e "KEYSTORE=$KEYSTORE_EXPERIMENTAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_EXPERIMENTAL_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
working_directory: android
- run:
name: Build Android
command: |
echo "RUNNING_E2E_TESTS=true" > ./.env
yarn e2e:android-build
- save_cache: *save-gradle-cache
- store_artifacts:
path: android/app/build/outputs/apk/experimentalPlay/release/app-experimental-play-release.apk
- store_artifacts:
path: android/app/build/outputs/apk/androidTest/experimentalPlay/release/app-experimental-play-release-androidTest.apk
- persist_to_workspace:
root: /home/circleci/repo
paths:
- android/app/build/outputs/apk/
e2e-test-android:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
tag: 2022.12.1
parallelism: 4
steps:
- checkout
- attach_workspace:
at: /home/circleci/repo
- restore_cache: *restore-npm-cache-linux
- run: *install-npm-modules
- save_cache: *save-npm-cache-linux
- run: mkdir ~/junit
- create-e2e-account-file
- android/create-avd:
avd-name: Pixel_API_31_AOSP
install: true
system-image: system-images;android-31;default;x86_64
- run:
name: Setup emulator
command: |
echo "hw.lcd.density = 440" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
echo "hw.lcd.height = 2280" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
echo "hw.lcd.width = 1080" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
- run:
name: Run Detox Tests
command: |
TEST=$(circleci tests glob "e2e/tests/**/*.ts" | circleci tests split --split-by=timings)
yarn e2e:android-test $TEST
- store_artifacts:
path: artifacts
- run:
command: cp junit.xml ~/junit/
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
# iOS builds # iOS builds
ios-build-experimental: ios-build-experimental:
executor: mac-env executor: mac-env
@ -560,89 +457,11 @@ jobs:
- upload-to-testflight: - upload-to-testflight:
official: true official: true
e2e-build-ios:
executor: mac-env
steps:
- checkout
- restore_cache: *restore-gems-cache
- restore_cache: *restore-npm-cache-mac
- run: *install-npm-modules
- run: *update-fastlane-ios
- save_cache: *save-npm-cache-mac
- save_cache: *save-gems-cache
- manage-pods
- run:
name: Configure Detox
command: |
brew tap wix/brew
brew install applesimutils
- run:
name: Build
command: |
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/RocketChatRN/Info.plist
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/ShareRocketChatRN/Info.plist
yarn detox clean-framework-cache && yarn detox build-framework-cache
echo "RUNNING_E2E_TESTS=true" > ./.env
yarn e2e:ios-build
- persist_to_workspace:
root: /Users/distiller/project
paths:
- ios/build/Build/Products/Release-iphonesimulator/Rocket.Chat Experimental.app
e2e-test-ios:
executor: mac-env
parallelism: 5
steps:
- checkout
- attach_workspace:
at: /Users/distiller/project
- restore_cache: *restore-npm-cache-mac
- run: *install-npm-modules
- save_cache: *save-npm-cache-mac
- run: mkdir ~/junit
- run:
name: Configure Detox
command: |
brew tap wix/brew
brew install applesimutils
yarn detox clean-framework-cache && yarn detox build-framework-cache
- create-e2e-account-file
- run:
name: Run tests
command: |
TEST=$(circleci tests glob "e2e/tests/**/*.ts" | circleci tests split --split-by=timings)
yarn e2e:ios-test $TEST
- store_artifacts:
path: artifacts
- run:
command: cp junit.xml ~/junit/
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
workflows: workflows:
build-and-test: build-and-test:
jobs: jobs:
- lint-testunit - lint-testunit
# E2E tests
- e2e-hold:
type: approval
- e2e-build-ios:
requires:
- e2e-hold
- e2e-test-ios:
requires:
- e2e-build-ios
- e2e-build-android:
requires:
- e2e-hold
- e2e-test-android:
requires:
- e2e-build-android
# iOS Experimental # iOS Experimental
- ios-hold-build-experimental: - ios-hold-build-experimental:
type: approval type: approval

View File

@ -1,91 +0,0 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
$0: 'jest',
config: 'e2e/jest.config.js'
},
retries: process.env.CI ? 3 : 0
},
artifacts: {
plugins: {
screenshot: 'failing',
video: 'failing',
uiHierarchy: 'enabled'
}
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/Rocket.Chat Experimental.app',
build:
'xcodebuild -workspace ios/RocketChatRN.xcworkspace -scheme RocketChatRN -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
},
'ios.release': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/Rocket.Chat Experimental.app',
build:
'xcodebuild -workspace ios/RocketChatRN.xcworkspace -scheme RocketChatRN -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/experimentalPlay/debug/app-experimental-play-debug.apk',
build:
'cd android ; ./gradlew assembleExperimentalPlayDebug assembleExperimentalPlayDebugAndroidTest -DtestBuildType=debug ; cd -',
reversePorts: [8081]
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/experimentalPlay/release/app-experimental-play-release.apk',
build:
'cd android ; ./gradlew assembleExperimentalPlayRelease assembleExperimentalPlayReleaseAndroidTest -DtestBuildType=release ; cd -'
}
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14'
}
},
attached: {
type: 'android.attached',
device: {
adbName: '.*'
}
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_API_31_AOSP'
},
headless: process.env.CI ? true : false
}
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
'ios.sim.release': {
device: 'simulator',
app: 'ios.release'
},
'android.att.debug': {
device: 'attached',
app: 'android.debug'
},
'android.att.release': {
device: 'attached',
app: 'android.release'
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug'
},
'android.emu.release': {
device: 'emulator',
app: 'android.release'
}
}
};

2
.env
View File

@ -1,2 +0,0 @@
# DON'T COMMIT THIS FILE
RUNNING_E2E_TESTS=

View File

@ -2,7 +2,7 @@ module.exports = {
settings: { settings: {
'import/resolver': { 'import/resolver': {
node: { node: {
extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js', '.ios.tsx', '.android.tsx'] extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js']
} }
} }
}, },
@ -156,6 +156,22 @@ module.exports = {
__DEV__: true __DEV__: true
}, },
overrides: [ overrides: [
{
files: ['e2e/**'],
globals: {
by: true,
detox: true,
device: true,
element: true,
expect: true,
waitFor: true
},
rules: {
'import/no-extraneous-dependencies': 0,
'no-await-in-loop': 0,
'no-restricted-syntax': 0
}
},
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
extends: [ extends: [
@ -237,12 +253,6 @@ module.exports = {
} }
} }
} }
},
{
files: ['e2e/**'],
rules: {
'no-await-in-loop': 0
}
} }
] ]
}; };

View File

@ -3,5 +3,5 @@ updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "daily"
open-pull-requests-limit: 25 open-pull-requests-limit: 25

2
.gitignore vendored
View File

@ -66,7 +66,5 @@ artifacts
e2e/docker/rc_test_env/docker-compose.yml e2e/docker/rc_test_env/docker-compose.yml
e2e/docker/data/db e2e/docker/data/db
e2e/e2e_account.js e2e/e2e_account.js
e2e/e2e_account.ts
junit.xml
*.p8 *.p8

View File

@ -1 +1 @@
2.7.7 2.7.4

View File

@ -6,13 +6,11 @@ import RNBootSplash from 'react-native-bootsplash';
import { selectServerRequest } from '../app/actions/server'; import { selectServerRequest } from '../app/actions/server';
import { mockedStore as store } from '../app/reducers/mockedStore'; import { mockedStore as store } from '../app/reducers/mockedStore';
import database from '../app/lib/database'; import database from '../app/lib/database';
import { setUser } from '../app/actions/login';
RNBootSplash.hide(); RNBootSplash.hide();
const baseUrl = 'https://open.rocket.chat'; const baseUrl = 'https://open.rocket.chat';
store.dispatch(selectServerRequest(baseUrl)); store.dispatch(selectServerRequest(baseUrl));
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));
database.setActiveDB(baseUrl); database.setActiveDB(baseUrl);
const StorybookUIRoot = getStorybookUI({}); const StorybookUIRoot = getStorybookUI({});

View File

@ -3,13 +3,7 @@ import { Provider } from 'react-redux';
import { themes } from '../app/lib/constants'; import { themes } from '../app/lib/constants';
import MessageContext from '../app/containers/message/Context'; import MessageContext from '../app/containers/message/Context';
import { selectServerRequest } from '../app/actions/server';
import { mockedStore as store } from '../app/reducers/mockedStore'; import { mockedStore as store } from '../app/reducers/mockedStore';
import { setUser } from '../app/actions/login';
const baseUrl = 'https://open.rocket.chat';
store.dispatch(selectServerRequest(baseUrl));
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));
export const decorators = [ export const decorators = [
Story => ( Story => (
@ -21,7 +15,7 @@ export const decorators = [
username: 'diego.mello', username: 'diego.mello',
token: 'abc' token: 'abc'
}, },
baseUrl, baseUrl: 'https://open.rocket.chat',
onPress: () => {}, onPress: () => {},
onLongPress: () => {}, onLongPress: () => {},
reactionInit: () => {}, reactionInit: () => {},

View File

@ -22,7 +22,6 @@ const getStories = () => {
require("../app/containers/Avatar/Avatar.stories.tsx"), require("../app/containers/Avatar/Avatar.stories.tsx"),
require("../app/containers/BackgroundContainer/index.stories.tsx"), require("../app/containers/BackgroundContainer/index.stories.tsx"),
require("../app/containers/Button/Button.stories.tsx"), require("../app/containers/Button/Button.stories.tsx"),
require("../app/containers/Chip/Chip.stories.tsx"),
require("../app/containers/HeaderButton/HeaderButtons.stories.tsx"), require("../app/containers/HeaderButton/HeaderButtons.stories.tsx"),
require("../app/containers/List/List.stories.tsx"), require("../app/containers/List/List.stories.tsx"),
require("../app/containers/LoginServices/LoginServices.stories.tsx"), require("../app/containers/LoginServices/LoginServices.stories.tsx"),
@ -30,7 +29,6 @@ const getStories = () => {
require("../app/containers/markdown/new/NewMarkdown.stories.tsx"), require("../app/containers/markdown/new/NewMarkdown.stories.tsx"),
require("../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories.tsx"), require("../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.stories.tsx"),
require("../app/containers/message/Message.stories.tsx"), require("../app/containers/message/Message.stories.tsx"),
require("../app/containers/ReactionsList/ReactionsList.stories.tsx"),
require("../app/containers/RoomHeader/RoomHeader.stories.tsx"), require("../app/containers/RoomHeader/RoomHeader.stories.tsx"),
require("../app/containers/RoomItem/RoomItem.stories.tsx"), require("../app/containers/RoomItem/RoomItem.stories.tsx"),
require("../app/containers/SearchBox/SearchBox.stories.tsx"), require("../app/containers/SearchBox/SearchBox.stories.tsx"),
@ -40,7 +38,6 @@ const getStories = () => {
require("../app/containers/UIKit/UiKitModal.stories.tsx"), require("../app/containers/UIKit/UiKitModal.stories.tsx"),
require("../app/containers/UnreadBadge/UnreadBadge.stories.tsx"), require("../app/containers/UnreadBadge/UnreadBadge.stories.tsx"),
require("../app/views/CannedResponsesListView/CannedResponseItem.stories.tsx"), require("../app/views/CannedResponsesListView/CannedResponseItem.stories.tsx"),
require("../app/views/CreateChannelView/RoomSettings/SwitchItem.stories.tsx"),
require("../app/views/DiscussionsView/Item.stories.tsx"), require("../app/views/DiscussionsView/Item.stories.tsx"),
require("../app/views/RoomView/LoadMore/LoadMore.stories.tsx"), require("../app/views/RoomView/LoadMore/LoadMore.stories.tsx"),
require("../app/views/ThreadMessagesView/Item.stories.tsx"), require("../app/views/ThreadMessagesView/Item.stories.tsx"),

View File

@ -1,4 +1,4 @@
source 'https://rubygems.org' source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby '2.7.7' ruby '2.7.4'
gem 'cocoapods', '~> 1.11', '>= 1.11.2' gem 'cocoapods', '~> 1.11', '>= 1.11.2'

View File

@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Button Custom Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"purple\\",\\"padding\\":10,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"yellow\\",\\"fontSize\\":18},[{\\"textAlign\\":\\"left\\"}]],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Custom Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"purple\\",\\"padding\\":10,\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"yellow\\",\\"fontSize\\":18},[{\\"textAlign\\":\\"left\\"}]],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button Disabled Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Disabled Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button Disabled Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`; exports[`Storyshots Button Disabled Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":0.3}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`;
exports[`Storyshots Button Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`; exports[`Storyshots Button Loading Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":true},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"ActivityIndicator\\",\\"props\\":{\\"style\\":[{\\"padding\\":16,\\"flex\\":1},null],\\"color\\":\\"#ffffff\\"},\\"children\\":null}]}"`;
exports[`Storyshots Button Primary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Primary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#1d74f5\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#ffffff\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;
exports[`Storyshots Button Secondary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`; exports[`Storyshots Button Secondary Button 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Press me!\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"testID\\":\\"testButton\\",\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":12,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Press me!\\"},\\"children\\":[\\"Press me!\\"]}]}"`;

View File

@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar And Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar and Icon\\"]}]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":4,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`;

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Login Services Separators 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"More options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"More options\\"},\\"children\\":[\\"More options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]},{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Less options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":4,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Less options\\"},\\"children\\":[\\"Less options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]}]"`; exports[`Storyshots Login Services Separators 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"More options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"More options\\"},\\"children\\":[\\"More options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]},{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityLabel\\":\\"Less options\\",\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"collapsable\\":false,\\"style\\":{\\"paddingHorizontal\\":14,\\"justifyContent\\":\\"center\\",\\"height\\":48,\\"borderRadius\\":2,\\"marginBottom\\":0,\\"backgroundColor\\":\\"#ffffff\\",\\"opacity\\":1}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"center\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#1d74f5\\",\\"fontSize\\":16},null],\\"accessibilityLabel\\":\\"Less options\\"},\\"children\\":[\\"Less options\\"]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"marginVertical\\":24}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"marginLeft\\":14,\\"marginRight\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"OR\\"]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"height\\":1,\\"flex\\":1},{\\"backgroundColor\\":\\"#e1e5e8\\"}]},\\"children\\":null}]}]"`;
exports[`Storyshots Login Services Service List 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"github\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"gitlab\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"google\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":4,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"apple\\"]}]}]}]"`; exports[`Storyshots Login Services Service List 1`] = `"[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"github\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"gitlab\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"google\\"]}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"borderRadius\\":2,\\"width\\":\\"100%\\",\\"height\\":48,\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"center\\",\\"paddingHorizontal\\":15}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":24,\\"color\\":\\"#0d0e12\\"},{\\"position\\":\\"absolute\\",\\"left\\":15,\\"top\\":12,\\"width\\":24,\\"height\\":24},{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"fontSize\\":16},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Continue with\\",\\" \\",{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"600\\"}},\\"children\\":[\\"apple\\"]}]}]}]"`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots SearchBox Basic 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"testID\\":\\"searchbox\\",\\"style\\":{\\"backgroundColor\\":\\"#ffffff\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10},{\\"margin\\":16,\\"marginBottom\\":16}]},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"position\\":\\"relative\\",\\"justifyContent\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"TextInput\\",\\"props\\":{\\"style\\":[{\\"color\\":\\"#0d0e12\\"},[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"height\\":48,\\"fontSize\\":16,\\"paddingHorizontal\\":16,\\"paddingVertical\\":10,\\"borderWidth\\":1,\\"borderRadius\\":4},null,{\\"paddingRight\\":45},{\\"backgroundColor\\":\\"#ffffff\\",\\"borderColor\\":\\"#cbcbcc\\",\\"color\\":\\"#0d0e12\\"},null,null],{\\"textAlign\\":\\"auto\\"}],\\"placeholderTextColor\\":\\"#9ca2a8\\",\\"keyboardAppearance\\":\\"light\\",\\"autoCorrect\\":false,\\"autoCapitalize\\":\\"none\\",\\"underlineColorAndroid\\":\\"transparent\\",\\"accessibilityLabel\\":\\"Search\\",\\"placeholder\\":\\"Search\\",\\"value\\":\\"\\",\\"blurOnSubmit\\":true,\\"returnKeyType\\":\\"search\\"},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":20,\\"color\\":\\"#2f343d\\"},[{\\"position\\":\\"absolute\\"},{\\"right\\":12}],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`; exports[`Storyshots SearchBox Basic 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"testID\\":\\"searchbox\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"marginBottom\\":10},{\\"margin\\":16,\\"marginBottom\\":16}]},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"position\\":\\"relative\\"}},\\"children\\":[{\\"type\\":\\"TextInput\\",\\"props\\":{\\"style\\":[{\\"color\\":\\"#0d0e12\\"},[{\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\",\\"height\\":48,\\"fontSize\\":16,\\"padding\\":14,\\"borderWidth\\":0.5,\\"borderRadius\\":2},null,{\\"paddingRight\\":45},{\\"backgroundColor\\":\\"#ffffff\\",\\"borderColor\\":\\"#cbcbcc\\",\\"color\\":\\"#0d0e12\\"},null,null],{\\"textAlign\\":\\"auto\\"}],\\"placeholderTextColor\\":\\"#9ca2a8\\",\\"keyboardAppearance\\":\\"light\\",\\"autoCorrect\\":false,\\"autoCapitalize\\":\\"none\\",\\"underlineColorAndroid\\":\\"transparent\\",\\"accessibilityLabel\\":\\"Search\\",\\"placeholder\\":\\"Search\\",\\"value\\":\\"\\",\\"blurOnSubmit\\":true,\\"returnKeyType\\":\\"search\\"},\\"children\\":null},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":20,\\"color\\":\\"#2f343d\\"},[{\\"position\\":\\"absolute\\",\\"top\\":14},{\\"right\\":15}],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots SwitchItem Switch 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"minHeight\\":54,\\"alignItems\\":\\"center\\",\\"justifyContent\\":\\"space-between\\",\\"flexDirection\\":\\"row\\",\\"maxHeight\\":80,\\"marginBottom\\":12},{\\"backgroundColor\\":\\"#ffffff\\"}]},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"marginRight\\":8}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#0d0e12\\"}]},\\"children\\":[\\"Welcome to Rocket.Chat\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"testID\\":\\"create-channel-switch-id-hint\\",\\"style\\":[{\\"fontSize\\":14,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"400\\"},{\\"color\\":\\"#9ca2a8\\"}]},\\"children\\":[\\"Only authorized users can write new messages\\"]}]},{\\"type\\":\\"RCTSwitch\\",\\"props\\":{\\"testID\\":\\"create-channel-switch-id\\",\\"disabled\\":false,\\"onTintColor\\":\\"#2de0a5\\",\\"style\\":{\\"height\\":31,\\"width\\":51},\\"tintColor\\":\\"#f5455c\\",\\"value\\":false,\\"accessibilityRole\\":\\"switch\\"},\\"children\\":null}]}]}"`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.37.0" versionName "4.30.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
@ -250,7 +250,6 @@ android {
release { release {
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro']) setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
signingConfig signingConfigs.release signingConfig signingConfigs.release
if (!isFoss) { if (!isFoss) {
firebaseCrashlytics { firebaseCrashlytics {
@ -269,11 +268,6 @@ android {
// pickFirst '**/x86_64/libc++_shared.so' // pickFirst '**/x86_64/libc++_shared.so'
// } // }
// FIXME: Remove when we update RN
packagingOptions {
pickFirst '**/*.so'
}
// applicationVariants are e.g. debug, release // applicationVariants are e.g. debug, release
flavorDimensions "app", "type" flavorDimensions "app", "type"
@ -286,6 +280,10 @@ android {
dimension = "app" dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false" buildConfigField "boolean", "IS_OFFICIAL", "false"
} }
e2e {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
foss { foss {
dimension = "type" dimension = "type"
buildConfigField "boolean", "FDROID_BUILD", "true" buildConfigField "boolean", "FDROID_BUILD", "true"
@ -313,6 +311,16 @@ android {
java.srcDirs = ['src/main/java', 'src/play/java'] java.srcDirs = ['src/main/java', 'src/play/java']
manifest.srcFile 'src/play/AndroidManifest.xml' manifest.srcFile 'src/play/AndroidManifest.xml'
} }
e2ePlayDebug {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayRelease {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
} }
applicationVariants.all { variant -> applicationVariants.all { variant ->
@ -343,12 +351,14 @@ android {
dependencies { dependencies {
implementation project(':@react-native-community_viewpager') implementation project(':@react-native-community_viewpager')
implementation "androidx.core:core-splashscreen:1.0.0"
playImplementation project(':react-native-notifications') playImplementation project(':react-native-notifications')
playImplementation 'com.google.firebase:firebase-core:16.0.0' playImplementation 'com.google.firebase:firebase-core:16.0.0'
playImplementation project(':@react-native-firebase_app') playImplementation project(':@react-native-firebase_app')
playImplementation project(':@react-native-firebase_analytics') playImplementation project(':@react-native-firebase_analytics')
playImplementation project(':@react-native-firebase_crashlytics') playImplementation project(':@react-native-firebase_crashlytics')
implementation(project(':react-native-jitsi-meet')) { // https://github.com/skrafft/react-native-jitsi-meet#side-note
exclude group: 'com.facebook.react',module:'react-native-svg'
}
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion //noinspection GradleDynamicVersion
@ -377,9 +387,8 @@ dependencies {
implementation "com.github.bumptech.glide:glide:4.9.0" implementation "com.github.bumptech.glide:glide:4.9.0"
annotationProcessor "com.github.bumptech.glide:compiler:4.9.0" annotationProcessor "com.github.bumptech.glide:compiler:4.9.0"
implementation "com.tencent:mmkv-static:1.2.10" implementation "com.tencent:mmkv-static:1.2.10"
androidTestImplementation('com.wix:detox:+') androidTestImplementation('com.wix:detox:+') { transitive = true }
implementation 'androidx.appcompat:appcompat:1.1.0' androidTestImplementation 'junit:junit:4.12'
implementation 'com.facebook.soloader:soloader:0.10.4'
} }
if (isNewArchitectureEnabled()) { if (isNewArchitectureEnabled()) {

View File

@ -18,7 +18,7 @@ public class DetoxTest {
@Rule @Rule
// Replace 'MainActivity' with the value of android:name entry in // Replace 'MainActivity' with the value of android:name entry in
// <activity> in AndroidManifest.xml // <activity> in AndroidManifest.xml
public ActivityTestRule<chat.rocket.reactnative.MainActivity> mActivityRule = new ActivityTestRule<>(chat.rocket.reactnative.MainActivity.class, false, false); public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
@Test @Test
public void runDetoxTests() { public void runDetoxTests() {

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

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -5,15 +5,6 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.VIDEO_CAPTURE" />
<uses-permission android:name="android.permission.AUDIO_CAPTURE" />
<!-- permissions related to jitsi call -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<application <application
android:name="chat.rocket.reactnative.MainApplication" android:name="chat.rocket.reactnative.MainApplication"
android:allowBackup="false" android:allowBackup="false"
@ -22,9 +13,17 @@
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/BootTheme" android:theme="@style/AppTheme"
android:hardwareAccelerated="true"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<activity
android:name="com.zoontek.rnbootsplash.RNBootSplashActivity"
android:theme="@style/BootTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity <activity
android:name="chat.rocket.reactnative.MainActivity" android:name="chat.rocket.reactnative.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
@ -57,10 +56,6 @@
android:host="jitsi.rocket.chat" android:host="jitsi.rocket.chat"
android:scheme="rocketchat" /> android:scheme="rocketchat" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name="chat.rocket.reactnative.share.ShareActivity" android:name="chat.rocket.reactnative.share.ShareActivity"
@ -68,7 +63,7 @@
android:label="@string/share_extension_name" android:label="@string/share_extension_name"
android:noHistory="true" android:noHistory="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/ShareTheme" android:theme="@style/AppTheme"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@ -79,10 +74,5 @@
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
<queries>
<package android:name="org.jitsi.meet" />
<intent>
<action android:name="android.intent.action.SEND" />
</intent>
</queries>
</manifest> </manifest>

View File

@ -1,23 +1,24 @@
package chat.rocket.reactnative; package chat.rocket.reactnative;
import android.os.Bundle;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView; import com.facebook.react.ReactRootView;
import com.zoontek.rnbootsplash.RNBootSplash; import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactActivity;
import expo.modules.ReactActivityDelegateWrapper; import expo.modules.ReactActivityDelegateWrapper;
import com.zoontek.rnbootsplash.RNBootSplash;
public class MainActivity extends ReactActivity { public class MainActivity extends ReactActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
RNBootSplash.init(this);
// https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067 // https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
super.onCreate(null); super.onCreate(null);
RNBootSplash.init(R.drawable.launch_screen, MainActivity.this);
} }
@Override @Override

View File

@ -9,16 +9,13 @@ import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.ReactInstanceManager;
import com.facebook.soloader.SoLoader; import com.facebook.soloader.SoLoader;
import com.reactnativecommunity.viewpager.RNCViewPagerPackage; import com.reactnativecommunity.viewpager.RNCViewPagerPackage;
import com.facebook.react.bridge.JSIModulePackage; import com.facebook.react.bridge.JSIModulePackage;
import com.swmansion.reanimated.ReanimatedJSIModulePackage; import com.swmansion.reanimated.ReanimatedJSIModulePackage;
import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import expo.modules.ApplicationLifecycleDispatcher; import expo.modules.ApplicationLifecycleDispatcher;
import expo.modules.ReactNativeHostWrapper; import expo.modules.ReactNativeHostWrapper;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -79,7 +76,6 @@ public class MainApplication extends Application implements ReactApplication {
// If you opted-in for the New Architecture, we enable the TurboModule system // If you opted-in for the New Architecture, we enable the TurboModule system
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
SoLoader.init(this, /* native exopackage */ false); SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
ApplicationLifecycleDispatcher.onApplicationCreate(this); ApplicationLifecycleDispatcher.onApplicationCreate(this);
} }
@ -88,35 +84,4 @@ public class MainApplication extends Application implements ReactApplication {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig);
} }
/**
* 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.ReactNativeFlipper");
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();
}
}
}
} }

View File

@ -1,6 +1,8 @@
package chat.rocket.reactnative.share; package chat.rocket.reactnative.share;
import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
public class ShareActivity extends ReactActivity { public class ShareActivity extends ReactActivity {
@Override @Override

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- the background color. it can be a system color or a custom one defined in colors.xml -->
<item android:drawable="@color/splashBackground" />
<item>
<!-- the app logo, centered horizontally and vertically -->
<bitmap
android:src="@drawable/splash"
android:gravity="center" />
</item>
</layer-list>

View File

@ -8,15 +8,25 @@
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style> </style>
<style name="ShareTheme" parent="AppTheme"> <style name="Share.Window" parent="android:Theme">
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:windowEnterAnimation">@null</item>
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:windowExitAnimation">@null</item>
</style> </style>
<style name="BootTheme" parent="Theme.SplashScreen"> <style name="Theme.Share.Transparent" parent="android:Theme">
<item name="windowSplashScreenBackground">@color/splashBackground</item> <item name="android:windowIsTranslucent">true</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item> <item name="android:windowBackground">@color/primary_dark</item>
<item name="postSplashScreenTheme">@style/AppTheme</item> <item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@style/Share.Window</item>
</style>
<style name="BootTheme" parent="AppTheme">
<item name="android:background">@drawable/launch_screen</item>
<item name="colorPrimaryDark">@color/splashBackground</item>
<item name="android:navigationBarColor">@color/splashBackground</item>
</style> </style>
<!-- https://github.com/facebook/react-native/blob/d1ab03235cb4b93304150878d2b9057ab45bba77/ReactAndroid/src/main/res/views/modal/values/themes.xml#L5 --> <!-- https://github.com/facebook/react-native/blob/d1ab03235cb4b93304150878d2b9057ab45bba77/ReactAndroid/src/main/res/views/modal/values/themes.xml#L5 -->

View File

@ -7,8 +7,4 @@
tools:ignore="AcceptsUserCertificates" /> tools:ignore="AcceptsUserCertificates" />
</trust-anchors> </trust-anchors>
</base-config> </base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config> </network-security-config>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- the background color. it can be a system color or a custom one defined in colors.xml -->
<item android:drawable="@color/splashBackground" />
<item>
<!-- the app logo, centered horizontally and vertically -->
<bitmap
android:src="@drawable/splash"
android:gravity="center" />
</item>
</layer-list>

View File

@ -6,6 +6,7 @@
android:name="chat.rocket.reactnative.MainPlayApplication" android:name="chat.rocket.reactnative.MainPlayApplication"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:name" tools:replace="android:name"
> >

View File

@ -1,7 +1,5 @@
package chat.rocket.reactnative; package chat.rocket.reactnative;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@ -37,6 +35,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
public class CustomPushNotification extends PushNotification { public class CustomPushNotification extends PushNotification {
public static ReactApplicationContext reactApplicationContext; public static ReactApplicationContext reactApplicationContext;
final NotificationManager notificationManager; final NotificationManager notificationManager;
@ -322,12 +322,7 @@ public class CustomPushNotification extends PushNotification {
replyIntent.setAction(KEY_REPLY); replyIntent.setAction(KEY_REPLY);
replyIntent.putExtra("pushNotification", bundle); replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent; PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
} else {
replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY)
.setLabel(label) .setLabel(label)
@ -348,13 +343,12 @@ public class CustomPushNotification extends PushNotification {
Intent intent = new Intent(mContext, DismissNotification.class); Intent intent = new Intent(mContext, DismissNotification.class);
intent.putExtra(NOTIFICATION_ID, notificationId); intent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, 0);
notification.setDeleteIntent(dismissPendingIntent); notification.setDeleteIntent(dismissPendingIntent);
} }
private void notificationLoad(Ejson ejson, Callback callback) { private void notificationLoad(Ejson ejson, Callback callback) {
LoadNotification loadNotification = new LoadNotification(); LoadNotification.load(reactApplicationContext, ejson, callback);
loadNotification.load(reactApplicationContext, ejson, callback);
} }
} }

View File

@ -1,14 +1,20 @@
package chat.rocket.reactnative; package chat.rocket.reactnative;
import android.os.Bundle; import android.os.Bundle;
import android.content.Context;
import com.facebook.react.bridge.ReactApplicationContext; import okhttp3.Call;
import com.google.gson.Gson;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.HttpUrl;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.Interceptor;
import com.google.gson.Gson;
import java.io.IOException;
import com.facebook.react.bridge.ReactApplicationContext;
class JsonResponse { class JsonResponse {
Data data; Data data;
@ -43,11 +49,11 @@ class JsonResponse {
} }
public class LoadNotification { public class LoadNotification {
private int RETRY_COUNT = 0; private static int RETRY_COUNT = 0;
private int[] TIMEOUT = new int[]{0, 1, 3, 5, 10}; private static int[] TIMEOUT = new int[]{0, 1, 3, 5, 10};
private String TOKEN_KEY = "reactnativemeteor_usertoken-"; private static String TOKEN_KEY = "reactnativemeteor_usertoken-";
public void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) { public static void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
final OkHttpClient client = new OkHttpClient(); final OkHttpClient client = new OkHttpClient();
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder(); HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
@ -67,7 +73,7 @@ public class LoadNotification {
runRequest(client, request, callback); runRequest(client, request, callback);
} }
private void runRequest(OkHttpClient client, Request request, Callback callback) { private static void runRequest(OkHttpClient client, Request request, Callback callback) {
try { try {
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000); Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);

View File

@ -1,5 +1,9 @@
import org.apache.tools.ant.taskdefs.condition.Os import org.apache.tools.ant.taskdefs.condition.Os
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase() def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
@ -9,7 +13,8 @@ buildscript {
buildToolsVersion = "31.0.0" buildToolsVersion = "31.0.0"
minSdkVersion = 23 minSdkVersion = 23
compileSdkVersion = 31 compileSdkVersion = 31
targetSdkVersion = 31 // TODO: Fix "android:exported" issue and target 31 again
targetSdkVersion = 30
if (System.properties['os.arch'] == "aarch64") { if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64 // For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = "24.0.8215888" ndkVersion = "24.0.8215888"
@ -22,6 +27,8 @@ buildscript {
kotlinVersion = '1.6.10' kotlinVersion = '1.6.10'
supportLibVersion = "28.0.0" supportLibVersion = "28.0.0"
libre_build = !(isPlay.toBoolean()) libre_build = !(isPlay.toBoolean())
jitsi_url = isPlay ? "https://github.com/RocketChat/jitsi-maven-repository/raw/master/releases" : "https://github.com/RocketChat/jitsi-maven-repository/raw/libre/releases"
jitsi_version = isPlay ? "3.6.0" : "3.6.0-libre"
} }
repositories { repositories {
@ -62,6 +69,9 @@ allprojects {
url "$rootDir/../node_modules/detox/Detox-android" url "$rootDir/../node_modules/detox/Detox-android"
} }
maven {
url jitsi_url
}
mavenCentral { mavenCentral {
content { content {
excludeGroup "com.facebook.react" excludeGroup "com.facebook.react"
@ -71,38 +81,5 @@ allprojects {
google() google()
maven { url 'https://maven.google.com' } maven { url 'https://maven.google.com' }
maven { url 'https://www.jitpack.io' } maven { url 'https://www.jitpack.io' }
// https://stackoverflow.com/a/74333788/5447468
// TODO: remove once we update RN
exclusiveContent {
// We get React Native's Android binaries exclusively through npm,
// from a local Maven repo inside node_modules/react-native/.
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
// and potentially getting a wrong version.)
filter {
includeGroup "com.facebook.react"
}
forRepository {
maven {
// NOTE: if you are in a monorepo, you may have "$rootDir/../../../node_modules/react-native/android"
url "$rootDir/../node_modules/react-native/android"
}
}
}
}
}
subprojects { subproject ->
afterEvaluate {
if (!project.name.equalsIgnoreCase("app") && project.hasProperty("android")) {
android {
compileSdkVersion 31
buildToolsVersion "31.0.0"
defaultConfig {
minSdkVersion 23
targetSdkVersion 31
}
}
}
} }
} }

View File

@ -23,7 +23,7 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# Version of flipper SDK to use with React Native # Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.175.0 FLIPPER_VERSION=0.125.0
# Use this property to specify which architecture you want to build. # Use this property to specify which architecture you want to build.
# You can also override it from the CLI using # You can also override it from the CLI using

View File

@ -28,9 +28,7 @@ export const ROOM = createRequestTypes('ROOM', [
'DELETE', 'DELETE',
'REMOVED', 'REMOVED',
'FORWARD', 'FORWARD',
'USER_TYPING', 'USER_TYPING'
'HISTORY_REQUEST',
'HISTORY_FINISHED'
]); ]);
export const INQUIRY = createRequestTypes('INQUIRY', [ export const INQUIRY = createRequestTypes('INQUIRY', [
...defaultTypes, ...defaultTypes,
@ -40,14 +38,7 @@ export const INQUIRY = createRequestTypes('INQUIRY', [
'QUEUE_UPDATE', 'QUEUE_UPDATE',
'QUEUE_REMOVE' 'QUEUE_REMOVE'
]); ]);
export const APP = createRequestTypes('APP', [ export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
'START',
'READY',
'INIT',
'INIT_LOCAL_SETTINGS',
'SET_MASTER_DETAIL',
'SET_NOTIFICATION_PRESENCE_CAP'
]);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]); export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);

View File

@ -12,11 +12,7 @@ interface ISetMasterDetail extends Action {
isMasterDetail: boolean; isMasterDetail: boolean;
} }
interface ISetNotificationPresenceCap extends Action { export type TActionApp = IAppStart & ISetMasterDetail;
show: boolean;
}
export type TActionApp = IAppStart & ISetMasterDetail & ISetNotificationPresenceCap;
interface Params { interface Params {
root: RootEnum; root: RootEnum;
@ -55,10 +51,3 @@ export function setMasterDetail(isMasterDetail: boolean): ISetMasterDetail {
isMasterDetail isMasterDetail
}; };
} }
export function setNotificationPresenceCap(show: boolean): ISetNotificationPresenceCap {
return {
type: APP.SET_NOTIFICATION_PRESENCE_CAP,
show
};
}

View File

@ -13,7 +13,6 @@ interface ILoginRequest extends Action {
credentials: any; credentials: any;
logoutOnError?: boolean; logoutOnError?: boolean;
isFromWebView?: boolean; isFromWebView?: boolean;
registerCustomFields?: any;
} }
interface ILoginSuccess extends Action { interface ILoginSuccess extends Action {
@ -57,15 +56,13 @@ export type TActionsLogin = ILoginRequest &
export function loginRequest( export function loginRequest(
credentials: Partial<ICredentials>, credentials: Partial<ICredentials>,
logoutOnError?: boolean, logoutOnError?: boolean,
isFromWebView?: boolean, isFromWebView?: boolean
registerCustomFields?: any
): ILoginRequest { ): ILoginRequest {
return { return {
type: types.LOGIN.REQUEST, type: types.LOGIN.REQUEST,
credentials, credentials,
logoutOnError, logoutOnError,
isFromWebView, isFromWebView
registerCustomFields
}; };
} }

View File

@ -1,6 +1,6 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { ERoomType, RoomType } from '../definitions'; import { ERoomType } from '../definitions/ERoomType';
import { ROOM } from './actionsTypes'; import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED // TYPE RETURN RELATED
@ -44,24 +44,7 @@ interface IUserTyping extends Action {
status: boolean; status: boolean;
} }
export interface IRoomHistoryRequest extends Action { export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
rid: string;
t: RoomType;
loaderId: string;
}
export interface IRoomHistoryFinished extends Action {
loaderId: string;
}
export type TActionsRoom = TSubscribeRoom &
TUnsubscribeRoom &
ILeaveRoom &
IDeleteRoom &
IForwardRoom &
IUserTyping &
IRoomHistoryRequest &
IRoomHistoryFinished;
export function subscribeRoom(rid: string): TSubscribeRoom { export function subscribeRoom(rid: string): TSubscribeRoom {
return { return {
@ -116,19 +99,3 @@ export function userTyping(rid: string, status = true): IUserTyping {
status status
}; };
} }
export function roomHistoryRequest({ rid, t, loaderId }: { rid: string; t: RoomType; loaderId: string }): IRoomHistoryRequest {
return {
type: ROOM.HISTORY_REQUEST,
rid,
t,
loaderId
};
}
export function roomHistoryFinished({ loaderId }: { loaderId: string }): IRoomHistoryFinished {
return {
type: ROOM.HISTORY_FINISHED,
loaderId
};
}

View File

@ -18,10 +18,10 @@ const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MIN_SNAP_HEIGHT = 16; const MIN_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64; const CANCEL_HEIGHT = 64;
export const ACTION_SHEET_ANIMATION_DURATION = 250; const ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = { const ANIMATION_CONFIG = {
duration: ACTION_SHEET_ANIMATION_DURATION, duration: ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic // https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0) easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
}; };

View File

@ -122,6 +122,7 @@ const ActionSheetContentWithInputAndSubmit = ({
}} }}
testID={testID} testID={testID}
secureTextEntry={secureTextEntry} secureTextEntry={secureTextEntry}
inputStyle={{ borderWidth: 2 }}
bottomSheet={isIOS} bottomSheet={isIOS}
/> />
) : null} ) : null}

View File

@ -52,11 +52,7 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: I
/> />
); );
} }
return ( return <BottomSheetView style={styles.contentContainer}>{children}</BottomSheetView>;
<BottomSheetView testID='action-sheet' style={styles.contentContainer}>
{children}
</BottomSheetView>
);
}); });
export default BottomSheetContent; export default BottomSheetContent;

View File

@ -1,2 +1 @@
export * from './Provider'; export * from './Provider';
export * from './ActionSheet';

View File

@ -52,7 +52,7 @@ export default StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
justifyContent: 'center', justifyContent: 'center',
height: ITEM_HEIGHT, height: ITEM_HEIGHT,
borderRadius: 4, borderRadius: 2,
marginBottom: 12 marginBottom: 12
}, },
text: { text: {

View File

@ -43,7 +43,9 @@ const Avatar = React.memo(
let image; let image;
if (emoji) { if (emoji) {
image = <Emoji getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />; image = (
<Emoji baseUrl={server} getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />
);
} else { } else {
let uri = avatar; let uri = avatar;
if (!isStatic) { if (!isStatic) {

View File

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { ViewStyle } from 'react-native';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
export interface IAvatar { export interface IAvatar {
server?: string; server?: string;
style?: ViewStyle; style?: any;
text?: string; text?: string;
avatar?: string; avatar?: string;
emoji?: string; emoji?: string;

View File

@ -22,7 +22,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
justifyContent: 'center', justifyContent: 'center',
height: 48, height: 48,
borderRadius: 4, borderRadius: 2,
marginBottom: 12 marginBottom: 12
}, },
text: { text: {

View File

@ -1,32 +0,0 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import Chip, { IChip } from './index';
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'flex-start',
padding: 16
}
});
export default {
title: 'Chip'
};
const ChipWrapped = ({ avatar, text, onPress, testID, style }: IChip) => (
<View style={styles.container}>
<Chip avatar={avatar} text={text} onPress={onPress} testID={testID} style={style} />
</View>
);
export const ChipText = () => <ChipWrapped avatar='rocket.cat' text={'Rocket.Cat'} onPress={() => {}} />;
export const ChipWithShortText = () => <ChipWrapped avatar='rocket.cat' text={'Short'} onPress={() => {}} />;
export const ChipWithoutAvatar = () => <ChipWrapped text={'Without Avatar'} onPress={() => {}} />;
export const ChipWithoutIcon = () => <ChipWrapped avatar='rocket.cat' text='Without Icon' />;
export const ChipWithoutAvatarAndIcon = () => <ChipWrapped text='Without Avatar and Icon' />;

View File

@ -1,58 +0,0 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import Chip, { IChip } from '.';
import { ISelectedUser } from '../../reducers/selectedUsers';
import { mockedStore as store } from '../../reducers/mockedStore';
const onPressMock = jest.fn((item: any) => item);
const testChip = {
testID: 'test-chip-id',
item: { fname: 'rocket.chat', name: 'rocket.chat' } as ISelectedUser,
onPress: onPressMock
};
const Render = ({ testID, text, avatar, onPress }: IChip) => (
<Provider store={store}>
<Chip testID={testID} text={text} onPress={onPress} avatar={avatar} />
</Provider>
);
describe('Chips', () => {
it('should render the Chip component', () => {
const { findByTestId } = render(
<Render
text={testChip.item.fname}
avatar={testChip.item.name}
testID={testChip.testID}
onPress={() => testChip.onPress(testChip.item)}
/>
);
expect(findByTestId(testChip.testID)).toBeTruthy();
});
it("should not call onPress if it's not passed", async () => {
const { findByTestId } = render(<Render text={testChip.item.fname} avatar={testChip.item.name} testID={testChip.testID} />);
const component = await findByTestId(testChip.testID);
fireEvent.press(component);
expect(onPressMock).not.toHaveBeenCalled();
});
it('should tap Chip and return item', async () => {
const { findByTestId } = render(
<Render
text={testChip.item.fname}
avatar={testChip.item.name}
testID={testChip.testID}
onPress={() => testChip.onPress(testChip.item)}
/>
);
const component = await findByTestId(testChip.testID);
fireEvent.press(component);
expect(onPressMock).toHaveBeenCalled();
expect(onPressMock).toHaveReturnedWith(testChip.item);
});
});

View File

@ -1,75 +0,0 @@
import React from 'react';
import { Pressable, StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native';
import { useTheme } from '../../theme';
import { CustomIcon } from '../CustomIcon';
import sharedStyles from '../../views/Styles';
import Avatar from '../Avatar';
const styles = StyleSheet.create({
pressable: {
paddingHorizontal: 8,
marginRight: 8,
borderRadius: 4,
justifyContent: 'center',
maxWidth: 192
},
container: {
flexDirection: 'row',
alignItems: 'center'
},
avatar: {
marginRight: 8,
marginVertical: 8
},
textContainer: {
marginRight: 8,
maxWidth: 120
},
name: {
fontSize: 16,
...sharedStyles.textMedium
}
});
export interface IChip {
avatar?: string;
text: string;
onPress?: Function;
testID?: string;
style?: StyleProp<ViewStyle>;
}
const Chip = ({ avatar, text, onPress, testID, style }: IChip) => {
const { colors } = useTheme();
return (
<Pressable
testID={testID}
style={({ pressed }) => [
styles.pressable,
{
backgroundColor: pressed ? colors.bannerBackground : colors.auxiliaryBackground
},
style
]}
disabled={!onPress}
onPress={() => onPress?.()}
android_ripple={{
color: colors.bannerBackground
}}
>
<View style={styles.container}>
{avatar ? <Avatar text={avatar} size={28} style={styles.avatar} /> : null}
<View style={styles.textContainer}>
<Text style={[styles.name, { color: colors.bodyText }]} numberOfLines={1}>
{text}
</Text>
</View>
{onPress ? <CustomIcon name='close' size={16} color={colors.auxiliaryTintColor} /> : null}
</View>
</Pressable>
);
};
export default Chip;

View File

@ -1,10 +1,7 @@
export const mappedIcons = { export const mappedIcons = {
'status-disabled': 59837, 'lamp-bulb': 59812,
'lamp-bulb': 59836,
'phone-in': 59835,
'basketball': 59776, 'basketball': 59776,
'percentage': 59777, 'percentage': 59777,
'glasses': 59812,
'burger': 59813, 'burger': 59813,
'leaf': 59814, 'leaf': 59814,
'airplane': 59815, 'airplane': 59815,

File diff suppressed because one or more lines are too long

View File

@ -1,30 +1,24 @@
import React from 'react'; import React from 'react';
import { StyleProp } from 'react-native'; import FastImage from 'react-native-fast-image';
import FastImage, { ImageStyle } from 'react-native-fast-image';
import { useAppSelector } from '../../lib/hooks'; import { ICustomEmoji } from '../../definitions/IEmoji';
import { ICustomEmoji } from '../../definitions';
interface ICustomEmojiProps {
emoji: ICustomEmoji;
style: StyleProp<ImageStyle>;
}
const CustomEmoji = React.memo( const CustomEmoji = React.memo(
({ emoji, style }: ICustomEmojiProps) => { ({ baseUrl, emoji, style }: ICustomEmoji) => (
const baseUrl = useAppSelector(state => state.share.server.server || state.server.server);
return (
<FastImage <FastImage
style={style} style={style}
source={{ source={{
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.name)}.${emoji.extension}`, uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`,
priority: FastImage.priority.high priority: FastImage.priority.high
}} }}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
); ),
}, (prevProps, nextProps) => {
() => true const prevEmoji = prevProps.emoji.content || prevProps.emoji.name;
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name;
return prevEmoji === nextEmoji;
}
); );
export default CustomEmoji; export default CustomEmoji;

View File

@ -1,18 +0,0 @@
import React from 'react';
import { Text } from 'react-native';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import { IEmoji } from '../../definitions/IEmoji';
interface IEmojiProps {
emoji: IEmoji;
}
export const Emoji = ({ emoji }: IEmojiProps): React.ReactElement => {
if (typeof emoji === 'string') {
return <Text style={styles.categoryEmoji}>{shortnameToUnicode(`:${emoji}:`)}</Text>;
}
return <CustomEmoji style={styles.customCategoryEmoji} emoji={emoji} />;
};

View File

@ -1,34 +1,67 @@
import React from 'react'; import React from 'react';
import { Text, Pressable } from 'react-native';
import { FlatList } from 'react-native-gesture-handler'; import { FlatList } from 'react-native-gesture-handler';
import { IEmoji } from '../../definitions/IEmoji'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles, { MIN_EMOJI_SIZE, MAX_EMOJI_SIZE } from './styles';
import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { PressableEmoji } from './PressableEmoji'; import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
import { EMOJI_BUTTON_SIZE } from './styles'; import { useTheme } from '../../theme';
import { isIOS } from '../../lib/methods/helpers';
import { useDimensions } from '../../dimensions';
interface IEmojiCategoryProps { interface IEmojiProps {
emojis: IEmoji[]; emoji: string | IEmoji;
onEmojiSelected: (emoji: IEmoji) => void; size: number;
tabLabel?: string; // needed for react-native-scrollable-tab-view only baseUrl: string;
parentWidth: number;
} }
const EmojiCategory = ({ onEmojiSelected, emojis, parentWidth }: IEmojiCategoryProps): React.ReactElement | null => { const Emoji = ({ emoji, size, baseUrl }: IEmojiProps): React.ReactElement => {
if (!parentWidth) { if (typeof emoji === 'string')
return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
{shortnameToUnicode(`:${emoji}:`)}
</Text>
);
return (
<CustomEmoji style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]} emoji={emoji} baseUrl={baseUrl} />
);
};
const EmojiCategory = ({ baseUrl, onEmojiSelected, emojis, tabsCount }: IEmojiCategory): React.ReactElement | null => {
const { colors } = useTheme();
const { width } = useDimensions();
const emojiSize = Math.min(Math.max(width / tabsCount, MIN_EMOJI_SIZE), MAX_EMOJI_SIZE);
const numColumns = Math.trunc(width / emojiSize);
const marginHorizontal = (width - numColumns * emojiSize) / 2;
const renderItem = (emoji: IEmoji | string) => (
<Pressable
key={typeof emoji === 'string' ? emoji : emoji.content}
onPress={() => onEmojiSelected(emoji)}
testID={`emoji-${typeof emoji === 'string' ? emoji : emoji.content}`}
android_ripple={{ color: colors.bannerBackground, borderless: true, radius: emojiSize / 2 }}
style={({ pressed }: { pressed: boolean }) => ({
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
})}
>
<Emoji emoji={emoji} size={emojiSize} baseUrl={baseUrl} />
</Pressable>
);
if (!width) {
return null; return null;
} }
const numColumns = Math.trunc(parentWidth / EMOJI_BUTTON_SIZE);
const marginHorizontal = (parentWidth % EMOJI_BUTTON_SIZE) / 2;
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
return ( return (
<FlatList <FlatList
key={`emoji-category-${parentWidth}`} // rerender FlatList in case of width changes
keyExtractor={item => (typeof item === 'string' ? item : item.name)} key={`emoji-category-${width}`}
keyExtractor={item => (typeof item === 'string' ? item : item.content)}
data={emojis} data={emojis}
renderItem={renderItem} extraData={{ baseUrl, width }}
renderItem={({ item }) => renderItem(item)}
numColumns={numColumns} numColumns={numColumns}
initialNumToRender={45} initialNumToRender={45}
removeClippedSubviews removeClippedSubviews

View File

@ -1,61 +0,0 @@
import React, { useState } from 'react';
import { StyleSheet, TextInputProps } from 'react-native';
import { FormTextInput } from '../TextInput/FormTextInput';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { isIOS } from '../../lib/methods/helpers';
const styles = StyleSheet.create({
input: {
height: 32,
borderWidth: 0,
paddingVertical: 0,
borderRadius: 4
},
textInputContainer: {
marginBottom: 0
}
});
interface IEmojiSearchBarProps {
onBlur?: TextInputProps['onBlur'];
onChangeText: TextInputProps['onChangeText'];
bottomSheet?: boolean;
}
export const EmojiSearch = ({ onBlur, onChangeText, bottomSheet }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const handleTextChange = (text: string) => {
setSearchText(text);
if (onChangeText) {
onChangeText(text);
}
};
return (
<FormTextInput
autoCapitalize='none'
autoCorrect={false}
autoComplete='off'
returnKeyType='search'
textContentType='none'
blurOnSubmit
placeholder={I18n.t('Search_emoji')}
placeholderTextColor={colors.auxiliaryText}
underlineColorAndroid='transparent'
onChangeText={handleTextChange}
inputStyle={[styles.input, { backgroundColor: colors.textInputSecondaryBackground }]}
containerStyle={styles.textInputContainer}
value={searchText}
onClearInput={() => handleTextChange('')}
onBlur={onBlur}
iconRight={'search'}
testID='emoji-searchbar-input'
bottomSheet={bottomSheet && isIOS}
autoFocus={!bottomSheet} // focus on input when not in reaction picker
/>
);
};

View File

@ -11,11 +11,11 @@ const BUTTON_HIT_SLOP = { top: 15, right: 15, bottom: 15, left: 15 };
const Footer = ({ onSearchPressed, onBackspacePressed }: IFooterProps): React.ReactElement => { const Footer = ({ onSearchPressed, onBackspacePressed }: IFooterProps): React.ReactElement => {
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<View style={[styles.footerContainer, { borderTopColor: colors.borderColor }]}> <View style={[styles.footerContainer, { backgroundColor: colors.bannerBackground }]}>
<Pressable <Pressable
onPress={onSearchPressed} onPress={onSearchPressed}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]} style={({ pressed }) => [[styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]]}
testID='emoji-picker-search' testID='emoji-picker-search'
> >
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' /> <CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' />
@ -24,7 +24,7 @@ const Footer = ({ onSearchPressed, onBackspacePressed }: IFooterProps): React.Re
<Pressable <Pressable
onPress={onBackspacePressed} onPress={onBackspacePressed}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]} style={({ pressed }) => [{ opacity: pressed ? 0.7 : 1 }]}
testID='emoji-picker-backspace' testID='emoji-picker-backspace'
> >
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='backspace' /> <CustomIcon color={colors.auxiliaryTintColor} size={24} name='backspace' />

View File

@ -1,28 +0,0 @@
import React from 'react';
import { Pressable } from 'react-native';
import styles, { EMOJI_BUTTON_SIZE } from './styles';
import { IEmoji } from '../../definitions/IEmoji';
import { useTheme } from '../../theme';
import { isIOS } from '../../lib/methods/helpers';
import { Emoji } from './Emoji';
export const PressableEmoji = ({ emoji, onPress }: { emoji: IEmoji; onPress: (emoji: IEmoji) => void }): React.ReactElement => {
const { colors } = useTheme();
return (
<Pressable
key={typeof emoji === 'string' ? emoji : emoji.name}
onPress={() => onPress(emoji)}
testID={`emoji-${typeof emoji === 'string' ? emoji : emoji.name}`}
android_ripple={{ color: colors.bannerBackground, borderless: true, radius: EMOJI_BUTTON_SIZE / 2 }}
style={({ pressed }: { pressed: boolean }) => [
styles.emojiButton,
{
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
]}
>
<Emoji emoji={emoji} />
</Pressable>
);
};

View File

@ -7,16 +7,17 @@ import { ITabBarProps } from './interfaces';
import { isIOS } from '../../lib/methods/helpers'; import { isIOS } from '../../lib/methods/helpers';
import { CustomIcon } from '../CustomIcon'; import { CustomIcon } from '../CustomIcon';
const TabBar = ({ activeTab, tabs, goToPage }: ITabBarProps): React.ReactElement => { const TabBar = ({ tabs, activeTab, onPress, showFrequentlyUsed }: ITabBarProps): React.ReactElement => {
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<View style={styles.tabsContainer}> <View style={styles.tabsContainer}>
{tabs?.map((tab, i) => ( {tabs?.map((tab, i) => {
if (i === 0 && !showFrequentlyUsed) return null;
return (
<Pressable <Pressable
key={tab} key={tab.key}
onPress={() => goToPage?.(i)} onPress={() => onPress(tab.key)}
testID={`emoji-picker-tab-${tab}`} testID={`emoji-picker-tab-${tab.key}`}
android_ripple={{ color: colors.bannerBackground }} android_ripple={{ color: colors.bannerBackground }}
style={({ pressed }: { pressed: boolean }) => [ style={({ pressed }: { pressed: boolean }) => [
styles.tab, styles.tab,
@ -25,16 +26,11 @@ const TabBar = ({ activeTab, tabs, goToPage }: ITabBarProps): React.ReactElement
} }
]} ]}
> >
<CustomIcon name={tab} size={24} color={activeTab === i ? colors.tintColor : colors.auxiliaryTintColor} /> <CustomIcon name={tab.key} size={24} color={activeTab === i ? colors.tintColor : colors.auxiliaryTintColor} />
<View <View style={activeTab === i ? [styles.activeTabLine, { backgroundColor: colors.tintColor }] : styles.tabLine} />
style={
activeTab === i
? [styles.activeTabLine, { backgroundColor: colors.tintColor }]
: [styles.tabLine, { backgroundColor: colors.borderColor }]
}
/>
</Pressable> </Pressable>
))} );
})}
</View> </View>
); );
}; };

View File

@ -0,0 +1,49 @@
import { TIconsName } from '../CustomIcon';
import { IEmojiCategoryName } from '../../definitions';
const tabs: {
key: TIconsName;
title: IEmojiCategoryName;
}[] = [
{
key: 'clock',
title: 'frequentlyUsed'
},
{
key: 'rocket',
title: 'custom'
},
{
key: 'emoji',
title: 'people'
},
{
key: 'leaf',
title: 'nature'
},
{
key: 'burger',
title: 'food'
},
{
key: 'basketball',
title: 'activity'
},
{
key: 'airplane',
title: 'travel'
},
{
key: 'lamp-bulb',
title: 'objects'
},
{
key: 'percentage',
title: 'symbols'
},
{
key: 'flag',
title: 'flags'
}
];
export default tabs;

View File

@ -2814,4 +2814,4 @@ export const emojis = [
'flag_mf' 'flag_mf'
]; ];
export const DEFAULT_EMOJIS = ['clap', 'thumbsup', 'heart_eyes', 'grinning', 'thinking', 'smiley']; export const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];

View File

@ -0,0 +1,59 @@
import { useEffect, useState } from 'react';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import database from '../../lib/database';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { IEmoji, TFrequentlyUsedEmojiModel } from '../../definitions';
export const useFrequentlyUsedEmoji = (): {
frequentlyUsed: (string | IEmoji)[];
loaded: boolean;
} => {
const [frequentlyUsed, setFrequentlyUsed] = useState<(string | IEmoji)[]>([]);
const [loaded, setLoaded] = useState(false);
const getFrequentlyUsedEmojis = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsedEmojis = frequentlyUsedOrdered.map(item => {
if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
}
return shortnameToUnicode(`${item.content}`);
}) as (string | IEmoji)[];
setFrequentlyUsed(frequentlyUsedEmojis);
setLoaded(true);
};
useEffect(() => {
getFrequentlyUsedEmojis();
}, []);
return { frequentlyUsed, loaded };
};
export const addFrequentlyUsed = async (emoji: IEmoji) => {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: TFrequentlyUsedEmojiModel;
try {
freqEmojiRecord = await freqEmojiCollection.find(emoji.content || emoji.name);
} catch (error) {
// Do nothing
}
await db.write(async () => {
if (freqEmojiRecord) {
await freqEmojiRecord.update(f => {
if (f.count) {
f.count += 1;
}
});
} else {
await freqEmojiCollection.create(f => {
f._raw = sanitizedRaw({ id: emoji.content || emoji.name }, freqEmojiCollection.schema);
Object.assign(f, emoji);
f.count = 1;
});
}
});
};

View File

@ -1,17 +1,49 @@
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import { TabView, SceneRendererProps, NavigationState } from 'react-native-tab-view';
import { shallowEqual } from 'react-redux';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
import Footer from './Footer'; import Footer from './Footer';
import styles from './styles'; import styles from './styles';
import { categories, emojisByCategory } from '../../lib/constants'; import categories from './categories';
import { emojisByCategory } from './emojis';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import log from '../../lib/methods/helpers/log';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { IEmoji, ICustomEmojis } from '../../definitions'; import { IEmoji, ICustomEmojis, IEmojiPickerCategory, IEmojiCategoryName } from '../../definitions';
import { useAppSelector, useFrequentlyUsedEmoji } from '../../lib/hooks'; import { useAppSelector } from '../../lib/hooks';
import { addFrequentlyUsed } from '../../lib/methods';
import { IEmojiPickerProps, EventTypes } from './interfaces'; import { IEmojiPickerProps, EventTypes } from './interfaces';
import { useFrequentlyUsedEmoji, addFrequentlyUsed } from './frequentlyUsedEmojis';
import { TIconsName } from '../CustomIcon';
const Category = ({
title,
frequentlyUsed,
customEmojis,
handleEmojiSelect,
baseUrl,
tabsCount
}: IEmojiPickerCategory): React.ReactElement => {
let emojis: (IEmoji | string)[] = [];
if (title === 'frequentlyUsed') {
emojis = frequentlyUsed;
} else if (title === 'custom') {
emojis = customEmojis;
} else {
emojis = emojisByCategory[title];
}
return (
<EmojiCategory
emojis={emojis}
onEmojiSelected={(emoji: IEmoji | string) => handleEmojiSelect(emoji)}
style={styles.categoryContainer}
baseUrl={baseUrl}
tabsCount={tabsCount}
/>
);
};
const EmojiPicker = ({ const EmojiPicker = ({
onItemClicked, onItemClicked,
@ -20,71 +52,89 @@ const EmojiPicker = ({
searchedEmojis = [] searchedEmojis = []
}: IEmojiPickerProps): React.ReactElement | null => { }: IEmojiPickerProps): React.ReactElement | null => {
const { colors } = useTheme(); const { colors } = useTheme();
const [parentWidth, setParentWidth] = useState(0);
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(); const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
const [index, setIndex] = useState(0);
const [routes] = useState(categories);
const allCustomEmojis: ICustomEmojis = useAppSelector( const baseUrl = useAppSelector(state => state.server?.server);
state => state.customEmojis, const allCustomEmojis: ICustomEmojis = useAppSelector(state => state.customEmojis, shallowEqual);
() => true const customEmojis: IEmoji[] = useMemo(
); () =>
const customEmojis = Object.keys(allCustomEmojis) Object.keys(allCustomEmojis)
.filter(item => item === allCustomEmojis[item].name) .filter(item => item === allCustomEmojis[item].name)
.map(item => ({ .map(item => ({
content: allCustomEmojis[item].name,
name: allCustomEmojis[item].name, name: allCustomEmojis[item].name,
extension: allCustomEmojis[item].extension extension: allCustomEmojis[item].extension,
})); isCustom: true
})),
const handleEmojiSelect = (emoji: IEmoji) => { [allCustomEmojis]
onItemClicked(EventTypes.EMOJI_PRESSED, emoji);
addFrequentlyUsed(emoji);
};
const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => {
let emojis = [];
if (i === 0) {
emojis = frequentlyUsed;
} else if (i === 1) {
emojis = customEmojis;
} else {
emojis = emojisByCategory[category];
}
if (!emojis.length) {
return null;
}
return (
<EmojiCategory
parentWidth={parentWidth}
emojis={emojis}
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
tabLabel={label}
/>
); );
const handleEmojiSelect = (emoji: IEmoji | string) => {
try {
if (typeof emoji === 'string') {
addFrequentlyUsed({ content: emoji, name: emoji, isCustom: false });
const shortname = `:${emoji}:`;
onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname), shortname);
} else {
addFrequentlyUsed({
content: emoji.content,
name: emoji.name,
extension: emoji.extension,
isCustom: true
});
onItemClicked(EventTypes.EMOJI_PRESSED, `:${emoji.content}:`);
}
} catch (e) {
log(e);
}
}; };
const tabsCount = frequentlyUsed.length === 0 ? categories.length - 1 : categories.length;
type Route = {
key: TIconsName;
title: IEmojiCategoryName;
};
type State = NavigationState<Route>;
const renderTabBar = (props: SceneRendererProps & { navigationState: State }) => (
<TabBar tabs={categories} onPress={props.jumpTo} activeTab={index} showFrequentlyUsed={frequentlyUsed.length > 0} />
);
if (!loaded) { if (!loaded) {
return null; return null;
} }
return ( return (
<View style={styles.emojiPickerContainer} onLayout={e => setParentWidth(e.nativeEvent.layout.width)}> <View style={styles.emojiPickerContainer}>
{searching ? ( {searching ? (
<EmojiCategory <EmojiCategory
emojis={searchedEmojis} emojis={searchedEmojis}
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} onEmojiSelected={(emoji: IEmoji | string) => handleEmojiSelect(emoji)}
parentWidth={parentWidth} style={styles.categoryContainer}
baseUrl={baseUrl}
tabsCount={tabsCount}
/> />
) : ( ) : (
<ScrollableTabView <TabView
renderTabBar={() => <TabBar />} lazy
contentProps={{ navigationState={{ index, routes }}
keyboardShouldPersistTaps: 'always', renderScene={({ route }: { route: Route }) => (
keyboardDismissMode: 'none' <Category
}} key={route.key}
style={{ backgroundColor: colors.messageboxBackground }} title={route.title}
> frequentlyUsed={frequentlyUsed}
{categories.tabs.map((tab: any, i) => renderCategory(tab.category, i, tab.tabLabel))} customEmojis={customEmojis}
</ScrollableTabView> handleEmojiSelect={handleEmojiSelect}
baseUrl={baseUrl}
tabsCount={tabsCount}
/>
)}
onIndexChange={setIndex}
style={{ backgroundColor: colors.focusedBackground }}
renderTabBar={renderTabBar}
/>
)} )}
{isEmojiKeyboard && ( {isEmojiKeyboard && (
<Footer <Footer

View File

@ -8,10 +8,10 @@ export enum EventTypes {
} }
export interface IEmojiPickerProps { export interface IEmojiPickerProps {
onItemClicked: (event: EventTypes, emoji?: IEmoji) => void; onItemClicked: (event: EventTypes, emoji?: string, shortname?: string) => void;
isEmojiKeyboard?: boolean; isEmojiKeyboard?: boolean;
searching?: boolean; searching?: boolean;
searchedEmojis?: IEmoji[]; searchedEmojis?: (string | IEmoji)[];
} }
export interface IFooterProps { export interface IFooterProps {
@ -20,7 +20,8 @@ export interface IFooterProps {
} }
export interface ITabBarProps { export interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number; activeTab?: number;
tabs?: TIconsName[]; tabs?: { key: TIconsName; title: string }[];
onPress: (ket: string) => void;
showFrequentlyUsed?: boolean;
} }

View File

@ -2,15 +2,15 @@ import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
export const EMOJI_BUTTON_SIZE = 44; export const MAX_EMOJI_SIZE = 50;
export const EMOJI_SIZE = EMOJI_BUTTON_SIZE - 16; export const MIN_EMOJI_SIZE = 42;
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {
flex: 1 flex: 1
}, },
tabsContainer: { tabsContainer: {
height: EMOJI_BUTTON_SIZE, height: 45,
flexDirection: 'row' flexDirection: 'row'
}, },
tab: { tab: {
@ -18,7 +18,7 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 10, paddingVertical: 10,
width: EMOJI_BUTTON_SIZE width: 44
}, },
tabEmoji: { tabEmoji: {
fontSize: 20, fontSize: 20,
@ -36,6 +36,7 @@ export default StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
height: 2, height: 2,
backgroundColor: 'rgba(0,0,0,0.05)',
bottom: 0 bottom: 0
}, },
categoryContainer: { categoryContainer: {
@ -51,32 +52,22 @@ export default StyleSheet.create({
}, },
categoryEmoji: { categoryEmoji: {
...sharedStyles.textAlignCenter, ...sharedStyles.textAlignCenter,
textAlignVertical: 'center',
fontSize: EMOJI_SIZE,
backgroundColor: 'transparent', backgroundColor: 'transparent',
color: '#ffffff' color: '#ffffff'
}, },
customCategoryEmoji: { customCategoryEmoji: {
height: EMOJI_SIZE, margin: 8
width: EMOJI_SIZE
},
emojiButton: {
alignItems: 'center',
justifyContent: 'center',
height: EMOJI_BUTTON_SIZE,
width: EMOJI_BUTTON_SIZE
}, },
footerContainer: { footerContainer: {
height: EMOJI_BUTTON_SIZE, height: 44,
paddingHorizontal: 12, paddingHorizontal: 12,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center'
borderTopWidth: 1
}, },
footerButtonsContainer: { footerButtonsContainer: {
height: EMOJI_BUTTON_SIZE, height: 44,
width: EMOJI_BUTTON_SIZE, width: 44,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
}, },

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet } from 'react-native';
import { STATUS_COLORS } from '../../lib/constants';
import UnreadBadge from '../UnreadBadge'; import UnreadBadge from '../UnreadBadge';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -16,8 +15,6 @@ const styles = StyleSheet.create({
} }
}); });
export const BadgeUnread = ({ ...props }): React.ReactElement => <UnreadBadge {...props} style={styles.badgeContainer} small />; export const Badge = ({ ...props }): React.ReactElement => <UnreadBadge {...props} style={styles.badgeContainer} small />;
export const BadgeWarn = (): React.ReactElement => ( export default Badge;
<View style={[styles.badgeContainer, { width: 10, height: 10, backgroundColor: STATUS_COLORS.disabled }]} />
);

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native';
import { Header, HeaderBackground } from '@react-navigation/elements'; import { Header, HeaderBackground } from '@react-navigation/elements';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
@ -104,10 +103,9 @@ export const Badge = () => (
<HeaderExample <HeaderExample
left={() => ( left={() => (
<HeaderButton.Container left> <HeaderButton.Container left>
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} />} /> <HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} tunreadUser={[1]} />} /> <HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} tunreadUser={[1]} />} />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} tunreadGroup={[1]} />} /> <HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} tunreadGroup={[1]} />} />
<HeaderButton.Drawer badge={() => <HeaderButton.BadgeWarn />} />
</HeaderButton.Container> </HeaderButton.Container>
)} )}
/> />
@ -116,23 +114,20 @@ export const Badge = () => (
const ThemeStory = ({ theme }: { theme: TSupportedThemes }) => ( const ThemeStory = ({ theme }: { theme: TSupportedThemes }) => (
<ThemeContext.Provider value={{ theme, colors: colors[theme] }}> <ThemeContext.Provider value={{ theme, colors: colors[theme] }}>
<View style={{ flexDirection: 'column' }}>
<HeaderExample <HeaderExample
left={() => ( left={() => (
<HeaderButton.Container left> <HeaderButton.Container left>
<HeaderButton.Drawer badge={() => <HeaderButton.BadgeWarn />} />
<HeaderButton.Item iconName='threads' /> <HeaderButton.Item iconName='threads' />
</HeaderButton.Container> </HeaderButton.Container>
)} )}
right={() => ( right={() => (
<HeaderButton.Container> <HeaderButton.Container>
<HeaderButton.Item title='Threads' /> <HeaderButton.Item title='Threads' />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.BadgeUnread tunread={[1]} />} /> <HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
</HeaderButton.Container> </HeaderButton.Container>
)} )}
colors={colors[theme]} colors={colors[theme]}
/> />
</View>
</ThemeContext.Provider> </ThemeContext.Provider>
); );

View File

@ -1,4 +1,4 @@
export { default as Container } from './HeaderButtonContainer'; export { default as Container } from './HeaderButtonContainer';
export { default as Item } from './HeaderButtonItem'; export { default as Item } from './HeaderButtonItem';
export * from './HeaderButtonItemBadge'; export { default as Badge } from './HeaderButtonItemBadge';
export * from './Common'; export * from './Common';

View File

@ -12,6 +12,7 @@ import { themes } from '../../lib/constants';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { ROW_HEIGHT } from '../RoomItem'; import { ROW_HEIGHT } from '../RoomItem';
import { goRoom } from '../../lib/methods/helpers/goRoom'; import { goRoom } from '../../lib/methods/helpers/goRoom';
import Navigation from '../../lib/navigation/appNavigation';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions'; import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
@ -97,7 +98,12 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifie
prid prid
}; };
goRoom({ item, isMasterDetail, jumpToMessageId: _id, popToRoot: true }); if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
goRoom({ item, isMasterDetail, jumpToMessageId: _id });
hideNotification(); hideNotification();
}; };
@ -118,7 +124,6 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifie
onPress={onPress} onPress={onPress}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()} background={Touchable.SelectableBackgroundBorderless()}
testID={`in-app-notification-${text}`}
> >
<> <>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} /> <Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />

View File

@ -1,20 +1,19 @@
import React, { memo, useEffect } from 'react'; import React, { memo, useEffect } from 'react';
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier'; import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
import NotifierComponent, { INotifierComponent } from './NotifierComponent'; import NotifierComponent, { INotifierComponent } from './NotifierComponent';
import EventEmitter from '../../lib/methods/helpers/events'; import EventEmitter from '../../lib/methods/helpers/events';
import Navigation from '../../lib/navigation/appNavigation'; import Navigation from '../../lib/navigation/appNavigation';
import { getActiveRoute } from '../../lib/methods/helpers/navigation'; import { getActiveRoute } from '../../lib/methods/helpers/navigation';
import { useAppSelector } from '../../lib/hooks'; import { IApplicationState } from '../../definitions';
import { IRoom } from '../../reducers/room';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp'; export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo(() => { const InAppNotification = memo(
const { appState, subscribedRoom } = useAppSelector(state => ({ ({ rooms, appState }: { rooms: IRoom['rooms']; appState: string }) => {
subscribedRoom: state.room.subscribedRoom,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
}));
const show = (notification: INotifierComponent['notification']) => { const show = (notification: INotifierComponent['notification']) => {
if (appState !== 'foreground') { if (appState !== 'foreground') {
return; return;
@ -24,7 +23,7 @@ const InAppNotification = memo(() => {
const state = Navigation.navigationRef.current?.getRootState(); const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state); const route = getActiveRoute(state);
if (payload.rid) { if (payload.rid) {
if (payload.rid === subscribedRoom || route?.name === 'JitsiMeetView') { if (rooms.includes(payload.rid) || route?.name === 'JitsiMeetView') {
return; return;
} }
Notifier.showNotification({ Notifier.showNotification({
@ -42,9 +41,16 @@ const InAppNotification = memo(() => {
return () => { return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener); EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener);
}; };
}, [subscribedRoom, appState]); }, [rooms]);
return <NotifierRoot />; return <NotifierRoot />;
},
(prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)
);
const mapStateToProps = (state: IApplicationState) => ({
rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
}); });
export default InAppNotification; export default connect(mapStateToProps)(InAppNotification);

View File

@ -9,7 +9,7 @@ import { PADDING_HORIZONTAL } from './constants';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingVertical: 8, paddingBottom: 12,
paddingHorizontal: PADDING_HORIZONTAL paddingHorizontal: PADDING_HORIZONTAL
}, },
title: { title: {

View File

@ -5,7 +5,7 @@ import { Header } from '.';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
marginBottom: 16 marginVertical: 16
} }
}); });

View File

@ -2,6 +2,6 @@ import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
contentContainerStyleFlatList: { contentContainerStyleFlatList: {
paddingVertical: 16 paddingVertical: 32
} }
}); });

View File

@ -4,7 +4,7 @@ import sharedStyles from '../../views/Styles';
export const BUTTON_HEIGHT = 48; export const BUTTON_HEIGHT = 48;
export const SERVICE_HEIGHT = 58; export const SERVICE_HEIGHT = 58;
export const BORDER_RADIUS = 4; export const BORDER_RADIUS = 2;
export const SERVICES_COLLAPSED_HEIGHT = 174; export const SERVICES_COLLAPSED_HEIGHT = 174;
export default StyleSheet.create({ export default StyleSheet.create({

View File

@ -1,29 +1,33 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native'; import { FlatList, StyleSheet, Text, View } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme'; import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants'; import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon'; import { CustomIcon } from '../CustomIcon';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed } from '../../lib/methods';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { IEmoji, TAnyMessageModel } from '../../definitions'; import { TAnyMessageModel, TFrequentlyUsedEmojiModel } from '../../definitions';
import Touch from '../Touch'; import Touch from '../Touch';
import { DEFAULT_EMOJIS } from '../EmojiPicker/emojis';
type TItem = TFrequentlyUsedEmojiModel | string;
export interface IHeader { export interface IHeader {
handleReaction: (emoji: IEmoji | null, message: TAnyMessageModel) => void; handleReaction: (emoji: TItem, message: TAnyMessageModel) => void;
server: string;
message: TAnyMessageModel; message: TAnyMessageModel;
isMasterDetail: boolean; isMasterDetail: boolean;
} }
type TOnReaction = ({ emoji }: { emoji?: IEmoji }) => void; type TOnReaction = ({ emoji }: { emoji: TItem }) => void;
interface THeaderItem { interface THeaderItem {
item: IEmoji; item: TItem;
onReaction: TOnReaction; onReaction: TOnReaction;
server: string;
theme: TSupportedThemes; theme: TSupportedThemes;
} }
@ -61,19 +65,28 @@ const styles = StyleSheet.create({
} }
}); });
const HeaderItem = ({ item, onReaction, theme }: THeaderItem) => ( const keyExtractor = (item: TItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
return (emojiModel.id ? emojiModel.content : item) as string;
};
const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
const emoji = (emojiModel.id ? emojiModel.content : item) as string;
return (
<Touch <Touch
testID={`message-actions-emoji-${item}`} testID={`message-actions-emoji-${emoji}`}
onPress={() => onReaction({ emoji: item })} onPress={() => onReaction({ emoji: `:${emoji}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
> >
{typeof item === 'string' ? ( {emojiModel?.isCustom ? (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item}:`)}</Text> <CustomEmoji style={styles.customEmoji} emoji={emojiModel} baseUrl={server} />
) : ( ) : (
<CustomEmoji style={styles.customEmoji} emoji={item} /> <Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
)} )}
</Touch> </Touch>
); );
};
const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => ( const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
<Touch <Touch
@ -85,37 +98,49 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
</Touch> </Touch>
); );
const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => { const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => {
const [items, setItems] = useState<TItem[]>([]);
const { width, height } = useDimensions(); const { width, height } = useDimensions();
const { theme } = useTheme(); const { theme } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
// TODO: create custom hook to re-render based on screen size
const setEmojis = async () => {
try {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis: TItem[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height; const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2; const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = Math.trunc(size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1); const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
const onReaction: TOnReaction = ({ emoji }) => { freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
handleReaction(emoji || null, message); setItems(freqEmojis);
if (emoji) { } catch {
addFrequentlyUsed(emoji); // Do nothing
} }
}; };
const renderItem = ({ item }: { item: IEmoji }) => <HeaderItem item={item} onReaction={onReaction} theme={theme} />; useEffect(() => {
setEmojis();
}, []);
const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = ({ item }: { item: TItem }) => (
<HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
);
const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />; const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
if (!loaded) {
return null;
}
return ( return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList <FlatList
data={frequentlyUsed.slice(0, quantity)} data={items}
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={item => (typeof item === 'string' ? item : item.name)} keyExtractor={keyExtractor}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
scrollEnabled={false} scrollEnabled={false}
horizontal horizontal

View File

@ -12,12 +12,12 @@ import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../lib/methods/helpers/events'; import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../lib/methods/helpers/info'; import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet'; import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header'; import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events'; import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods'; import { getPermalinkMessage } from '../../lib/methods';
import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers'; import { hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
export interface IMessageActionsProps { export interface IMessageActionsProps {
@ -26,7 +26,7 @@ export interface IMessageActionsProps {
user: Pick<ILoggedUser, 'id'>; user: Pick<ILoggedUser, 'id'>;
editInit: (message: TAnyMessageModel) => void; editInit: (message: TAnyMessageModel) => void;
reactionInit: (message: TAnyMessageModel) => void; reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: (shortname: IEmoji, messageId: string) => void; onReactionPress: (shortname: string, messageId: string) => void;
replyInit: (message: TAnyMessageModel, mention: boolean) => void; replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean; isMasterDetail: boolean;
isReadOnly: boolean; isReadOnly: boolean;
@ -37,12 +37,11 @@ export interface IMessageActionsProps {
Message_AllowPinning?: boolean; Message_AllowPinning?: boolean;
Message_AllowStarring?: boolean; Message_AllowStarring?: boolean;
Message_Read_Receipt_Store_Users?: boolean; Message_Read_Receipt_Store_Users?: boolean;
server: string;
editMessagePermission?: string[]; editMessagePermission?: string[];
deleteMessagePermission?: string[]; deleteMessagePermission?: string[];
forceDeleteMessagePermission?: string[]; forceDeleteMessagePermission?: string[];
deleteOwnMessagePermission?: string[];
pinMessagePermission?: string[]; pinMessagePermission?: string[];
createDirectMessagePermission?: string[];
} }
export interface IMessageActions { export interface IMessageActions {
@ -61,6 +60,7 @@ const MessageActions = React.memo(
onReactionPress, onReactionPress,
replyInit, replyInit,
isReadOnly, isReadOnly,
server,
Message_AllowDeleting, Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes, Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing, Message_AllowEditing,
@ -72,9 +72,7 @@ const MessageActions = React.memo(
editMessagePermission, editMessagePermission,
deleteMessagePermission, deleteMessagePermission,
forceDeleteMessagePermission, forceDeleteMessagePermission,
deleteOwnMessagePermission, pinMessagePermission
pinMessagePermission,
createDirectMessagePermission
}, },
ref ref
) => { ) => {
@ -82,27 +80,19 @@ const MessageActions = React.memo(
hasEditPermission: false, hasEditPermission: false,
hasDeletePermission: false, hasDeletePermission: false,
hasForceDeletePermission: false, hasForceDeletePermission: false,
hasPinPermission: false, hasPinPermission: false
hasDeleteOwnPermission: false
}; };
const { showActionSheet, hideActionSheet } = useActionSheet(); const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async () => { const getPermissions = async () => {
try { try {
const permission = [ const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission];
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission,
deleteOwnMessagePermission
];
const result = await hasPermission(permission, room.rid); const result = await hasPermission(permission, room.rid);
permissions = { permissions = {
hasEditPermission: result[0], hasEditPermission: result[0],
hasDeletePermission: result[1], hasDeletePermission: result[1],
hasForceDeletePermission: result[2], hasForceDeletePermission: result[2],
hasPinPermission: result[3], hasPinPermission: result[3]
hasDeleteOwnPermission: result[4]
}; };
} catch { } catch {
// Do nothing // Do nothing
@ -144,7 +134,7 @@ const MessageActions = React.memo(
if (tmid === message.id) { if (tmid === message.id) {
return false; return false;
} }
const deleteOwn = isOwn(message) && permissions.hasDeleteOwnPermission; const deleteOwn = isOwn(message);
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) { if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
return false; return false;
} }
@ -247,23 +237,6 @@ const MessageActions = React.memo(
replyInit(message, false); replyInit(message, false);
}; };
const handleReplyInDM = async (message: TAnyMessageModel) => {
if (message?.u?.username) {
const result = await Services.createDirectMessage(message.u.username);
if (result.success) {
const { room } = result;
const params = {
rid: room.rid,
name: getRoomTitle(room),
t: room.t,
roomUserId: getUidDirectMessage(room),
replyInDM: message
};
Navigation.replace('RoomView', params);
}
}
};
const handleStar = async (message: TAnyMessageModel) => { const handleStar = async (message: TAnyMessageModel) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR); logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try { try {
@ -285,13 +258,16 @@ const MessageActions = React.memo(
} }
}; };
const handleReaction: IHeader['handleReaction'] = (emoji, message) => { const handleReaction: IHeader['handleReaction'] = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION); logEvent(events.ROOM_MSG_ACTION_REACTION);
if (emoji) { if (shortname) {
onReactionPress(emoji, message.id); // TODO: evaluate unification with IEmoji
onReactionPress(shortname as any, message.id);
} else { } else {
setTimeout(() => reactionInit(message), ACTION_SHEET_ANIMATION_DURATION); // Wait for the Action Sheet to close before opening reaction picker
setTimeout(() => reactionInit(message), 500);
} }
// close actionSheet when click at header
hideActionSheet(); hideActionSheet();
}; };
@ -352,11 +328,21 @@ const MessageActions = React.memo(
}; };
const getOptions = (message: TAnyMessageModel) => { const getOptions = (message: TAnyMessageModel) => {
const options: TActionSheetOptionsItem[] = []; let options: TActionSheetOptionsItem[] = [];
const videoConfBlock = message.t === 'videoconf';
// Reply
if (!isReadOnly && !tmid) {
options = [
{
title: I18n.t('Reply_in_Thread'),
icon: 'threads',
onPress: () => handleReply(message)
}
];
}
// Quote // Quote
if (!isReadOnly && !videoConfBlock) { if (!isReadOnly) {
options.push({ options.push({
title: I18n.t('Quote'), title: I18n.t('Quote'),
icon: 'quote', icon: 'quote',
@ -364,23 +350,21 @@ const MessageActions = React.memo(
}); });
} }
// Reply // Edit
if (!isReadOnly && !tmid) { if (allowEdit(message)) {
options.push({ options.push({
title: I18n.t('Reply_in_Thread'), title: I18n.t('Edit'),
icon: 'threads', icon: 'edit',
onPress: () => handleReply(message) onPress: () => handleEdit(message)
}); });
} }
// Reply in DM // Permalink
if (room.t !== 'd' && room.t !== 'l' && createDirectMessagePermission && !videoConfBlock) {
options.push({ options.push({
title: I18n.t('Reply_in_direct_message'), title: I18n.t('Permalink'),
icon: 'arrow-back', icon: 'link',
onPress: () => handleReplyInDM(message) onPress: () => handlePermalink(message)
}); });
}
// Create Discussion // Create Discussion
options.push({ options.push({
@ -389,21 +373,21 @@ const MessageActions = React.memo(
onPress: () => handleCreateDiscussion(message) onPress: () => handleCreateDiscussion(message)
}); });
// Permalink // Mark as unread
if (message.u && message.u._id !== user.id) {
options.push({ options.push({
title: I18n.t('Get_link'), title: I18n.t('Mark_unread'),
icon: 'link', icon: 'flag',
onPress: () => handlePermalink(message) onPress: () => handleUnread(message)
}); });
}
// Copy // Copy
if (!videoConfBlock) {
options.push({ options.push({
title: I18n.t('Copy'), title: I18n.t('Copy'),
icon: 'copy', icon: 'copy',
onPress: () => handleCopy(message) onPress: () => handleCopy(message)
}); });
}
// Share // Share
options.push({ options.push({
@ -412,26 +396,8 @@ const MessageActions = React.memo(
onPress: () => handleShare(message) onPress: () => handleShare(message)
}); });
// Edit
if (allowEdit(message) && !videoConfBlock) {
options.push({
title: I18n.t('Edit'),
icon: 'edit',
onPress: () => handleEdit(message)
});
}
// Pin
if (Message_AllowPinning && permissions?.hasPinPermission && !videoConfBlock) {
options.push({
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'pin',
onPress: () => handlePin(message)
});
}
// Star // Star
if (Message_AllowStarring && !videoConfBlock) { if (Message_AllowStarring) {
options.push({ options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'), title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star', icon: message.starred ? 'star-filled' : 'star',
@ -439,12 +405,12 @@ const MessageActions = React.memo(
}); });
} }
// Mark as unread // Pin
if (message.u && message.u._id !== user.id) { if (Message_AllowPinning && permissions?.hasPinPermission) {
options.push({ options.push({
title: I18n.t('Mark_unread'), title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'flag', icon: 'pin',
onPress: () => handleUnread(message) onPress: () => handlePin(message)
}); });
} }
@ -495,7 +461,7 @@ const MessageActions = React.memo(
headerHeight: HEADER_HEIGHT, headerHeight: HEADER_HEIGHT,
customHeader: customHeader:
!isReadOnly || room.reactWhenReadOnly ? ( !isReadOnly || room.reactWhenReadOnly ? (
<Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} /> <Header server={server} handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
) : null ) : null
}); });
}; };
@ -518,10 +484,8 @@ const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'], editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'], deleteMessagePermission: state.permissions['delete-message'],
deleteOwnMessagePermission: state.permissions['delete-own-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'], forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message'], pinMessagePermission: state.permissions['pin-message']
createDirectMessagePermission: state.permissions['create-d']
}); });
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions); export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);

View File

@ -6,31 +6,21 @@ import { Provider } from 'react-redux';
import store from '../../lib/store'; import store from '../../lib/store';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
import { ThemeContext, TSupportedThemes } from '../../theme'; import { useTheme } from '../../theme';
import { EventTypes } from '../EmojiPicker/interfaces'; import { EventTypes } from '../EmojiPicker/interfaces';
import { IEmoji } from '../../definitions';
import { colors } from '../../lib/constants';
const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => { const EmojiKeyboard = () => {
const onItemClicked = (eventType: EventTypes, emoji?: IEmoji) => { const { colors } = useTheme();
const onItemClicked = (eventType: EventTypes, emoji: string | undefined) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji });
}; };
return ( return (
<Provider store={store}> <Provider store={store}>
<ThemeContext.Provider <View style={[styles.emojiKeyboardContainer, { borderTopColor: colors.borderColor }]} testID='messagebox-keyboard-emoji'>
value={{
theme,
colors: colors[theme]
}}
>
<View
style={[styles.emojiKeyboardContainer, { borderTopColor: colors[theme].borderColor }]}
testID='messagebox-keyboard-emoji'
>
<EmojiPicker onItemClicked={onItemClicked} isEmojiKeyboard={true} /> <EmojiPicker onItemClicked={onItemClicked} isEmojiKeyboard={true} />
</View> </View>
</ThemeContext.Provider>
</Provider> </Provider>
); );
}; };

View File

@ -1,116 +1,143 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native'; import { View, Text, Pressable, TextInput, FlatList } from 'react-native';
import { FormTextInput } from '../TextInput/FormTextInput';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { CustomIcon } from '../CustomIcon'; import { CustomIcon } from '../CustomIcon';
import { IEmoji } from '../../definitions'; import { IEmoji } from '../../definitions';
import { useFrequentlyUsedEmoji } from '../../lib/hooks'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed, searchEmojis } from '../../lib/methods'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { useDebounce } from '../../lib/methods/helpers'; import styles from './styles';
import sharedStyles from '../../views/Styles'; import { useFrequentlyUsedEmoji, addFrequentlyUsed } from '../EmojiPicker/frequentlyUsedEmojis';
import { PressableEmoji } from '../EmojiPicker/PressableEmoji'; import { DEFAULT_EMOJIS } from '../EmojiPicker/emojis';
import { EmojiSearch } from '../EmojiPicker/EmojiSearch';
import { EMOJI_BUTTON_SIZE } from '../EmojiPicker/styles';
import { events, logEvent } from '../../lib/methods/helpers/log';
const BUTTON_HIT_SLOP = { top: 4, right: 4, bottom: 4, left: 4 }; const BUTTON_HIT_SLOP = { top: 4, right: 4, bottom: 4, left: 4 };
const EMOJI_SIZE = 30;
const styles = StyleSheet.create({
listContainer: {
height: EMOJI_BUTTON_SIZE,
margin: 8,
flexGrow: 1
},
container: {
borderTopWidth: 1
},
searchContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
marginBottom: 12
},
backButton: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 4
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
emptyText: {
...sharedStyles.textRegular,
fontSize: 16
},
inputContainer: {
flex: 1
}
});
interface IEmojiSearchBarProps { interface IEmojiSearchBarProps {
openEmoji: () => void; openEmoji: () => void;
closeEmoji: () => void; onChangeText: (value: string) => void;
onEmojiSelected: (emoji: IEmoji) => void; emojis: (IEmoji | string)[];
onEmojiSelected: (emoji: IEmoji | string) => void;
baseUrl: string;
} }
const EmojiSearchBar = ({ openEmoji, closeEmoji, onEmojiSelected }: IEmojiSearchBarProps): React.ReactElement => { interface IListItem {
emoji: IEmoji | string;
onEmojiSelected: (emoji: IEmoji | string) => void;
baseUrl: string;
}
const Emoji = ({ emoji, baseUrl }: { emoji: IEmoji | string; baseUrl: string }): React.ReactElement => {
const { colors } = useTheme(); const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>(''); if (typeof emoji === 'string') {
const { frequentlyUsed } = useFrequentlyUsedEmoji(true); return (
const [emojis, setEmojis] = useState<IEmoji[]>([]); <Text style={[styles.searchedEmoji, { fontSize: EMOJI_SIZE, color: colors.backdropColor }]}>
{shortnameToUnicode(`:${emoji}:`)}
const handleTextChange = useDebounce(async (text: string) => { </Text>
logEvent(events.MB_SB_EMOJI_SEARCH); );
setSearchText(text); }
const result = await searchEmojis(text); return (
setEmojis(result); <CustomEmoji
}, 300); style={[styles.emojiSearchCustomEmoji, { height: EMOJI_SIZE, width: EMOJI_SIZE }]}
emoji={emoji}
const handleEmojiSelected = (emoji: IEmoji) => { baseUrl={baseUrl}
logEvent(events.MB_SB_EMOJI_SELECTED); />
onEmojiSelected(emoji); );
addFrequentlyUsed(emoji);
}; };
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={handleEmojiSelected} />; const ListItem = ({ emoji, onEmojiSelected, baseUrl }: IListItem): React.ReactElement => {
const key = typeof emoji === 'string' ? emoji : emoji?.name || emoji?.content;
const onPress = () => {
onEmojiSelected(emoji);
if (typeof emoji === 'string') {
addFrequentlyUsed({ content: emoji, name: emoji, isCustom: false });
} else {
addFrequentlyUsed({
content: emoji?.content || emoji?.name,
name: emoji?.name,
extension: emoji.extension,
isCustom: true
});
}
};
return ( return (
<View style={[styles.container, { borderTopColor: colors.borderColor, backgroundColor: colors.messageboxBackground }]}> <View style={[styles.emojiContainer]} key={key} testID={`searched-emoji-${key}`}>
<FlatList <Pressable onPress={onPress}>
horizontal <Emoji emoji={emoji} baseUrl={baseUrl} />
data={searchText ? emojis : frequentlyUsed}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
ListEmptyComponent={() => (
<View style={styles.emptyContainer} testID='no-results-found'>
<Text style={[styles.emptyText, { color: colors.auxiliaryText }]}>{I18n.t('No_results_found')}</Text>
</View>
)}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
contentContainerStyle={styles.listContainer}
keyboardShouldPersistTaps='always'
/>
<View style={styles.searchContainer}>
<Pressable
style={({ pressed }: { pressed: boolean }) => [styles.backButton, { opacity: pressed ? 0.7 : 1 }]}
onPress={openEmoji}
hitSlop={BUTTON_HIT_SLOP}
testID='openback-emoji-keyboard'
>
<CustomIcon name='chevron-left' size={24} color={colors.auxiliaryTintColor} />
</Pressable> </Pressable>
<View style={styles.inputContainer}>
<EmojiSearch onBlur={closeEmoji} onChangeText={handleTextChange} />
</View>
</View>
</View> </View>
); );
}; };
const EmojiSearchBar = React.forwardRef<TextInput, IEmojiSearchBarProps>(
({ openEmoji, onChangeText, emojis, onEmojiSelected, baseUrl }, ref) => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const { frequentlyUsed } = useFrequentlyUsedEmoji();
const frequentlyUsedWithDefaultEmojis = frequentlyUsed
.filter(emoji => {
if (typeof emoji === 'string') return !DEFAULT_EMOJIS.includes(emoji);
return !DEFAULT_EMOJIS.includes(emoji.name);
})
.concat(DEFAULT_EMOJIS);
const handleTextChange = (text: string) => {
setSearchText(text);
onChangeText(text);
};
return (
<View
style={[styles.emojiSearchViewContainer, { borderTopColor: colors.borderColor, backgroundColor: colors.backgroundColor }]}
>
<FlatList
horizontal
data={searchText ? emojis : frequentlyUsedWithDefaultEmojis}
renderItem={({ item }) => <ListItem emoji={item} onEmojiSelected={onEmojiSelected} baseUrl={baseUrl} />}
showsHorizontalScrollIndicator={false}
ListEmptyComponent={() => (
<View style={styles.listEmptyComponent} testID='no-results-found'>
<Text style={{ color: colors.auxiliaryText }}>{I18n.t('No_results_found')}</Text>
</View>
)}
// @ts-ignore
keyExtractor={item => item?.content || item?.name || item}
contentContainerStyle={styles.emojiListContainer}
keyboardShouldPersistTaps='always'
/>
<View style={styles.emojiSearchbarContainer}>
<Pressable
style={({ pressed }: { pressed: boolean }) => [styles.openEmojiKeyboard, { opacity: pressed ? 0.7 : 1 }]}
onPress={openEmoji}
hitSlop={BUTTON_HIT_SLOP}
testID='openback-emoji-keyboard'
>
<CustomIcon name='chevron-left' size={30} color={colors.collapsibleChevron} />
</Pressable>
<View style={styles.emojiSearchInput}>
<FormTextInput
inputRef={ref}
autoCapitalize='none'
autoCorrect={false}
blurOnSubmit
placeholder={I18n.t('Search_emoji')}
returnKeyType='search'
underlineColorAndroid='transparent'
onChangeText={handleTextChange}
style={[styles.emojiSearchbar, { backgroundColor: colors.passcodeButtonActive }]}
containerStyle={styles.textInputContainer}
value={searchText}
onClearInput={() => handleTextChange('')}
iconRight={'search'}
testID='emoji-searchbar-input'
/>
</View>
</View>
</View>
);
}
);
export default EmojiSearchBar; export default EmojiSearchBar;

View File

@ -1,9 +1,10 @@
import React from 'react'; import React, { useContext } from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import { IEmoji } from '../../../definitions/IEmoji'; import { IEmoji } from '../../../definitions/IEmoji';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import MessageboxContext from '../Context';
import styles from '../styles'; import styles from '../styles';
interface IMessageBoxMentionEmoji { interface IMessageBoxMentionEmoji {
@ -11,10 +12,13 @@ interface IMessageBoxMentionEmoji {
} }
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => { const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
if (typeof item === 'string') { const context = useContext(MessageboxContext);
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>; const { baseUrl } = context;
if (item.name) {
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
} }
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} />; return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
}; };
export default MentionEmoji; export default MentionEmoji;

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Text, TouchableOpacity, View } from 'react-native'; import { Text, TouchableOpacity } from 'react-native';
import { themes } from '../../../lib/constants'; import { themes } from '../../../lib/constants';
import { IEmoji } from '../../../definitions/IEmoji'; import { IEmoji } from '../../../definitions/IEmoji';
@ -37,9 +37,7 @@ const MentionItemContent = React.memo(({ trackingType, item }: IMessageBoxMentio
case MENTIONS_TRACKING_TYPE_COMMANDS: case MENTIONS_TRACKING_TYPE_COMMANDS:
return ( return (
<> <>
<View style={[styles.slash, { backgroundColor: themes[theme].borderColor }]}> <Text style={[styles.slash, { backgroundColor: themes[theme].borderColor, color: themes[theme].tintColor }]}>/</Text>
<Text style={{ color: themes[theme].tintColor }}>/</Text>
</View>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.id}</Text>
</> </>
); );

View File

@ -29,8 +29,7 @@ const styles = StyleSheet.create({
}, },
username: { username: {
fontSize: 16, fontSize: 16,
...sharedStyles.textMedium, ...sharedStyles.textMedium
flexShrink: 1
}, },
time: { time: {
fontSize: 12, fontSize: 12,
@ -68,7 +67,7 @@ const ReplyPreview = React.memo(
<View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}>
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}> <View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
<View style={styles.header}> <View style={styles.header}>
<Text numberOfLines={1} style={[styles.username, { color: themes[theme].tintColor }]}> <Text style={[styles.username, { color: themes[theme].tintColor }]}>
{useRealName ? message.u?.name : message.u?.username} {useRealName ? message.u?.name : message.u?.username}
</Text> </Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>

View File

@ -13,11 +13,12 @@ import { TextInput, IThemedTextInput } from '../TextInput';
import { userTyping as userTypingAction } from '../../actions/room'; import { userTyping as userTypingAction } from '../../actions/room';
import styles from './styles'; import styles from './styles';
import database from '../../lib/database'; import database from '../../lib/database';
import { emojis } from '../EmojiPicker/emojis';
import log, { events, logEvent } from '../../lib/methods/helpers/log'; import log, { events, logEvent } from '../../lib/methods/helpers/log';
import RecordAudio from './RecordAudio'; import RecordAudio from './RecordAudio';
import I18n from '../../i18n'; import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview'; import ReplyPreview from './ReplyPreview';
import { themes, emojis } from '../../lib/constants'; import { themes } from '../../lib/constants';
import LeftButtons from './LeftButtons'; import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons'; import RightButtons from './RightButtons';
import { canUploadFile } from '../../lib/methods/helpers/media'; import { canUploadFile } from '../../lib/methods/helpers/media';
@ -33,7 +34,8 @@ import {
MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_ROOMS, MENTIONS_TRACKING_TYPE_ROOMS,
MENTIONS_TRACKING_TYPE_USERS, MENTIONS_TRACKING_TYPE_USERS,
TIMEOUT_CLOSE_EMOJI TIMEOUT_CLOSE_EMOJI,
MAX_EMOJIS_TO_DISPLAY
} from './constants'; } from './constants';
import CommandsPreview from './CommandsPreview'; import CommandsPreview from './CommandsPreview';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
@ -50,8 +52,7 @@ import {
TGetCustomEmoji, TGetCustomEmoji,
TSubscriptionModel, TSubscriptionModel,
TThreadModel, TThreadModel,
IMessage, IMessage
IEmoji
} from '../../definitions'; } from '../../definitions';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods'; import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
@ -133,6 +134,7 @@ interface IMessageBoxState {
mentionLoading: boolean; mentionLoading: boolean;
permissionToUpload: boolean; permissionToUpload: boolean;
showEmojiSearchbar: boolean; showEmojiSearchbar: boolean;
searchedEmojis: any[];
} }
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> { class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -164,6 +166,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
private typingTimeout: any; private typingTimeout: any;
private emojiSearchbarRef: any;
static defaultProps = { static defaultProps = {
message: { message: {
id: '' id: ''
@ -188,7 +192,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tshow: this.sendThreadToChannel, tshow: this.sendThreadToChannel,
mentionLoading: false, mentionLoading: false,
permissionToUpload: true, permissionToUpload: true,
showEmojiSearchbar: false showEmojiSearchbar: false,
searchedEmojis: []
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 }; this.selection = { start: 0, end: 0 };
@ -237,7 +242,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
async componentDidMount() { async componentDidMount() {
const db = database.active; const db = database.active;
const { rid, tmid, navigation, sharing, usedCannedResponse } = this.props; const { rid, tmid, navigation, sharing, usedCannedResponse, isMasterDetail } = this.props;
let msg; let msg;
try { try {
const threadsCollection = db.get('threads'); const threadsCollection = db.get('threads');
@ -272,7 +277,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands); EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
} }
if (usedCannedResponse) { if (isMasterDetail && usedCannedResponse) {
this.onChangeText(usedCannedResponse); this.onChangeText(usedCannedResponse);
} }
@ -302,7 +307,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (usedCannedResponse !== nextProps.usedCannedResponse) { if (usedCannedResponse !== nextProps.usedCannedResponse) {
this.onChangeText(nextProps.usedCannedResponse ?? ''); this.onChangeText(nextProps.usedCannedResponse ?? '');
} }
if (sharing && !replying) { if (sharing) {
this.setInput(nextProps.message.msg ?? ''); this.setInput(nextProps.message.msg ?? '');
return; return;
} }
@ -334,7 +339,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
mentionLoading, mentionLoading,
trackingType, trackingType,
permissionToUpload, permissionToUpload,
showEmojiSearchbar showEmojiSearchbar,
searchedEmojis
} = this.state; } = this.state;
const { const {
@ -354,9 +360,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) { if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true; return true;
} }
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
return true;
}
if (!isFocused()) { if (!isFocused()) {
return false; return false;
} }
@ -405,6 +408,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextProps.goToCannedResponses !== goToCannedResponses) { if (nextProps.goToCannedResponses !== goToCannedResponses) {
return true; return true;
} }
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
return true;
}
if (!dequal(nextState.searchedEmojis, searchedEmojis)) {
return true;
}
return false; return false;
} }
@ -531,10 +540,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}, 100); }, 100);
onKeyboardResigned = () => { onKeyboardResigned = () => {
const { showEmojiSearchbar } = this.state;
if (!showEmojiSearchbar) {
this.closeEmoji(); this.closeEmoji();
}
}; };
onPressMention = (item: any) => { onPressMention = (item: any) => {
@ -592,7 +598,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
}; };
onKeyboardItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: IEmoji }) => { onKeyboardItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: string }) => {
const { eventType, emoji } = params; const { eventType, emoji } = params;
const { text } = this; const { text } = this;
let newText = ''; let newText = '';
@ -603,7 +609,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
switch (eventType) { switch (eventType) {
case EventTypes.BACKSPACE_PRESSED: case EventTypes.BACKSPACE_PRESSED:
logEvent(events.MB_BACKSPACE);
const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/; const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/;
let charsToRemove = 1; let charsToRemove = 1;
const lastEmoji = text.substr(cursor > 0 ? cursor - 2 : text.length - 2, cursor > 0 ? cursor : text.length); const lastEmoji = text.substr(cursor > 0 ? cursor - 2 : text.length - 2, cursor > 0 ? cursor : text.length);
@ -616,22 +621,18 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.setShowSend(newText !== ''); this.setShowSend(newText !== '');
break; break;
case EventTypes.EMOJI_PRESSED: case EventTypes.EMOJI_PRESSED:
logEvent(events.MB_EMOJI_SELECTED); newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
let emojiText = ''; newCursor = cursor + emoji.length;
if (typeof emoji === 'string') {
const shortname = `:${emoji}:`;
emojiText = shortnameToUnicode(shortname);
} else {
emojiText = `:${emoji.name}:`;
}
newText = `${text.substr(0, cursor)}${emojiText}${text.substr(cursor)}`;
newCursor = cursor + emojiText.length;
this.setInput(newText, { start: newCursor, end: newCursor }); this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true); this.setShowSend(true);
break; break;
case EventTypes.SEARCH_PRESSED: case EventTypes.SEARCH_PRESSED:
logEvent(events.MB_EMOJI_SEARCH_PRESSED);
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: true }); this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: true });
setTimeout(() => {
if (this.emojiSearchbarRef && this.emojiSearchbarRef.focus) {
this.emojiSearchbarRef.focus();
}
}, 400);
break; break;
default: default:
// Do nothing // Do nothing
@ -658,8 +659,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
getUsers = debounce(async (keyword: any) => { getUsers = debounce(async (keyword: any) => {
const { rid } = this.props; let res = await search({ text: keyword, filterRooms: false, filterUsers: true });
let res = await search({ text: keyword, filterRooms: false, filterUsers: true, rid });
res = [...this.getFixedMentions(keyword), ...res]; res = [...this.getFixedMentions(keyword), ...res];
this.setState({ mentions: res, mentionLoading: false }); this.setState({ mentions: res, mentionLoading: false });
}, 300); }, 300);
@ -857,21 +857,14 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
openShareView = (attachments: any) => { openShareView = (attachments: any) => {
const { message, replyCancel, replyWithMention, replying } = this.props; const { message, replyCancel, replyWithMention } = this.props;
// Start a thread with an attachment // Start a thread with an attachment
let value: TThreadModel | IMessage = this.thread; let value: TThreadModel | IMessage = this.thread;
if (replyWithMention) { if (replyWithMention) {
value = message; value = message;
replyCancel(); replyCancel();
} }
Navigation.navigate('ShareView', { Navigation.navigate('ShareView', { room: this.room, thread: value, attachments });
room: this.room,
thread: value,
attachments,
replying,
replyingMessage: message,
closeReply: replyCancel
});
}; };
createDiscussion = () => { createDiscussion = () => {
@ -963,15 +956,18 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
closeEmoji = () => { closeEmoji = () => {
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: false }); this.setState({ showEmojiKeyboard: false });
}; };
closeEmojiKeyboardAndFocus = () => { closeEmojiKeyboardAndFocus = () => {
logEvent(events.ROOM_CLOSE_EMOJI);
this.closeEmoji(); this.closeEmoji();
this.focus(); this.focus();
}; };
closeEmojiSearchbar = () => {
this.setState({ showEmojiSearchbar: false });
};
closeEmojiAndAction = (action?: Function, params?: any) => { closeEmojiAndAction = (action?: Function, params?: any) => {
const { showEmojiKeyboard } = this.state; const { showEmojiKeyboard } = this.state;
@ -1049,7 +1045,16 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
// Legacy reply or quote (quote is a reply without mention) // Legacy reply or quote (quote is a reply without mention)
} else { } else {
const msg = await this.formatReplyMessage(replyingMessage, message); const { user, roomType } = this.props;
const permalink = await this.getPermalink(replyingMessage);
let msg = `[ ](${permalink}) `;
// if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) {
msg += `@${replyingMessage?.u?.username} `;
}
msg = `${msg} ${message}`;
onSubmit(msg); onSubmit(msg);
} }
replyCancel(); replyCancel();
@ -1061,20 +1066,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
}; };
formatReplyMessage = async (replyingMessage: IMessage, message = '') => {
const { user, roomType, replyWithMention, serverVersion } = this.props;
const permalink = await this.getPermalink(replyingMessage);
let msg = `[ ](${permalink}) `;
// if original message wasn't sent by current user and neither from a direct room
if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) {
msg += `@${replyingMessage?.u?.username} `;
}
const connectionString = compareServerVersion(serverVersion, 'lowerThan', '5.0.0') ? ' ' : '\n';
return `${msg}${connectionString}${message}`;
};
updateMentions = (keyword: any, type: string) => { updateMentions = (keyword: any, type: string) => {
if (type === MENTIONS_TRACKING_TYPE_USERS) { if (type === MENTIONS_TRACKING_TYPE_USERS) {
this.getUsers(keyword); this.getUsers(keyword);
@ -1155,15 +1146,44 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
renderEmojiSearchbar = () => { renderEmojiSearchbar = () => {
const { showEmojiSearchbar } = this.state; const { showEmojiSearchbar, searchedEmojis } = this.state;
const { baseUrl } = this.props;
const searchEmojis = debounce(async (keyword: any) => {
const customEmojis = await this.getCustomEmojis(keyword, MAX_EMOJIS_TO_DISPLAY / 2);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MAX_EMOJIS_TO_DISPLAY / 2);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MAX_EMOJIS_TO_DISPLAY);
this.setState({ searchedEmojis: mergedEmojis });
}, 300);
const onChangeText = (value: string) => {
searchEmojis(value);
};
const onEmojiSelected = (emoji: any) => {
let selectedEmoji;
if (emoji.name || emoji.content) {
selectedEmoji = `:${emoji.name || emoji.content}:`;
} else {
selectedEmoji = shortnameToUnicode(`:${emoji}:`);
}
const { text } = this;
let newText = '';
const { start, end } = this.selection;
const cursor = Math.max(start, end);
newText = `${text.substr(0, cursor)}${selectedEmoji}${text.substr(cursor)}`;
const newCursor = cursor + selectedEmoji.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
};
return showEmojiSearchbar ? ( return showEmojiSearchbar ? (
<EmojiSearchbar <EmojiSearchbar
ref={ref => (this.emojiSearchbarRef = ref)}
openEmoji={this.openEmoji} openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji} onChangeText={onChangeText}
onEmojiSelected={(emoji: IEmoji) => { emojis={searchedEmojis}
this.onKeyboardItemSelected('EmojiKeyboard', { eventType: EventTypes.EMOJI_PRESSED, emoji }); baseUrl={baseUrl}
}} onEmojiSelected={onEmojiSelected}
/> />
) : null; ) : null;
}; };
@ -1181,7 +1201,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const { const {
recording, recording,
showEmojiKeyboard, showEmojiKeyboard,
showEmojiSearchbar,
showSend, showSend,
mentions, mentions,
trackingType, trackingType,
@ -1244,7 +1263,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const textInputAndButtons = !recording ? ( const textInputAndButtons = !recording ? (
<> <>
<LeftButtons <LeftButtons
showEmojiKeyboard={showEmojiKeyboard || showEmojiSearchbar} showEmojiKeyboard={showEmojiKeyboard}
editing={editing} editing={editing}
editCancel={this.editCancel} editCancel={this.editCancel}
openEmoji={this.openEmoji} openEmoji={this.openEmoji}
@ -1264,6 +1283,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
defaultValue='' defaultValue=''
multiline multiline
testID={`messagebox-input${tmid ? '-thread' : ''}`} testID={`messagebox-input${tmid ? '-thread' : ''}`}
onFocus={this.closeEmojiSearchbar}
{...isAndroidTablet} {...isAndroidTablet}
/> />
<RightButtons <RightButtons

View File

@ -105,7 +105,7 @@ export default StyleSheet.create({
}, },
emojiKeyboardContainer: { emojiKeyboardContainer: {
flex: 1, flex: 1,
borderTopWidth: 1 borderTopWidth: StyleSheet.hairlineWidth
}, },
slash: { slash: {
height: 30, height: 30,
@ -113,7 +113,7 @@ export default StyleSheet.create({
padding: 5, padding: 5,
paddingHorizontal: 12, paddingHorizontal: 12,
marginHorizontal: 10, marginHorizontal: 10,
borderRadius: 4 borderRadius: 2
}, },
commandPreviewImage: { commandPreviewImage: {
justifyContent: 'center', justifyContent: 'center',
@ -157,5 +157,34 @@ export default StyleSheet.create({
fontSize: 12, fontSize: 12,
marginLeft: 4, marginLeft: 4,
...sharedStyles.textRegular ...sharedStyles.textRegular
},
searchedEmoji: {
backgroundColor: 'transparent'
},
emojiContainer: { justifyContent: 'center', marginHorizontal: 2 },
emojiListContainer: { height: 50, paddingHorizontal: 5, marginVertical: 5, flexGrow: 1 },
emojiSearchViewContainer: {
borderTopWidth: 1
},
emojiSearchbarContainer: {
flexDirection: 'row',
height: 50,
marginBottom: 15,
justifyContent: 'center',
alignItems: 'center'
},
openEmojiKeyboard: { marginHorizontal: 10, justifyContent: 'center' },
emojiSearchbar: { paddingHorizontal: 20, borderRadius: 2, fontSize: 16 },
textInputContainer: { justifyContent: 'center', marginBottom: 0, marginRight: 15 },
listEmptyComponent: {
width: '100%',
alignItems: 'center',
justifyContent: 'center'
},
emojiSearchCustomEmoji: {
margin: 4
},
emojiSearchInput: {
flex: 1
} }
}); });

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, StyleProp, ViewStyle } from 'react-native'; import { Text } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../../lib/constants'; import { themes } from '../../../lib/constants';
@ -12,17 +12,16 @@ interface IPasscodeButton {
icon?: TIconsName; icon?: TIconsName;
disabled?: boolean; disabled?: boolean;
onPress?: Function; onPress?: Function;
style?: StyleProp<ViewStyle>;
} }
const Button = React.memo(({ style, text, disabled, onPress, icon }: IPasscodeButton) => { const Button = React.memo(({ text, disabled, onPress, icon }: IPasscodeButton) => {
const { theme } = useTheme(); const { theme } = useTheme();
const press = () => onPress && onPress(text); const press = () => onPress && onPress(text);
return ( return (
<Touch <Touch
style={[styles.buttonView, { backgroundColor: 'transparent' }, style]} style={[styles.buttonView, { backgroundColor: 'transparent' }]}
underlayColor={themes[theme].passcodeButtonActive} underlayColor={themes[theme].passcodeButtonActive}
rippleColor={themes[theme].passcodeButtonActive} rippleColor={themes[theme].passcodeButtonActive}
enabled={!disabled} enabled={!disabled}

View File

@ -1,10 +1,9 @@
import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'; import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { Col, Grid, Row } from 'react-native-easy-grid'; import { Col, Grid, Row } from 'react-native-easy-grid';
import range from 'lodash/range'; import range from 'lodash/range';
import { View } from 'react-native'; import { View } from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import Orientation from 'react-native-orientation-locker';
import styles from './styles'; import styles from './styles';
import Button from './Button'; import Button from './Button';
@ -15,8 +14,6 @@ import { useTheme } from '../../../theme';
import LockIcon from './LockIcon'; import LockIcon from './LockIcon';
import Title from './Title'; import Title from './Title';
import Subtitle from './Subtitle'; import Subtitle from './Subtitle';
import { useDimensions } from '../../../dimensions';
import { isTablet } from '../../../lib/methods/helpers';
interface IPasscodeBase { interface IPasscodeBase {
type: string; type: string;
@ -37,25 +34,7 @@ export interface IBase {
const Base = forwardRef<IBase, IPasscodeBase>( const Base = forwardRef<IBase, IPasscodeBase>(
({ type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }, ref) => { ({ type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }, ref) => {
useLayoutEffect(() => {
if (!isTablet) {
Orientation.lockToPortrait();
}
return () => {
if (!isTablet) {
Orientation.unlockAllOrientations();
}
};
}, []);
const { theme } = useTheme(); const { theme } = useTheme();
const { height } = useDimensions();
// 206 is the height of the header calculating the margins, icon size height, title font size and subtitle height.
// 56 is a fixed number to decrease the height of button numbers.
const dinamicHeight = (height - 206 - 56) / 4;
const heightButtonRow = { height: dinamicHeight > 102 ? 102 : dinamicHeight };
const rootRef = useRef<Animatable.View & View>(null); const rootRef = useRef<Animatable.View & View>(null);
const dotsRef = useRef<Animatable.View & View>(null); const dotsRef = useRef<Animatable.View & View>(null);
@ -124,40 +103,40 @@ const Base = forwardRef<IBase, IPasscodeBase>(
<Dots passcode={passcode} length={PASSCODE_LENGTH} /> <Dots passcode={passcode} length={PASSCODE_LENGTH} />
</Animatable.View> </Animatable.View>
</Row> </Row>
<Row style={[styles.row, heightButtonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(1, 4).map(i => ( {range(1, 4).map(i => (
<Col key={i} style={[styles.colButton, heightButtonRow]}> <Col key={i} style={styles.colButton}>
<Button style={heightButtonRow} text={i.toString()} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, heightButtonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(4, 7).map(i => ( {range(4, 7).map(i => (
<Col key={i} style={[styles.colButton, heightButtonRow]}> <Col key={i} style={styles.colButton}>
<Button style={heightButtonRow} text={i.toString()} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, heightButtonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{range(7, 10).map(i => ( {range(7, 10).map(i => (
<Col key={i} style={[styles.colButton, heightButtonRow]}> <Col key={i} style={styles.colButton}>
<Button style={heightButtonRow} text={i.toString()} onPress={onPressNumber} /> <Button text={i.toString()} onPress={onPressNumber} />
</Col> </Col>
))} ))}
</Row> </Row>
<Row style={[styles.row, heightButtonRow]}> <Row style={[styles.row, styles.buttonRow]}>
{showBiometry ? ( {showBiometry ? (
<Col style={[styles.colButton, heightButtonRow]}> <Col style={styles.colButton}>
<Button style={heightButtonRow} icon='fingerprint' onPress={onBiometryPress} /> <Button icon='fingerprint' onPress={onBiometryPress} />
</Col> </Col>
) : ( ) : (
<Col style={[styles.colButton, heightButtonRow]} /> <Col style={styles.colButton} />
)} )}
<Col style={[styles.colButton, heightButtonRow]}> <Col style={styles.colButton}>
<Button style={heightButtonRow} text='0' onPress={onPressNumber} /> <Button text='0' onPress={onPressNumber} />
</Col> </Col>
<Col style={[styles.colButton, heightButtonRow]}> <Col style={styles.colButton}>
<Button style={heightButtonRow} icon='backspace' onPress={onPressDelete} /> <Button icon='backspace' onPress={onPressDelete} />
</Col> </Col>
</Row> </Row>
</Grid> </Grid>

View File

@ -18,12 +18,16 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
}, },
buttonRow: {
height: 102
},
colButton: { colButton: {
flex: 0, flex: 0,
marginLeft: 12, marginLeft: 12,
marginRight: 12, marginRight: 12,
alignItems: 'center', alignItems: 'center',
width: 78 width: 78,
height: 78
}, },
buttonText: { buttonText: {
fontSize: 28, fontSize: 28,
@ -33,6 +37,7 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: 78, width: 78,
height: 78,
borderRadius: 4 borderRadius: 4
}, },
textTitle: { textTitle: {

View File

@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { StyleSheet, Text, Pressable, View, ScrollView } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { TabView, SceneRendererProps, NavigationState } from 'react-native-tab-view';
import Emoji from './message/Emoji';
import { useTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji';
import { IReaction } from '../definitions';
import Avatar from './Avatar';
import sharedStyles from '../views/Styles';
const MIN_TAB_WIDTH = 70;
const styles = StyleSheet.create({
reactionsListContainer: { height: '100%', width: '100%' },
tabBarItem: {
paddingHorizontal: 10,
paddingBottom: 10,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
},
reactionCount: { marginLeft: 5 },
emojiName: { margin: 10 },
userItemContainer: { marginHorizontal: 10, marginVertical: 5, flexDirection: 'row' },
usernameContainer: { marginHorizontal: 10, justifyContent: 'center' },
usernameText: { fontSize: 17, ...sharedStyles.textMedium },
standardEmojiStyle: { fontSize: 20, color: '#fff' },
customEmojiStyle: { width: 25, height: 25 }
});
type Route = {
key: string;
reaction: IReaction;
};
type State = NavigationState<Route>;
interface IReactionsListBase {
baseUrl: string;
getCustomEmoji: TGetCustomEmoji;
}
interface IReactionsListProps extends IReactionsListBase {
reactions?: IReaction[];
width: number;
}
interface ITabBarItem extends IReactionsListBase {
tab: { key: string; reaction: IReaction };
jumpTo?: (key: string) => void;
}
interface IReactionsTabBar extends IReactionsListBase {
activeTab?: number;
tabs?: { key: string; reaction: IReaction }[];
jumpTo?: (key: string) => void;
width: number;
}
const TabBarItem = ({ tab, jumpTo, baseUrl, getCustomEmoji }: ITabBarItem) => {
const { colors } = useTheme();
return (
<Pressable
key={tab.key}
onPress={() => {
jumpTo?.(tab.key);
}}
style={({ pressed }: { pressed: boolean }) => ({
opacity: pressed ? 0.7 : 1
})}
>
<View style={styles.tabBarItem}>
<Emoji
content={tab.key}
standardEmojiStyle={styles.standardEmojiStyle}
customEmojiStyle={styles.customEmojiStyle}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
/>
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.reaction.usernames.length}</Text>
</View>
</Pressable>
);
};
const ReactionsTabBar = ({ tabs, activeTab, jumpTo, baseUrl, getCustomEmoji, width }: IReactionsTabBar) => {
const tabWidth = tabs && Math.max(width / tabs.length, MIN_TAB_WIDTH);
const { colors } = useTheme();
return (
<View>
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
{tabs?.map((tab, index) => {
const isActiveTab = activeTab === index;
return (
<View
style={{
width: tabWidth,
borderBottomWidth: isActiveTab ? 2 : 1,
borderColor: isActiveTab ? colors.tintActive : colors.separatorColor
}}
>
<TabBarItem tab={tab} jumpTo={jumpTo} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} />
</View>
);
})}
</ScrollView>
</View>
);
};
const UsersList = ({ tabLabel }: { tabLabel: IReaction }) => {
const { colors } = useTheme();
const { emoji, usernames } = tabLabel;
return (
<FlatList
data={usernames}
ListHeaderComponent={() => (
<View style={styles.emojiName}>
<Text style={{ color: colors.auxiliaryTintColor }}>{emoji}</Text>
</View>
)}
renderItem={({ item }) => (
<View style={styles.userItemContainer}>
<Avatar text={item} size={36} />
<View style={styles.usernameContainer}>
<Text style={[styles.usernameText, { color: colors.titleText }]}>{item}</Text>
</View>
</View>
)}
keyExtractor={item => item}
/>
);
};
const ReactionsList = ({ reactions, baseUrl, getCustomEmoji, width }: IReactionsListProps): React.ReactElement => {
const [index, setIndex] = useState(0);
// sorting reactions in descending order on the basic of number of users reacted
const sortedReactions = reactions?.sort((reaction1, reaction2) => reaction2.usernames.length - reaction1.usernames.length);
const routes = sortedReactions ? sortedReactions?.map(reaction => ({ key: reaction.emoji, reaction })) : [];
return (
<View style={styles.reactionsListContainer}>
<TabView
lazy
navigationState={{ index, routes }}
renderScene={({ route }) => <UsersList tabLabel={route.reaction} />}
onIndexChange={setIndex}
renderTabBar={(props: SceneRendererProps & { navigationState: State }) => (
<ReactionsTabBar
tabs={routes}
jumpTo={props.jumpTo}
width={width}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
activeTab={index}
/>
)}
/>
</View>
);
};
export default ReactionsList;

View File

@ -1,75 +0,0 @@
import React from 'react';
import { Text, View, FlatList } from 'react-native';
import Emoji from '../message/Emoji';
import { useTheme } from '../../theme';
import { IReaction } from '../../definitions';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import I18n from '../../i18n';
import styles from './styles';
import { useAppSelector } from '../../lib/hooks';
interface IAllReactionsListItemProps {
getCustomEmoji: TGetCustomEmoji;
item: IReaction;
}
interface IAllTabProps {
getCustomEmoji: TGetCustomEmoji;
tabLabel: IReaction;
reactions?: IReaction[];
}
const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => {
const { colors } = useTheme();
const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name);
const username = useAppSelector(state => state.login.user.username);
const count = item.usernames.length;
let displayNames;
if (useRealName && item.names) {
displayNames = item.names
.slice(0, 3)
.map((name, index) => (item.usernames[index] === username ? I18n.t('you') : name))
.join(', ');
} else {
displayNames = item.usernames
.slice(0, 3)
.map((otherUsername: string) => (username === otherUsername ? I18n.t('you') : otherUsername))
.join(', ');
}
if (count > 3) {
displayNames = `${displayNames} ${I18n.t('and_more')} ${count - 3}`;
} else {
displayNames = displayNames.replace(/,(?=[^,]*$)/, ` ${I18n.t('and')}`);
}
return (
<View style={styles.listItemContainer}>
<Emoji
content={item.emoji}
standardEmojiStyle={styles.allTabStandardEmojiStyle}
customEmojiStyle={styles.allTabCustomEmojiStyle}
getCustomEmoji={getCustomEmoji}
/>
<View style={styles.textContainer}>
<Text style={[styles.allListNPeopleReacted, { color: colors.bodyText }]}>
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
</Text>
<Text style={[styles.allListWhoReacted, { color: colors.auxiliaryText }]}>{displayNames}</Text>
</View>
</View>
);
};
const AllTab = ({ reactions, getCustomEmoji }: IAllTabProps): React.ReactElement => (
<View style={styles.allTabContainer} testID='reactionsListAllTab'>
<FlatList
data={reactions}
contentContainerStyle={styles.listContainer}
renderItem={({ item }) => <AllReactionsListItem item={item} getCustomEmoji={getCustomEmoji} />}
keyExtractor={item => item.emoji}
/>
</View>
);
export default AllTab;

View File

@ -1,71 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import { TGetCustomEmoji, ICustomEmoji } from '../../definitions';
import ReactionsList from '.';
import { mockedStore as store } from '../../reducers/mockedStore';
import { updateSettings } from '../../actions/settings';
const getCustomEmoji: TGetCustomEmoji = content => {
const customEmoji = {
marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' }
}[content] as ICustomEmoji;
return customEmoji;
};
const reactions = [
{
emoji: ':marioparty:',
_id: 'marioparty',
usernames: ['rocket.cat', 'diego.mello'],
names: ['Rocket Cat', 'Diego Mello']
},
{
emoji: ':react_rocket:',
_id: 'react_rocket',
usernames: ['rocket.cat', 'diego.mello'],
names: ['Rocket Cat', 'Diego Mello']
},
{
emoji: ':nyan_rocket:',
_id: 'nyan_rocket',
usernames: ['rocket.cat'],
names: ['Rocket Cat']
},
{
emoji: ':grinning:',
_id: 'grinning',
usernames: ['diego.mello'],
names: ['Diego Mello']
},
{
emoji: ':tada:',
_id: 'tada',
usernames: ['diego.mello'],
names: ['Diego Mello']
}
];
export const ReactionsListStory = () => {
store.dispatch(updateSettings('UI_Use_Real_Name', false));
return (
<View style={{ paddingVertical: 10, flex: 1 }}>
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
</View>
);
};
export const ReactionsListFullName = () => {
store.dispatch(updateSettings('UI_Use_Real_Name', true));
return (
<View style={{ paddingVertical: 10, flex: 1 }}>
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
</View>
);
};
export default {
title: 'ReactionsList'
};

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