Merge branch 'develop' into chore.hooks-app-index

This commit is contained in:
Reinaldo Neto 2023-02-27 18:03:14 -03:00 committed by GitHub
commit 8b2046623a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
430 changed files with 13747 additions and 7164 deletions

View File

@ -3,7 +3,7 @@ defaults: &defaults
macos: &macos
macos:
xcode: "13.3.0"
xcode: "14.2.0"
resource_class: large
bash-env: &bash-env
@ -51,14 +51,14 @@ save-gems-cache: &save-gems-cache
update-fastlane-ios: &update-fastlane-ios
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
echo "ruby-2.7.7" > ~/.ruby-version
bundle install
working_directory: ios
update-fastlane-android: &update-fastlane-android
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
echo "ruby-2.7.7" > ~/.ruby-version
bundle install
working_directory: android
@ -118,26 +118,26 @@ commands:
if [[ $CIRCLE_JOB == "android-build-official" ]]; then
echo -e "APPLICATION_ID=chat.rocket.android" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY_OFFICIAL" >> ./gradle.properties
echo $CHAT_ROCKET_ANDROID_STORE_FILE_BASE64_JKS | base64 --decode > ./app/$KEYSTORE_OFFICIAL
echo $KEYSTORE_OFFICIAL_BASE64 | base64 --decode > ./app/$KEYSTORE_OFFICIAL
echo -e "KEYSTORE=$KEYSTORE_OFFICIAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$CHAT_ROCKET_ANDROID_STORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$CHAT_ROCKET_ANDROID_KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$CHAT_ROCKET_ANDROID_KEY_PASSWORD" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_OFFICIAL_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_OFFICIAL_PASSWORD" >> ./gradle.properties
else
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
echo -e "KEYSTORE=$KEYSTORE" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_PASSWORD" >> ./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
fi
working_directory: android
- run:
name: Set Google Services
command: |
if [[ $KEYSTORE ]]; then
if [[ $GOOGLE_SERVICES_ANDROID ]]; then
echo $GOOGLE_SERVICES_ANDROID | base64 --decode > google-services.json
fi
working_directory: android/app
@ -151,7 +151,7 @@ commands:
if [[ $CIRCLE_JOB == "android-build-experimental" || "android-automatic-build-experimental" ]]; then
./gradlew bundleExperimentalPlayRelease
fi
if [[ ! $KEYSTORE ]]; then
if [[ ! $GOOGLE_SERVICES_ANDROID ]]; then
./gradlew assembleExperimentalPlayDebug
fi
working_directory: android
@ -200,8 +200,12 @@ commands:
- run:
name: Set Google Services
command: |
if [[ $KEYSTORE ]]; then
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
echo $GOOGLE_SERVICES_IOS | base64 --decode > GoogleService-Info.plist
else
echo $GOOGLE_SERVICES_IOS_EXPERIMENTAL | base64 --decode > GoogleService-Info.plist
fi
fi
working_directory: ios
- run:
@ -223,12 +227,12 @@ commands:
/usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./NotificationService/Info.plist
fi
if [[ $APP_STORE_CONNECT_API_BASE64 ]]; then
echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
if [[ $CIRCLE_JOB == "ios-build-official" ]]; then
bundle exec fastlane ios build_official
else
if [[ $KEYSTORE ]]; then
if [[ $APP_STORE_CONNECT_API_KEY_BASE64 ]]; then
bundle exec fastlane ios build_experimental
else
bundle exec fastlane ios build_fork
@ -318,7 +322,7 @@ commands:
- run:
name: Fastlane Tesflight Upload
command: |
echo $APP_STORE_CONNECT_API_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
echo $APP_STORE_CONNECT_API_KEY_BASE64 | base64 --decode > ./fastlane/app_store_connect_api_key.p8
bundle exec fastlane ios beta official:<< parameters.official >>
working_directory: ios
- save_cache: *save-gems-cache

View File

@ -2,7 +2,7 @@ module.exports = {
settings: {
'import/resolver': {
node: {
extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js']
extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js', '.ios.tsx', '.android.tsx']
}
}
},
@ -156,22 +156,6 @@ module.exports = {
__DEV__: true
},
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'],
extends: [
@ -253,6 +237,23 @@ module.exports = {
}
}
}
},
{
files: ['e2e/**'],
globals: {
by: true,
detox: true,
device: true,
element: true,
waitFor: true
},
rules: {
'import/no-extraneous-dependencies': 0,
'no-await-in-loop': 0,
'no-restricted-syntax': 0,
// TODO: remove this rule when update Detox to 20 and test if the namespace Detox is available
'no-undef': 1
}
}
]
};

View File

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

1
.gitignore vendored
View File

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

View File

@ -1 +1 @@
2.7.4
2.7.7

View File

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

View File

@ -3,7 +3,13 @@ import { Provider } from 'react-redux';
import { themes } from '../app/lib/constants';
import MessageContext from '../app/containers/message/Context';
import { selectServerRequest } from '../app/actions/server';
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 = [
Story => (
@ -15,7 +21,7 @@ export const decorators = [
username: 'diego.mello',
token: 'abc'
},
baseUrl: 'https://open.rocket.chat',
baseUrl,
onPress: () => {},
onLongPress: () => {},
reactionInit: () => {},

View File

@ -22,6 +22,7 @@ const getStories = () => {
require("../app/containers/Avatar/Avatar.stories.tsx"),
require("../app/containers/BackgroundContainer/index.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/List/List.stories.tsx"),
require("../app/containers/LoginServices/LoginServices.stories.tsx"),
@ -29,6 +30,7 @@ const getStories = () => {
require("../app/containers/markdown/new/NewMarkdown.stories.tsx"),
require("../app/containers/message/Components/CollapsibleQuote/CollapsibleQuote.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/RoomItem/RoomItem.stories.tsx"),
require("../app/containers/SearchBox/SearchBox.stories.tsx"),
@ -38,6 +40,7 @@ const getStories = () => {
require("../app/containers/UIKit/UiKitModal.stories.tsx"),
require("../app/containers/UnreadBadge/UnreadBadge.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/RoomView/LoadMore/LoadMore.stories.tsx"),
require("../app/views/ThreadMessagesView/Item.stories.tsx"),

View File

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

View File

@ -1,13 +1,13 @@
// 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\\":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 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 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 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 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 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 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 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 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 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 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!\\"]}]}"`;
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!\\"]}]}"`;

View File

@ -0,0 +1,11 @@
// 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
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 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 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\\"]}]}]}]"`;
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\\"]}]}]}]"`;

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
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\\":[\\"\\"]}]}]}]}"`;
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\\":[\\"\\"]}]}]}]}"`;

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

@ -0,0 +1,3 @@
// 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
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.30.0"
versionName "4.36.0"
vectorDrawables.useSupportLibrary = true
if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
@ -351,14 +351,12 @@ android {
dependencies {
implementation project(':@react-native-community_viewpager')
implementation "androidx.core:core-splashscreen:1.0.0"
playImplementation project(':react-native-notifications')
playImplementation 'com.google.firebase:firebase-core:16.0.0'
playImplementation project(':@react-native-firebase_app')
playImplementation project(':@react-native-firebase_analytics')
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"])
//noinspection GradleDynamicVersion

View File

@ -1,25 +0,0 @@
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);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

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

View File

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

View File

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

View File

@ -1,12 +0,0 @@
<?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,25 +8,15 @@
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style>
<style name="Share.Window" parent="android:Theme">
<item name="android:windowEnterAnimation">@null</item>
<item name="android:windowExitAnimation">@null</item>
<style name="ShareTheme" parent="AppTheme">
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
<style name="Theme.Share.Transparent" parent="android:Theme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@color/primary_dark</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 name="BootTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashBackground</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
<!-- https://github.com/facebook/react-native/blob/d1ab03235cb4b93304150878d2b9057ab45bba77/ReactAndroid/src/main/res/views/modal/values/themes.xml#L5 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -1,12 +0,0 @@
<?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,7 +6,6 @@
android:name="chat.rocket.reactnative.MainPlayApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:name"
>

View File

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

View File

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

View File

@ -13,8 +13,7 @@ buildscript {
buildToolsVersion = "31.0.0"
minSdkVersion = 23
compileSdkVersion = 31
// TODO: Fix "android:exported" issue and target 31 again
targetSdkVersion = 30
targetSdkVersion = 31
if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = "24.0.8215888"
@ -27,8 +26,6 @@ buildscript {
kotlinVersion = '1.6.10'
supportLibVersion = "28.0.0"
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 {
@ -69,9 +66,6 @@ allprojects {
url "$rootDir/../node_modules/detox/Detox-android"
}
maven {
url jitsi_url
}
mavenCentral {
content {
excludeGroup "com.facebook.react"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Action } from 'redux';
import { ERoomType } from '../definitions/ERoomType';
import { ERoomType, RoomType } from '../definitions';
import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED
@ -44,7 +44,24 @@ interface IUserTyping extends Action {
status: boolean;
}
export type TActionsRoom = TSubscribeRoom & TUnsubscribeRoom & ILeaveRoom & IDeleteRoom & IForwardRoom & IUserTyping;
export interface IRoomHistoryRequest extends Action {
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 {
return {
@ -99,3 +116,19 @@ export function userTyping(rid: string, status = true): IUserTyping {
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 CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
export const ACTION_SHEET_ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
duration: ACTION_SHEET_ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
@ -140,7 +140,7 @@ const ActionSheet = React.memo(
style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && onClose()}
// We need this to allow horizontal swipe gestures inside bottom sheet like in reaction picker
// We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker
enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet}
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
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

@ -0,0 +1,58 @@
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

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -0,0 +1,18 @@
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,75 +1,45 @@
import React from 'react';
import { FlatList, Text, TouchableOpacity } from 'react-native';
import { useWindowDimensions } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import { EMOJI_BUTTON_SIZE } from './styles';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
import { IEmoji } from '../../definitions/IEmoji';
import { PressableEmoji } from './PressableEmoji';
const EMOJI_SIZE = 50;
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
if (emoji && emoji.isCustom) {
return (
<CustomEmoji
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]}
emoji={emoji}
baseUrl={baseUrl}
/>
);
}
return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
{shortnameToUnicode(`:${emoji}:`)}
</Text>
);
};
class EmojiCategory extends React.Component<IEmojiCategory> {
renderItem(emoji: IEmoji) {
const { baseUrl, onEmojiSelected } = this.props;
return (
<TouchableOpacity
activeOpacity={0.7}
// @ts-ignore
key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}
>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
</TouchableOpacity>
);
interface IEmojiCategoryProps {
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
tabLabel?: string; // needed for react-native-scrollable-tab-view only
}
render() {
const { emojis, width } = this.props;
const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
const { width } = useWindowDimensions();
const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
if (!width) {
return null;
}
const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return (
<FlatList
contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes
// needed to update the numColumns when the width changes
key={`emoji-category-${width}`}
// @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
data={emojis}
extraData={this.props}
renderItem={({ item }) => this.renderItem(item)}
renderItem={renderItem}
numColumns={numColumns}
initialNumToRender={45}
removeClippedSubviews
contentContainerStyle={{ marginHorizontal }}
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
);
}
}
};
export default EmojiCategory;

View File

@ -0,0 +1,61 @@
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

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

View File

@ -0,0 +1,28 @@
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

@ -1,56 +1,42 @@
import React from 'react';
import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native';
import { Pressable, View } from 'react-native';
import styles from './styles';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import { useTheme } from '../../theme';
import { ITabBarProps } from './interfaces';
import { isIOS } from '../../lib/methods/helpers';
import { CustomIcon } from '../CustomIcon';
interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>;
theme: TSupportedThemes;
}
export default class TabBar extends React.Component<ITabBarProps> {
shouldComponentUpdate(nextProps: ITabBarProps) {
const { activeTab, theme } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
return false;
}
render() {
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props;
const TabBar = ({ activeTab, tabs, goToPage }: ITabBarProps): React.ReactElement => {
const { colors } = useTheme();
return (
<View style={styles.tabsContainer}>
{tabs?.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
<Pressable
key={tab}
onPress={() => {
if (goToPage) {
goToPage(i);
onPress={() => goToPage?.(i)}
testID={`emoji-picker-tab-${tab}`}
android_ripple={{ color: colors.bannerBackground }}
style={({ pressed }: { pressed: boolean }) => [
styles.tab,
{
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
}}
style={styles.tab}
testID={`reaction-picker-${tab}`}
]}
>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
<CustomIcon name={tab} size={24} color={activeTab === i ? colors.tintColor : colors.auxiliaryTintColor} />
<View
style={
activeTab === i
? [styles.activeTabLine, { backgroundColor: colors.tintColor }]
: [styles.tabLine, { backgroundColor: colors.borderColor }]
}
/>
</Pressable>
))}
</View>
);
}
}
};
export default TabBar;

View File

@ -1,155 +1,44 @@
import React, { Component } from 'react';
import { StyleProp, TextStyle, View } from 'react-native';
import React from 'react';
import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { ImageStyle } from 'react-native-fast-image';
import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
import Footer from './Footer';
import styles from './styles';
import categories from './categories';
import database from '../../lib/database';
import { emojisByCategory } from './emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import log from '../../lib/methods/helpers/log';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
import { categories, emojisByCategory } from '../../lib/constants';
import { useTheme } from '../../theme';
import { IEmoji, ICustomEmojis } from '../../definitions';
import { useAppSelector, useFrequentlyUsedEmoji } from '../../lib/hooks';
import { addFrequentlyUsed } from '../../lib/methods';
import { IEmojiPickerProps, EventTypes } from './interfaces';
interface IEmojiPickerProps {
isMessageContainsOnlyEmoji?: boolean;
getCustomEmoji?: TGetCustomEmoji;
baseUrl: string;
customEmojis: ICustomEmojis;
style?: StyleProp<ImageStyle>;
theme: TSupportedThemes;
onEmojiSelected: (emoji: string, shortname?: string) => void;
tabEmojiStyle?: StyleProp<TextStyle>;
}
const EmojiPicker = ({
onItemClicked,
isEmojiKeyboard = false,
searching = false,
searchedEmojis = []
}: IEmojiPickerProps): React.ReactElement | null => {
const { colors } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
interface IEmojiPickerState {
frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[];
customEmojis: any;
show: boolean;
width: number | null;
}
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
constructor(props: IEmojiPickerProps) {
super(props);
const customEmojis = Object.keys(props.customEmojis)
.filter(item => item === props.customEmojis[item].name)
const allCustomEmojis: ICustomEmojis = useAppSelector(
state => state.customEmojis,
() => true
);
const customEmojis = Object.keys(allCustomEmojis)
.filter(item => item === allCustomEmojis[item].name)
.map(item => ({
content: props.customEmojis[item].name,
extension: props.customEmojis[item].extension,
isCustom: true
name: allCustomEmojis[item].name,
extension: allCustomEmojis[item].extension
}));
this.state = {
frequentlyUsed: [],
customEmojis,
show: false,
width: null
};
}
async componentDidMount() {
await this.updateFrequentlyUsed();
this.setState({ show: true });
}
shouldComponentUpdate(nextProps: IEmojiPickerProps, nextState: IEmojiPickerState) {
const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
return true;
}
if (nextState.show !== show) {
return true;
}
if (nextState.width !== width) {
return true;
}
if (!dequal(nextState.frequentlyUsed, frequentlyUsed)) {
return true;
}
return false;
}
onEmojiSelected = (emoji: IEmoji) => {
try {
const { onEmojiSelected } = this.props;
if (emoji.isCustom) {
this._addFrequentlyUsed({
content: emoji.content,
extension: emoji.extension,
isCustom: true
});
onEmojiSelected(`:${emoji.content}:`);
} else {
const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname);
}
} catch (e) {
log(e);
}
const handleEmojiSelect = (emoji: IEmoji) => {
onItemClicked(EventTypes.EMOJI_PRESSED, emoji);
addFrequentlyUsed(emoji);
};
_addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: TFrequentlyUsedEmojiModel;
try {
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} 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 }, freqEmojiCollection.schema);
Object.assign(f, emoji);
f.count = 1;
});
}
});
});
updateFrequentlyUsed = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsed = frequentlyUsedOrdered.map(item => {
if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
}
return shortnameToUnicode(`${item.content}`);
});
this.setState({ frequentlyUsed });
};
onLayout = ({
nativeEvent: {
layout: { width }
}
}: any) => this.setState({ width });
renderCategory(category: keyof typeof emojisByCategory, i: number, label: string) {
const { frequentlyUsed, customEmojis, width } = this.state;
const { baseUrl } = this.props;
const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => {
let emojis = [];
if (i === 0) {
emojis = frequentlyUsed;
@ -158,49 +47,40 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
} else {
emojis = emojisByCategory[category];
}
return (
<EmojiCategory
emojis={emojis}
onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
style={styles.categoryContainer}
width={width}
baseUrl={baseUrl}
tabLabel={label}
/>
);
}
render() {
const { show, frequentlyUsed } = this.state;
const { tabEmojiStyle, theme } = this.props;
if (!show) {
if (!emojis.length) {
return null;
}
return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
};
if (!loaded) {
return null;
}
return (
<View onLayout={this.onLayout} style={{ flex: 1 }}>
<View style={styles.emojiPickerContainer}>
{searching ? (
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
) : (
<ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
renderTabBar={() => <TabBar />}
contentProps={{
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none'
}}
style={{ backgroundColor: themes[theme].focusedBackground }}
style={{ backgroundColor: colors.messageboxBackground }}
>
{categories.tabs.map((tab: any, i) =>
i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab
: this.renderCategory(tab.category, i, tab.tabLabel)
)}
{categories.tabs.map((tab: any, i) => renderCategory(tab.category, i, tab.tabLabel))}
</ScrollableTabView>
)}
{isEmojiKeyboard && (
<Footer
onSearchPressed={() => onItemClicked(EventTypes.SEARCH_PRESSED)}
onBackspacePressed={() => onItemClicked(EventTypes.BACKSPACE_PRESSED)}
/>
)}
</View>
);
}
}
};
const mapStateToProps = (state: IApplicationState) => ({
customEmojis: state.customEmojis,
baseUrl: state.share.server.server || state.server.server
});
export default connect(mapStateToProps)(EmojiPicker);
export default EmojiPicker;

View File

@ -0,0 +1,26 @@
import { TIconsName } from '../CustomIcon';
import { IEmoji } from '../../definitions';
export enum EventTypes {
EMOJI_PRESSED = 'emojiPressed',
BACKSPACE_PRESSED = 'backspacePressed',
SEARCH_PRESSED = 'searchPressed'
}
export interface IEmojiPickerProps {
onItemClicked: (event: EventTypes, emoji?: IEmoji) => void;
isEmojiKeyboard?: boolean;
searching?: boolean;
searchedEmojis?: IEmoji[];
}
export interface IFooterProps {
onBackspacePressed: () => void;
onSearchPressed: () => void;
}
export interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: TIconsName[];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +1,29 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed } from '../../lib/methods';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles';
import { TAnyMessageModel, TFrequentlyUsedEmojiModel } from '../../definitions';
import { IEmoji, TAnyMessageModel } from '../../definitions';
import Touch from '../Touch';
type TItem = TFrequentlyUsedEmojiModel | string;
export interface IHeader {
handleReaction: (emoji: TItem, message: TAnyMessageModel) => void;
server: string;
handleReaction: (emoji: IEmoji | null, message: TAnyMessageModel) => void;
message: TAnyMessageModel;
isMasterDetail: boolean;
}
type TOnReaction = ({ emoji }: { emoji: TItem }) => void;
type TOnReaction = ({ emoji }: { emoji?: IEmoji }) => void;
interface THeaderItem {
item: TItem;
item: IEmoji;
onReaction: TOnReaction;
server: string;
theme: TSupportedThemes;
}
@ -64,30 +61,19 @@ const styles = StyleSheet.create({
}
});
const keyExtractor = (item: TItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
return (emojiModel.id ? emojiModel.content : item) as string;
};
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
const emoji = (emojiModel.id ? emojiModel.content : item) as string;
return (
const HeaderItem = ({ item, onReaction, theme }: THeaderItem) => (
<Touch
testID={`message-actions-emoji-${emoji}`}
onPress={() => onReaction({ emoji: `:${emoji}:` })}
testID={`message-actions-emoji-${item}`}
onPress={() => onReaction({ emoji: item })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
>
{emojiModel?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={emojiModel} baseUrl={server} />
{typeof item === 'string' ? (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item}:`)}</Text>
) : (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
<CustomEmoji style={styles.customEmoji} emoji={item} />
)}
</Touch>
);
};
const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
<Touch
@ -99,49 +85,37 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
</Touch>
);
const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => {
const [items, setItems] = useState<TItem[]>([]);
const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => {
const { width, height } = useDimensions();
const { theme } = useTheme();
// 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 { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
const quantity = Math.trunc(size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1);
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis);
} catch {
// Do nothing
const onReaction: TOnReaction = ({ emoji }) => {
handleReaction(emoji || null, message);
if (emoji) {
addFrequentlyUsed(emoji);
}
};
useEffect(() => {
setEmojis();
}, []);
const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = ({ item }: { item: TItem }) => (
<HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
);
const renderItem = ({ item }: { item: IEmoji }) => <HeaderItem item={item} onReaction={onReaction} theme={theme} />;
const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
if (!loaded) {
return null;
}
return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList
data={items}
data={frequentlyUsed.slice(0, quantity)}
renderItem={renderItem}
ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={keyExtractor}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
showsHorizontalScrollIndicator={false}
scrollEnabled={false}
horizontal

View File

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

View File

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

View File

@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { CustomIcon } from '../CustomIcon';
import { IEmoji } from '../../definitions';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import { addFrequentlyUsed, searchEmojis } from '../../lib/methods';
import { useDebounce } from '../../lib/methods/helpers';
import sharedStyles from '../../views/Styles';
import { PressableEmoji } from '../EmojiPicker/PressableEmoji';
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 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 {
openEmoji: () => void;
closeEmoji: () => void;
onEmojiSelected: (emoji: IEmoji) => void;
}
const EmojiSearchBar = ({ openEmoji, closeEmoji, onEmojiSelected }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const { frequentlyUsed } = useFrequentlyUsedEmoji(true);
const [emojis, setEmojis] = useState<IEmoji[]>([]);
const handleTextChange = useDebounce(async (text: string) => {
logEvent(events.MB_SB_EMOJI_SEARCH);
setSearchText(text);
const result = await searchEmojis(text);
setEmojis(result);
}, 300);
const handleEmojiSelected = (emoji: IEmoji) => {
logEvent(events.MB_SB_EMOJI_SELECTED);
onEmojiSelected(emoji);
addFrequentlyUsed(emoji);
};
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={handleEmojiSelected} />;
return (
<View style={[styles.container, { borderTopColor: colors.borderColor, backgroundColor: colors.messageboxBackground }]}>
<FlatList
horizontal
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>
<View style={styles.inputContainer}>
<EmojiSearch onBlur={closeEmoji} onChangeText={handleTextChange} />
</View>
</View>
</View>
);
};
export default EmojiSearchBar;

View File

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

View File

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

View File

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

View File

@ -4,5 +4,6 @@ export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
export const MENTIONS_TRACKING_TYPE_CANNED = '!';
export const MENTIONS_COUNT_TO_DISPLAY = 4;
export const MAX_EMOJIS_TO_DISPLAY = 20;
export const TIMEOUT_CLOSE_EMOJI = 300;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Alert, Keyboard, NativeModules, Text, View } from 'react-native';
import { Alert, Keyboard, NativeModules, Text, View, BackHandler } from 'react-native';
import { connect } from 'react-redux';
import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker';
@ -13,12 +13,11 @@ import { TextInput, IThemedTextInput } from '../TextInput';
import { userTyping as userTypingAction } from '../../actions/room';
import styles from './styles';
import database from '../../lib/database';
import { emojis } from '../EmojiPicker/emojis';
import log, { events, logEvent } from '../../lib/methods/helpers/log';
import RecordAudio from './RecordAudio';
import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview';
import { themes } from '../../lib/constants';
import { themes, emojis } from '../../lib/constants';
import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons';
import { canUploadFile } from '../../lib/methods/helpers/media';
@ -51,14 +50,18 @@ import {
TGetCustomEmoji,
TSubscriptionModel,
TThreadModel,
IMessage
IMessage,
IEmoji
} from '../../definitions';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
import { hasPermission, debounce, isAndroid, isIOS, isTablet } from '../../lib/methods/helpers';
import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersion } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
import { TSupportedThemes } from '../../theme';
import { ChatsStackParamList } from '../../stacks/types';
import { EventTypes } from '../EmojiPicker/interfaces';
import EmojiSearchbar from './EmojiSearchbar';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
require('./EmojiKeyboard');
@ -111,8 +114,8 @@ export interface IMessageBoxProps extends IBaseScreen<ChatsStackParamList & Mast
isActionsEnabled: boolean;
usedCannedResponse: string;
uploadFilePermission: string[];
serverVersion: string;
goToCannedResponses: () => void | null;
serverVersion: string;
}
interface IMessageBoxState {
@ -129,6 +132,7 @@ interface IMessageBoxState {
tshow: boolean;
mentionLoading: boolean;
permissionToUpload: boolean;
showEmojiSearchbar: boolean;
}
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -181,9 +185,10 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
commandPreview: [],
showCommandPreview: false,
command: {},
tshow: false,
tshow: this.sendThreadToChannel,
mentionLoading: false,
permissionToUpload: true
permissionToUpload: true,
showEmojiSearchbar: false
};
this.text = '';
this.selection = { start: 0, end: 0 };
@ -209,11 +214,30 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
...videoPickerConfig,
...libPickerLabels
};
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}
get sendThreadToChannel() {
const { user, serverVersion, tmid } = this.props;
if (tmid && compareServerVersion(serverVersion, 'lowerThan', '5.0.0')) {
return false;
}
if (tmid && user.alsoSendThreadToChannel === 'default') {
return false;
}
if (user.alsoSendThreadToChannel === 'always') {
return true;
}
if (user.alsoSendThreadToChannel === 'never') {
return false;
}
return true;
}
async componentDidMount() {
const db = database.active;
const { rid, tmid, navigation, sharing, usedCannedResponse, isMasterDetail } = this.props;
const { rid, tmid, navigation, sharing, usedCannedResponse } = this.props;
let msg;
try {
const threadsCollection = db.get('threads');
@ -248,7 +272,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
}
if (isMasterDetail && usedCannedResponse) {
if (usedCannedResponse) {
this.onChangeText(usedCannedResponse);
}
@ -278,7 +302,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (usedCannedResponse !== nextProps.usedCannedResponse) {
this.onChangeText(nextProps.usedCannedResponse ?? '');
}
if (sharing) {
if (sharing && !replying) {
this.setInput(nextProps.message.msg ?? '');
return;
}
@ -309,7 +333,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tshow,
mentionLoading,
trackingType,
permissionToUpload
permissionToUpload,
showEmojiSearchbar
} = this.state;
const {
@ -329,6 +354,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true;
}
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
return true;
}
if (!isFocused()) {
return false;
}
@ -381,7 +409,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}
componentDidUpdate(prevProps: IMessageBoxProps) {
const { uploadFilePermission, goToCannedResponses } = this.props;
const { uploadFilePermission, goToCannedResponses, replyWithMention, threadsEnabled } = this.props;
if (prevProps.replyWithMention !== replyWithMention) {
if (threadsEnabled && replyWithMention) {
this.setState({ tshow: this.sendThreadToChannel });
}
}
if (!dequal(prevProps.uploadFilePermission, uploadFilePermission) || prevProps.goToCannedResponses !== goToCannedResponses) {
this.setOptions();
}
@ -416,6 +449,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (isTablet) {
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
}
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}
setOptions = async () => {
@ -497,7 +531,10 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}, 100);
onKeyboardResigned = () => {
const { showEmojiSearchbar } = this.state;
if (!showEmojiSearchbar) {
this.closeEmoji();
}
};
onPressMention = (item: any) => {
@ -555,18 +592,50 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}
};
onEmojiSelected = (keyboardId: string, params: { emoji: string }) => {
onKeyboardItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: IEmoji }) => {
const { eventType, emoji } = params;
const { text } = this;
const { emoji } = params;
let newText = '';
// if messagebox has an active cursor
const { start, end } = this.selection;
const cursor = Math.max(start, end);
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
const newCursor = cursor + emoji.length;
let newCursor;
switch (eventType) {
case EventTypes.BACKSPACE_PRESSED:
logEvent(events.MB_BACKSPACE);
const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/;
let charsToRemove = 1;
const lastEmoji = text.substr(cursor > 0 ? cursor - 2 : text.length - 2, cursor > 0 ? cursor : text.length);
// Check if last character is an emoji
if (emojiRegex.test(lastEmoji)) charsToRemove = 2;
newText =
text.substr(0, (cursor > 0 ? cursor : text.length) - charsToRemove) + text.substr(cursor > 0 ? cursor : text.length);
newCursor = cursor - charsToRemove;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(newText !== '');
break;
case EventTypes.EMOJI_PRESSED:
logEvent(events.MB_EMOJI_SELECTED);
let emojiText = '';
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.setShowSend(true);
break;
case EventTypes.SEARCH_PRESSED:
logEvent(events.MB_EMOJI_SEARCH_PRESSED);
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: true });
break;
default:
// Do nothing
}
};
getPermalink = async (message: any) => {
@ -589,7 +658,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
};
getUsers = debounce(async (keyword: any) => {
let res = await search({ text: keyword, filterRooms: false, filterUsers: true });
const { rid } = this.props;
let res = await search({ text: keyword, filterRooms: false, filterUsers: true, rid });
res = [...this.getFixedMentions(keyword), ...res];
this.setState({ mentions: res, mentionLoading: false });
}, 300);
@ -599,16 +669,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.setState({ mentions: res, mentionLoading: false });
}, 300);
getEmojis = debounce(async (keyword: any) => {
const db = database.active;
const customEmojisCollection = db.get('custom_emojis');
getCustomEmojis = async (keyword: any, count: number) => {
const likeString = sanitizeLikeString(keyword);
const whereClause = [];
if (likeString) {
whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
}
let customEmojis = await customEmojisCollection.query(...whereClause).fetch();
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY);
const db = database.active;
const customEmojisCollection = db.get('custom_emojis');
const customEmojis = await (await customEmojisCollection.query(...whereClause).fetch()).slice(0, count);
return customEmojis;
};
getEmojis = debounce(async (keyword: any) => {
const customEmojis = await this.getCustomEmojis(keyword, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [], mentionLoading: false });
@ -687,9 +761,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
};
clearInput = () => {
const { tshow } = this.state;
const { user, serverVersion } = this.props;
this.setInput('');
this.setShowSend(false);
if (compareServerVersion(serverVersion, 'lowerThan', '5.0.0') || (tshow && user.alsoSendThreadToChannel === 'default')) {
this.setState({ tshow: false });
}
};
canUploadFile = (file: any) => {
@ -779,14 +857,21 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
};
openShareView = (attachments: any) => {
const { message, replyCancel, replyWithMention } = this.props;
const { message, replyCancel, replyWithMention, replying } = this.props;
// Start a thread with an attachment
let value: TThreadModel | IMessage = this.thread;
if (replyWithMention) {
value = message;
replyCancel();
}
Navigation.navigate('ShareView', { room: this.room, thread: value, attachments });
Navigation.navigate('ShareView', {
room: this.room,
thread: value,
attachments,
replying,
replyingMessage: message,
closeReply: replyCancel
});
};
createDiscussion = () => {
@ -855,7 +940,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
openEmoji = () => {
logEvent(events.ROOM_OPEN_EMOJI);
this.setState({ showEmojiKeyboard: true });
this.setState({ showEmojiKeyboard: true, showEmojiSearchbar: false });
this.stopTrackingMention();
};
recordingCallback = (recording: any) => {
@ -877,7 +963,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
};
closeEmoji = () => {
this.setState({ showEmojiKeyboard: false });
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: false });
};
closeEmojiKeyboardAndFocus = () => {
logEvent(events.ROOM_CLOSE_EMOJI);
this.closeEmoji();
this.focus();
};
closeEmojiAndAction = (action?: Function, params?: any) => {
@ -900,7 +992,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.clearInput();
this.debouncedOnChangeText.stop();
this.closeEmoji();
this.closeEmojiKeyboardAndFocus();
this.stopTrackingMention();
this.handleTyping(false);
if (message.trim() === '' && !showSend) {
@ -957,7 +1049,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
// Legacy reply or quote (quote is a reply without mention)
} else {
const { user, roomType } = this.props;
const msg = await this.formatReplyMessage(replyingMessage, message);
onSubmit(msg);
}
replyCancel();
// Normal message
} else {
// @ts-ignore
onSubmit(message, undefined, tmid ? tshow : false);
}
};
formatReplyMessage = async (replyingMessage: IMessage, message = '') => {
const { user, roomType, replyWithMention, serverVersion } = this.props;
const permalink = await this.getPermalink(replyingMessage);
let msg = `[ ](${permalink}) `;
@ -966,16 +1071,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
msg += `@${replyingMessage?.u?.username} `;
}
msg = `${msg} ${message}`;
onSubmit(msg);
}
replyCancel();
// Normal message
} else {
// @ts-ignore
onSubmit(message, undefined, tshow);
}
const connectionString = compareServerVersion(serverVersion, 'lowerThan', '5.0.0') ? ' ' : '\n';
return `${msg}${connectionString}${message}`;
};
updateMentions = (keyword: any, type: string) => {
@ -1044,7 +1141,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
onPress={this.onPressSendToChannel}
testID='messagebox-send-to-channel'
>
<CustomIcon name={tshow ? 'checkbox-checked' : 'checkbox-unchecked'} size={24} color={themes[theme].auxiliaryText} />
<CustomIcon
testID={tshow ? 'send-to-channel-checked' : 'send-to-channel-unchecked'}
name={tshow ? 'checkbox-checked' : 'checkbox-unchecked'}
size={24}
color={themes[theme].auxiliaryText}
/>
<Text style={[styles.sendToChannelText, { color: themes[theme].auxiliaryText }]}>
{I18n.t('Messagebox_Send_to_channel')}
</Text>
@ -1052,10 +1154,34 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
);
};
renderEmojiSearchbar = () => {
const { showEmojiSearchbar } = this.state;
return showEmojiSearchbar ? (
<EmojiSearchbar
openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji}
onEmojiSelected={(emoji: IEmoji) => {
this.onKeyboardItemSelected('EmojiKeyboard', { eventType: EventTypes.EMOJI_PRESSED, emoji });
}}
/>
) : null;
};
handleBackPress = () => {
const { showEmojiSearchbar } = this.state;
if (showEmojiSearchbar) {
this.setState({ showEmojiSearchbar: false });
return true;
}
return false;
};
renderContent = () => {
const {
recording,
showEmojiKeyboard,
showEmojiSearchbar,
showSend,
mentions,
trackingType,
@ -1118,11 +1244,11 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const textInputAndButtons = !recording ? (
<>
<LeftButtons
showEmojiKeyboard={showEmojiKeyboard}
showEmojiKeyboard={showEmojiKeyboard || showEmojiSearchbar}
editing={editing}
editCancel={this.editCancel}
openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji}
closeEmoji={this.closeEmojiKeyboardAndFocus}
/>
<TextInput
ref={component => (this.component = component)}
@ -1166,6 +1292,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
{recordAudio}
</View>
{this.renderSendToChannel()}
{this.renderEmojiSearchbar()}
</View>
{children}
</>
@ -1193,7 +1320,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
kbInitialProps={{ theme }}
onKeyboardResigned={this.onKeyboardResigned}
onItemSelected={this.onEmojiSelected}
onItemSelected={this.onKeyboardItemSelected}
trackInteractive
requiresSameParentToManageScrollView
addBottomView
@ -1213,7 +1340,8 @@ const mapStateToProps = (state: IApplicationState) => ({
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList,
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize,
Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled,
uploadFilePermission: state.permissions['mobile-upload-file']
uploadFilePermission: state.permissions['mobile-upload-file'],
serverVersion: state.server.version
});
const dispatchToProps = {

View File

@ -105,7 +105,7 @@ export default StyleSheet.create({
},
emojiKeyboardContainer: {
flex: 1,
borderTopWidth: StyleSheet.hairlineWidth
borderTopWidth: 1
},
slash: {
height: 30,
@ -113,7 +113,7 @@ export default StyleSheet.create({
padding: 5,
paddingHorizontal: 12,
marginHorizontal: 10,
borderRadius: 2
borderRadius: 4
},
commandPreviewImage: {
justifyContent: 'center',

View File

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

View File

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

View File

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

View File

@ -0,0 +1,16 @@
import React from 'react';
import { RadioButton as RadioButtonUiLib } from 'react-native-ui-lib';
import { useTheme } from '../../theme';
export const RadioButton = ({ check, testID, size }: { check: boolean; testID?: string; size?: number }): React.ReactElement => {
const { colors } = useTheme();
return (
<RadioButtonUiLib
testID={testID}
selected={check}
size={size || 20}
color={check ? colors.tintActive : colors.auxiliaryTintColor}
/>
);
};

View File

@ -1,145 +0,0 @@
import React from 'react';
import { StyleSheet, Text, Pressable, View, ScrollView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { FlatList } from 'react-native-gesture-handler';
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 }
});
interface IReactionsListBase {
baseUrl: string;
getCustomEmoji: TGetCustomEmoji;
}
interface IReactionsListProps extends IReactionsListBase {
reactions?: IReaction[];
width: number;
}
interface ITabBarItem extends IReactionsListBase {
tab: IReaction;
index: number;
goToPage?: (index: number) => void;
}
interface IReactionsTabBar extends IReactionsListBase {
activeTab?: number;
tabs?: IReaction[];
goToPage?: (index: number) => void;
width: number;
}
const TabBarItem = ({ tab, index, goToPage, baseUrl, getCustomEmoji }: ITabBarItem) => {
const { colors } = useTheme();
return (
<Pressable
key={tab.emoji}
onPress={() => {
goToPage?.(index);
}}
style={({ pressed }: { pressed: boolean }) => ({
opacity: pressed ? 0.7 : 1
})}
>
<View style={styles.tabBarItem}>
<Emoji
content={tab.emoji}
standardEmojiStyle={styles.standardEmojiStyle}
customEmojiStyle={styles.customEmojiStyle}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
/>
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>
</View>
</Pressable>
);
};
const ReactionsTabBar = ({ tabs, activeTab, goToPage, 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} index={index} goToPage={goToPage} 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 => {
// 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);
return (
<View style={styles.reactionsListContainer}>
<ScrollableTabView renderTabBar={() => <ReactionsTabBar baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} width={width} />}>
{sortedReactions?.map(reaction => (
<UsersList tabLabel={reaction} key={reaction.emoji} />
))}
</ScrollableTabView>
</View>
);
};
export default ReactionsList;

View File

@ -0,0 +1,75 @@
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

@ -0,0 +1,71 @@
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'
};

View File

@ -0,0 +1,79 @@
import React from 'react';
import { fireEvent, render, within } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import ReactionsList from '.';
import { mockedStore } from '../../reducers/mockedStore';
const getCustomEmoji = jest.fn();
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']
}
];
const Render = () => (
<Provider store={mockedStore}>
<ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} />
</Provider>
);
describe('ReactionsList', () => {
test('should render Reactions List', async () => {
const { findByTestId } = render(<Render />);
const ReactionsListView = await findByTestId('reactionsList');
expect(ReactionsListView).toBeTruthy();
});
test('should render tab bar', async () => {
const { findByTestId } = render(<Render />);
const AllTab = await findByTestId('reactionsTabBar');
expect(AllTab).toBeTruthy();
});
test('should render All tab', async () => {
const { findByTestId } = render(<Render />);
const AllTab = await findByTestId('reactionsListAllTab');
expect(AllTab).toBeTruthy();
});
test('correct tab on clicking tab item', async () => {
const { findByTestId } = render(<Render />);
const tab = await findByTestId(`tabBarItem-${reactions[0].emoji}`);
fireEvent.press(tab);
const usersList = await findByTestId(`usersList-${reactions[0].emoji}`);
expect(usersList).toBeTruthy();
const emojiName = await within(usersList).getByTestId(`usersListEmojiName`);
expect(emojiName.props.children).toEqual(reactions[0].emoji);
});
test('should render correct number of reactions', async () => {
const { findByTestId } = render(<Render />);
const tab = await findByTestId(`tabBarItem-${reactions[0].emoji}`);
fireEvent.press(tab);
const usersList = await findByTestId(`usersList-${reactions[0].emoji}`);
const allReactions = await within(usersList).getAllByTestId('userItem');
expect(allReactions).toHaveLength(reactions[0].usernames.length);
});
});

View File

@ -0,0 +1,86 @@
import React from 'react';
import { Text, Pressable, View, ScrollView } 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, { MIN_TAB_WIDTH } from './styles';
import { useDimensions, useOrientation } from '../../dimensions';
interface ITabBarItem {
getCustomEmoji: TGetCustomEmoji;
tab: IReaction;
index: number;
goToPage?: (index: number) => void;
}
interface IReactionsTabBar {
getCustomEmoji: TGetCustomEmoji;
activeTab?: number;
tabs?: IReaction[];
goToPage?: (index: number) => void;
}
const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
const { colors } = useTheme();
return (
<Pressable
key={tab.emoji}
onPress={() => {
goToPage?.(index);
}}
style={({ pressed }: { pressed: boolean }) => ({
opacity: pressed ? 0.7 : 1
})}
testID={`tabBarItem-${tab.emoji}`}
>
<View style={styles.tabBarItem}>
{tab._id === 'All' ? (
<Text style={[styles.allTabItem, { color: colors.auxiliaryTintColor }]}>{I18n.t('All')}</Text>
) : (
<>
<Emoji
content={tab.emoji}
standardEmojiStyle={styles.standardEmojiStyle}
customEmojiStyle={styles.customEmojiStyle}
getCustomEmoji={getCustomEmoji}
/>
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>
</>
)}
</View>
</Pressable>
);
};
const ReactionsTabBar = ({ tabs, activeTab, goToPage, getCustomEmoji }: IReactionsTabBar): React.ReactElement => {
const { isLandscape } = useOrientation();
const { width } = useDimensions();
const reactionsListWidth = isLandscape ? width / 2 : width;
const tabWidth = tabs && Math.max(reactionsListWidth / tabs.length, MIN_TAB_WIDTH);
const { colors } = useTheme();
return (
<View testID='reactionsTabBar'>
<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
}}
key={tab.emoji}
>
<TabBarItem tab={tab} index={index} goToPage={goToPage} getCustomEmoji={getCustomEmoji} />
</View>
);
})}
</ScrollView>
</View>
);
};
export default ReactionsTabBar;

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Text, View, FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import { useTheme } from '../../theme';
import { IReaction, IApplicationState } from '../../definitions';
import Avatar from '../Avatar';
import styles from './styles';
const UsersList = ({ tabLabel }: { tabLabel: IReaction }): React.ReactElement => {
const { colors } = useTheme();
const useRealName = useSelector((state: IApplicationState) => state.settings.UI_Use_Real_Name);
const { emoji, usernames, names } = tabLabel;
const users =
names?.length > 0
? usernames.map((username, index) => ({ username, name: names[index] }))
: usernames.map(username => ({ username, name: '' }));
return (
<FlatList
data={users}
contentContainerStyle={styles.listContainer}
ListHeaderComponent={
<View style={styles.emojiNameContainer}>
<Text style={[styles.emojiName, { color: colors.auxiliaryText }]} testID='usersListEmojiName'>
{emoji}
</Text>
</View>
}
renderItem={({ item }) => (
<View style={styles.listItemContainer} testID='userItem'>
<Avatar text={item.username} size={36} />
<View style={styles.textContainer}>
<Text style={[styles.usernameText, { color: colors.bodyText }]} numberOfLines={1}>
{useRealName && item.name ? item.name : item.username}
</Text>
</View>
</View>
)}
keyExtractor={item => item.username}
testID={`usersList-${emoji}`}
/>
);
};
export default UsersList;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IReaction } from '../../definitions';
import I18n from '../../i18n';
import styles from './styles';
import AllTab from './AllTab';
import UsersList from './UsersList';
import ReactionsTabBar from './ReactionsTabBar';
interface IReactionsListProps {
getCustomEmoji: TGetCustomEmoji;
reactions?: IReaction[];
}
const ReactionsList = ({ reactions, getCustomEmoji }: IReactionsListProps): React.ReactElement => {
// 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 allTabLabel = { emoji: I18n.t('All'), usernames: [], names: [], _id: 'All' };
return (
<View style={styles.container} testID='reactionsList'>
<ScrollableTabView renderTabBar={() => <ReactionsTabBar getCustomEmoji={getCustomEmoji} />}>
<AllTab tabLabel={allTabLabel} reactions={sortedReactions} getCustomEmoji={getCustomEmoji} />
{sortedReactions?.map(reaction => (
<UsersList tabLabel={reaction} key={reaction.emoji} />
))}
</ScrollableTabView>
</View>
);
};
export default ReactionsList;

View File

@ -0,0 +1,88 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles';
export const MIN_TAB_WIDTH = 70;
export default StyleSheet.create({
container: {
flex: 1
},
allTabContainer: {
flex: 1
},
tabBarItem: {
paddingBottom: 4,
height: 44,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
},
listContainer: {
marginHorizontal: 12,
marginVertical: 8,
paddingBottom: 30
},
reactionCount: {
marginLeft: 4,
...sharedStyles.textSemibold
},
emojiNameContainer: {
marginVertical: 8
},
emojiName: {
fontSize: 14,
...sharedStyles.textMedium
},
listItemContainer: {
marginVertical: 6,
flexDirection: 'row',
alignItems: 'center'
},
textContainer: {
flex: 1,
marginLeft: 8,
justifyContent: 'center'
},
usernameText: {
fontSize: 16,
...sharedStyles.textSemibold
},
standardEmojiStyle: {
fontSize: 20,
width: 24,
height: 24,
textAlign: 'center',
color: '#fff'
},
customEmojiStyle: {
width: 24,
height: 24
},
allTabItem: {
fontSize: 16,
...sharedStyles.textSemibold
},
allTabStandardEmojiStyle: {
fontSize: 30,
width: 36,
textAlign: 'center',
color: '#fff'
},
allTabCustomEmojiStyle: {
width: 36,
height: 36
},
allListItemContainer: {
flexDirection: 'row',
alignItems: 'center'
},
allListNPeopleReacted: {
fontSize: 14,
...sharedStyles.textMedium
},
allListWhoReacted: {
fontSize: 14,
...sharedStyles.textRegular
}
});

View File

@ -7,6 +7,7 @@ import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon';
import { TUserStatus, IOmnichannelSource } from '../../definitions';
import { useTheme } from '../../theme';
import { useAppSelector } from '../../lib/hooks';
const HIT_SLOP = {
top: 5,
@ -141,8 +142,9 @@ const Header = React.memo(
const { colors } = useTheme();
const portrait = height > width;
let scale = 1;
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
if (!portrait && !tmid) {
if (!portrait && !tmid && !isMasterDetail) {
if (usersTyping.length > 0 || subtitle) {
scale = 0.8;
}

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