Compare commits

..

1 Commits

Author SHA1 Message Date
snyk-bot bd9aec1ada
fix: package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UAPARSERJS-3244450
2023-01-24 16:40:16 +00:00
176 changed files with 5842 additions and 3820 deletions

View File

@ -1,9 +1,6 @@
defaults: &defaults
working_directory: ~/repo
orbs:
android: circleci/android@2.1.2
macos: &macos
macos:
xcode: "14.2.0"
@ -330,14 +327,6 @@ commands:
working_directory: ios
- save_cache: *save-gems-cache
create-e2e-account-file:
description: "Create e2e account file"
steps:
- run:
command: |
echo $E2E_ACCOUNT | base64 --decode > ./e2e_account.ts
working_directory: e2e
version: 2.1
# EXECUTORS
@ -449,94 +438,6 @@ jobs:
- upload-to-google-play-beta:
official: true
e2e-build-android:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
tag: 2022.12.1
environment:
<<: *android-env
steps:
- checkout
- restore_cache: *restore-npm-cache-linux
- run: *install-npm-modules
- save_cache: *save-npm-cache-linux
- restore_cache: *restore-gradle-cache
- run:
name: Configure Gradle
command: |
echo -e "" > ./gradle.properties
# echo -e "android.enableAapt2=false" >> ./gradle.properties
echo -e "android.useAndroidX=true" >> ./gradle.properties
echo -e "android.enableJetifier=true" >> ./gradle.properties
echo -e "newArchEnabled=false" >> ./gradle.properties
echo -e "FLIPPER_VERSION=0.125.0" >> ./gradle.properties
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
echo -e "APPLICATION_ID=chat.rocket.reactnative" >> ./gradle.properties
echo -e "BugsnagAPIKey=$BUGSNAG_KEY" >> ./gradle.properties
echo $KEYSTORE_EXPERIMENTAL_BASE64 | base64 --decode > ./app/$KEYSTORE_EXPERIMENTAL
echo -e "KEYSTORE=$KEYSTORE_EXPERIMENTAL" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEYSTORE_EXPERIMENTAL_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_EXPERIMENTAL_PASSWORD" >> ./gradle.properties
working_directory: android
- run:
name: Build Android
command: |
echo "RUNNING_E2E_TESTS=true" > ./.env
yarn e2e:android-build
- save_cache: *save-gradle-cache
- store_artifacts:
path: android/app/build/outputs/apk/experimentalPlay/release/app-experimental-play-release.apk
- store_artifacts:
path: android/app/build/outputs/apk/androidTest/experimentalPlay/release/app-experimental-play-release-androidTest.apk
- persist_to_workspace:
root: /home/circleci/repo
paths:
- android/app/build/outputs/apk/
e2e-test-android:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
tag: 2022.12.1
parallelism: 4
steps:
- checkout
- attach_workspace:
at: /home/circleci/repo
- restore_cache: *restore-npm-cache-linux
- run: *install-npm-modules
- save_cache: *save-npm-cache-linux
- run: mkdir ~/junit
- create-e2e-account-file
- android/create-avd:
avd-name: Pixel_API_31_AOSP
install: true
system-image: system-images;android-31;default;x86_64
- run:
name: Setup emulator
command: |
echo "hw.lcd.density = 440" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
echo "hw.lcd.height = 2280" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
echo "hw.lcd.width = 1080" >> ~/.android/avd/Pixel_API_31_AOSP.avd/config.ini
- run:
name: Run Detox Tests
command: |
TEST=$(circleci tests glob "e2e/tests/**/*.ts" | circleci tests split --split-by=timings)
yarn e2e:android-test $TEST
- store_artifacts:
path: artifacts
- run:
command: cp junit.xml ~/junit/
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
# iOS builds
ios-build-experimental:
executor: mac-env
@ -560,89 +461,11 @@ jobs:
- upload-to-testflight:
official: true
e2e-build-ios:
executor: mac-env
steps:
- checkout
- restore_cache: *restore-gems-cache
- restore_cache: *restore-npm-cache-mac
- run: *install-npm-modules
- run: *update-fastlane-ios
- save_cache: *save-npm-cache-mac
- save_cache: *save-gems-cache
- manage-pods
- run:
name: Configure Detox
command: |
brew tap wix/brew
brew install applesimutils
- run:
name: Build
command: |
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/RocketChatRN/Info.plist
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/ShareRocketChatRN/Info.plist
yarn detox clean-framework-cache && yarn detox build-framework-cache
echo "RUNNING_E2E_TESTS=true" > ./.env
yarn e2e:ios-build
- persist_to_workspace:
root: /Users/distiller/project
paths:
- ios/build/Build/Products/Release-iphonesimulator/Rocket.Chat Experimental.app
e2e-test-ios:
executor: mac-env
parallelism: 5
steps:
- checkout
- attach_workspace:
at: /Users/distiller/project
- restore_cache: *restore-npm-cache-mac
- run: *install-npm-modules
- save_cache: *save-npm-cache-mac
- run: mkdir ~/junit
- run:
name: Configure Detox
command: |
brew tap wix/brew
brew install applesimutils
yarn detox clean-framework-cache && yarn detox build-framework-cache
- create-e2e-account-file
- run:
name: Run tests
command: |
TEST=$(circleci tests glob "e2e/tests/**/*.ts" | circleci tests split --split-by=timings)
yarn e2e:ios-test $TEST
- store_artifacts:
path: artifacts
- run:
command: cp junit.xml ~/junit/
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
workflows:
build-and-test:
jobs:
- lint-testunit
# E2E tests
- e2e-hold:
type: approval
- e2e-build-ios:
requires:
- e2e-hold
- e2e-test-ios:
requires:
- e2e-build-ios
- e2e-build-android:
requires:
- e2e-hold
- e2e-test-android:
requires:
- e2e-build-android
# iOS Experimental
- ios-hold-build-experimental:
type: approval

View File

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

2
.env
View File

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

View File

@ -240,8 +240,19 @@ module.exports = {
},
{
files: ['e2e/**'],
globals: {
by: true,
detox: true,
device: true,
element: true,
waitFor: true
},
rules: {
'no-await-in-loop': 0
'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
}
}
]

1
.gitignore vendored
View File

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

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.37.0"
versionName "4.36.0"
vectorDrawables.useSupportLibrary = true
if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
@ -250,7 +250,6 @@ android {
release {
minifyEnabled enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
signingConfig signingConfigs.release
if (!isFoss) {
firebaseCrashlytics {
@ -269,11 +268,6 @@ android {
// pickFirst '**/x86_64/libc++_shared.so'
// }
// FIXME: Remove when we update RN
packagingOptions {
pickFirst '**/*.so'
}
// applicationVariants are e.g. debug, release
flavorDimensions "app", "type"
@ -286,6 +280,10 @@ android {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
e2e {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
foss {
dimension = "type"
buildConfigField "boolean", "FDROID_BUILD", "true"
@ -313,6 +311,16 @@ android {
java.srcDirs = ['src/main/java', 'src/play/java']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayDebug {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayRelease {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
}
applicationVariants.all { variant ->
@ -377,9 +385,8 @@ dependencies {
implementation "com.github.bumptech.glide:glide:4.9.0"
annotationProcessor "com.github.bumptech.glide:compiler:4.9.0"
implementation "com.tencent:mmkv-static:1.2.10"
androidTestImplementation('com.wix:detox:+')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.facebook.soloader:soloader:0.10.4'
androidTestImplementation('com.wix:detox:+') { transitive = true }
androidTestImplementation 'junit:junit:4.12'
}
if (isNewArchitectureEnabled()) {

View File

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

View File

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

View File

@ -12,8 +12,14 @@
<uses-permission android:name="android.permission.AUDIO_CAPTURE" />
<!-- permissions related to jitsi call -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" tools:targetApi="Q"/>
<application
android:name="chat.rocket.reactnative.MainApplication"
android:allowBackup="false"

View File

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

View File

@ -1,5 +1,9 @@
import org.apache.tools.ant.taskdefs.condition.Os
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
@ -71,38 +75,5 @@ allprojects {
google()
maven { url 'https://maven.google.com' }
maven { url 'https://www.jitpack.io' }
// https://stackoverflow.com/a/74333788/5447468
// TODO: remove once we update RN
exclusiveContent {
// We get React Native's Android binaries exclusively through npm,
// from a local Maven repo inside node_modules/react-native/.
// (The use of exclusiveContent prevents looking elsewhere like Maven Central
// and potentially getting a wrong version.)
filter {
includeGroup "com.facebook.react"
}
forRepository {
maven {
// NOTE: if you are in a monorepo, you may have "$rootDir/../../../node_modules/react-native/android"
url "$rootDir/../node_modules/react-native/android"
}
}
}
}
}
subprojects { subproject ->
afterEvaluate {
if (!project.name.equalsIgnoreCase("app") && project.hasProperty("android")) {
android {
compileSdkVersion 31
buildToolsVersion "31.0.0"
defaultConfig {
minSdkVersion 23
targetSdkVersion 31
}
}
}
}
}

View File

@ -40,14 +40,7 @@ export const INQUIRY = createRequestTypes('INQUIRY', [
'QUEUE_UPDATE',
'QUEUE_REMOVE'
]);
export const APP = createRequestTypes('APP', [
'START',
'READY',
'INIT',
'INIT_LOCAL_SETTINGS',
'SET_MASTER_DETAIL',
'SET_NOTIFICATION_PRESENCE_CAP'
]);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);

View File

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

View File

@ -1,5 +1,4 @@
export const mappedIcons = {
'status-disabled': 59837,
'lamp-bulb': 59836,
'phone-in': 59835,
'basketball': 59776,

File diff suppressed because one or more lines are too long

View File

@ -1,31 +1,34 @@
import React from 'react';
import { useWindowDimensions } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { IEmoji } from '../../definitions/IEmoji';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { PressableEmoji } from './PressableEmoji';
import { EMOJI_BUTTON_SIZE } from './styles';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji } from '../../definitions/IEmoji';
import { PressableEmoji } from './PressableEmoji';
interface IEmojiCategoryProps {
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
tabLabel?: string; // needed for react-native-scrollable-tab-view only
parentWidth: number;
}
const EmojiCategory = ({ onEmojiSelected, emojis, parentWidth }: IEmojiCategoryProps): React.ReactElement | null => {
if (!parentWidth) {
return null;
}
const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
const { width } = useWindowDimensions();
const numColumns = Math.trunc(parentWidth / EMOJI_BUTTON_SIZE);
const marginHorizontal = (parentWidth % EMOJI_BUTTON_SIZE) / 2;
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;
}
return (
<FlatList
key={`emoji-category-${parentWidth}`}
// needed to update the numColumns when the width changes
key={`emoji-category-${width}`}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
data={emojis}
renderItem={renderItem}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
@ -20,8 +20,6 @@ const EmojiPicker = ({
searchedEmojis = []
}: IEmojiPickerProps): React.ReactElement | null => {
const { colors } = useTheme();
const [parentWidth, setParentWidth] = useState(0);
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
const allCustomEmojis: ICustomEmojis = useAppSelector(
@ -52,14 +50,7 @@ const EmojiPicker = ({
if (!emojis.length) {
return null;
}
return (
<EmojiCategory
parentWidth={parentWidth}
emojis={emojis}
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
tabLabel={label}
/>
);
return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
};
if (!loaded) {
@ -67,13 +58,9 @@ const EmojiPicker = ({
}
return (
<View style={styles.emojiPickerContainer} onLayout={e => setParentWidth(e.nativeEvent.layout.width)}>
<View style={styles.emojiPickerContainer}>
{searching ? (
<EmojiCategory
emojis={searchedEmojis}
onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
parentWidth={parentWidth}
/>
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
) : (
<ScrollableTabView
renderTabBar={() => <TabBar />}

View File

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

View File

@ -1,5 +1,4 @@
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';
@ -104,10 +103,9 @@ export const Badge = () => (
<HeaderExample
left={() => (
<HeaderButton.Container left>
<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.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.Container>
)}
/>
@ -116,23 +114,20 @@ 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.BadgeUnread tunread={[1]} />} />
</HeaderButton.Container>
)}
colors={colors[theme]}
/>
</View>
<HeaderExample
left={() => (
<HeaderButton.Container left>
<HeaderButton.Item iconName='threads' />
</HeaderButton.Container>
)}
right={() => (
<HeaderButton.Container>
<HeaderButton.Item title='Threads' />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
</HeaderButton.Container>
)}
colors={colors[theme]}
/>
</ThemeContext.Provider>
);

View File

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

View File

@ -1062,7 +1062,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
};
formatReplyMessage = async (replyingMessage: IMessage, message = '') => {
const { user, roomType, replyWithMention, serverVersion } = this.props;
const { user, roomType, replyWithMention } = this.props;
const permalink = await this.getPermalink(replyingMessage);
let msg = `[ ](${permalink}) `;
@ -1071,8 +1071,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
msg += `@${replyingMessage?.u?.username} `;
}
const connectionString = compareServerVersion(serverVersion, 'lowerThan', '5.0.0') ? ' ' : '\n';
return `${msg}${connectionString}${message}`;
return `${msg} ${message}`;
};
updateMentions = (keyword: any, type: string) => {

View File

@ -46,7 +46,6 @@ const RoomHeaderContainer = React.memo(
const connecting = useSelector((state: IApplicationState) => state.meteor.connecting || state.server.loading);
const usersTyping = useSelector((state: IApplicationState) => state.usersTyping, shallowEqual);
const connected = useSelector((state: IApplicationState) => state.meteor.connected);
const presenceDisabled = useSelector((state: IApplicationState) => state.settings.Presence_broadcast_disabled);
const activeUser = useSelector(
(state: IApplicationState) => (roomUserId ? state.activeUsers?.[roomUserId] : undefined),
shallowEqual
@ -62,13 +61,9 @@ const RoomHeaderContainer = React.memo(
if (connected) {
if ((type === 'd' || (tmid && roomUserId)) && activeUser) {
if (presenceDisabled) {
status = 'disabled';
} else {
const { status: statusActiveUser, statusText: statusTextActiveUser } = activeUser;
status = statusActiveUser;
statusText = statusTextActiveUser;
}
const { status: statusActiveUser, statusText: statusTextActiveUser } = activeUser;
status = statusActiveUser;
statusText = statusTextActiveUser;
} else if (type === 'l' && visitor?.status) {
const { status: statusVisitor } = visitor;
status = statusVisitor;

View File

@ -65,7 +65,6 @@ export const UserStatus = () => (
<RoomItem status='busy' />
<RoomItem status='offline' />
<RoomItem status='loading' />
<RoomItem status='disabled' />
<RoomItem status='wrong' />
</>
);

View File

@ -8,17 +8,11 @@ export const useUserStatus = (
id?: string
): { connected: boolean; status: TUserStatus } => {
const connected = useAppSelector(state => state.meteor.connected);
const presenceDisabled = useAppSelector(state => state.settings.Presence_broadcast_disabled);
const userStatus = useAppSelector(state => state.activeUsers[id || '']?.status);
let status = 'loading';
if (connected) {
if (type === 'd') {
if (presenceDisabled) {
status = 'disabled';
} else {
status = userStatus || 'loading';
}
status = userStatus || 'loading';
} else if (type === 'l' && liveChatStatus) {
status = liveChatStatus;
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { StyleProp, ViewStyle } from 'react-native';
import { SvgUri } from 'react-native-svg';
@ -29,12 +29,22 @@ interface IOmnichannelRoomIconProps {
}
export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: IOmnichannelRoomIconProps) => {
const [loading, setLoading] = useState(true);
const [svgError, setSvgError] = useState(false);
const baseUrl = useAppSelector(state => state.server?.server);
const connected = useAppSelector(state => state.meteor?.connected);
const customIcon = (
if (sourceType?.type === OmnichannelSourceType.APP && sourceType.id && sourceType.sidebarIcon && connected) {
return (
<SvgUri
height={size}
width={size}
color={STATUS_COLORS[status || 'offline']}
uri={`${baseUrl}/api/apps/public/${sourceType.id}/get-sidebar-icon?icon=${sourceType.sidebarIcon}`}
style={style}
/>
);
}
return (
<CustomIcon
name={iconMap[sourceType?.type || 'other']}
size={size}
@ -42,23 +52,4 @@ export const OmnichannelRoomIcon = ({ size, style, sourceType, status }: IOmnich
color={STATUS_COLORS[status || 'offline']}
/>
);
if (!svgError && sourceType?.type === OmnichannelSourceType.APP && sourceType.id && sourceType.sidebarIcon && connected) {
return (
<>
<SvgUri
height={size}
width={size}
color={STATUS_COLORS[status || 'offline']}
uri={`${baseUrl}/api/apps/public/${sourceType.id}/get-sidebar-icon?icon=${sourceType.sidebarIcon}`}
style={style}
onError={() => setSvgError(true)}
onLoad={() => setLoading(false)}
/>
{loading ? customIcon : null}
</>
);
}
return customIcon;
};

View File

@ -6,15 +6,9 @@ import { IStatus } from './definition';
import { useAppSelector } from '../../lib/hooks';
const StatusContainer = ({ id, style, size = 32, ...props }: Omit<IStatus, 'status'>): React.ReactElement => {
const status = useAppSelector(state => {
if (state.settings.Presence_broadcast_disabled) {
return 'disabled';
}
if (state.meteor.connected) {
return state.activeUsers[id] && state.activeUsers[id].status;
}
return 'loading';
}) as TUserStatus;
const status = useAppSelector(state =>
state.meteor.connected ? state.activeUsers[id] && state.activeUsers[id].status : 'loading'
) as TUserStatus;
return <Status size={size} style={style} status={status} {...props} />;
};

View File

@ -6,6 +6,7 @@ import i18n from '../../../../i18n';
import { getSubscriptionByRoomId } from '../../../../lib/database/services/Subscription';
import { useAppSelector } from '../../../../lib/hooks';
import { getRoomAvatar, getUidDirectMessage } from '../../../../lib/methods/helpers';
import { videoConfStartAndJoin } from '../../../../lib/methods/videoConf';
import { useTheme } from '../../../../theme';
import { useActionSheet } from '../../../ActionSheet';
import AvatarContainer from '../../../Avatar';
@ -15,12 +16,12 @@ import { BUTTON_HIT_SLOP } from '../../../message/utils';
import StatusContainer from '../../../Status';
import useStyle from './styles';
export default function StartACallActionSheet({ rid, initCall }: { rid: string; initCall: Function }): React.ReactElement {
export default function CallAgainActionSheet({ rid }: { rid: string }): React.ReactElement {
const style = useStyle();
const { colors } = useTheme();
const [user, setUser] = useState({ username: '', avatar: '', uid: '' });
const [mic, setMic] = useState(true);
const [cam, setCam] = useState(false);
const [user, setUser] = useState({ username: '', avatar: '', uid: '', rid: '' });
const [phone, setPhone] = useState(true);
const [camera, setCamera] = useState(false);
const username = useAppSelector(state => state.login.user.username);
const { hideActionSheet } = useActionSheet();
@ -30,7 +31,7 @@ export default function StartACallActionSheet({ rid, initCall }: { rid: string;
const room = await getSubscriptionByRoomId(rid);
const uid = (await getUidDirectMessage(room)) as string;
const avt = getRoomAvatar(room);
setUser({ uid, username: room?.name || '', avatar: avt });
setUser({ uid, username: room?.name || '', avatar: avt, rid: room?.id || '' });
})();
}, [rid]);
@ -42,27 +43,25 @@ export default function StartACallActionSheet({ rid, initCall }: { rid: string;
<Text style={style.actionSheetHeaderTitle}>{i18n.t('Start_a_call')}</Text>
<View style={style.actionSheetHeaderButtons}>
<Touchable
onPress={() => setCam(!cam)}
style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]}
onPress={() => setCamera(!camera)}
style={[style.iconCallContainer, camera && style.enabledBackground, { marginRight: 6 }]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={20} color={handleColor(cam)} />
<CustomIcon name={camera ? 'camera' : 'camera-disabled'} size={16} color={handleColor(camera)} />
</Touchable>
<Touchable
onPress={() => setMic(!mic)}
style={[style.iconCallContainer, mic && style.enabledBackground]}
onPress={() => setPhone(!phone)}
style={[style.iconCallContainer, phone && style.enabledBackground]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={20} color={handleColor(mic)} />
<CustomIcon name={phone ? 'microphone' : 'microphone-disabled'} size={16} color={handleColor(phone)} />
</Touchable>
</View>
</View>
<View style={style.actionSheetUsernameContainer}>
<AvatarContainer text={user.avatar} size={36} />
<StatusContainer size={16} id={user.uid} style={{ marginLeft: 8, marginRight: 6 }} />
<Text style={style.actionSheetUsername} numberOfLines={1}>
{user.username}
</Text>
<Text style={style.actionSheetUsername}>{user.username}</Text>
</View>
<View style={style.actionSheetPhotoContainer}>
<AvatarContainer size={62} text={username} />
@ -71,7 +70,7 @@ export default function StartACallActionSheet({ rid, initCall }: { rid: string;
onPress={() => {
hideActionSheet();
setTimeout(() => {
initCall({ cam, mic });
videoConfStartAndJoin(user.rid, camera);
}, 100);
}}
title={i18n.t('Call')}

View File

@ -3,16 +3,17 @@ import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import i18n from '../../../../i18n';
import { videoConfJoin } from '../../../../lib/methods/videoConf';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => {
const style = useStyle();
const { joinCall } = useVideoConf();
return (
<VideoConferenceBaseContainer variant='incoming'>
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(blockId)}>
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
</Touchable>
<Text style={style.callBack}>{i18n.t('Waiting_for_answer')}</Text>

View File

@ -6,7 +6,9 @@ import { IUser } from '../../../../definitions';
import { VideoConferenceType } from '../../../../definitions/IVideoConference';
import i18n from '../../../../i18n';
import { useAppSelector } from '../../../../lib/hooks';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import { useSnaps } from '../../../../lib/hooks/useSnaps';
import { useActionSheet } from '../../../ActionSheet';
import CallAgainActionSheet from './CallAgainActionSheet';
import { CallParticipants, TCallUsers } from './CallParticipants';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
@ -24,7 +26,8 @@ export default function VideoConferenceEnded({
}): React.ReactElement {
const style = useStyle();
const username = useAppSelector(state => state.login.user.username);
const { showInitCallActionSheet } = useVideoConf(rid);
const { showActionSheet } = useActionSheet();
const snaps = useSnaps([1250]);
const onlyAuthorOnCall = users.length === 1 && users.some(user => user.username === createdBy.username);
@ -32,7 +35,15 @@ export default function VideoConferenceEnded({
<VideoConferenceBaseContainer variant='ended'>
{type === 'direct' ? (
<>
<Touchable style={style.callToActionCallBack} onPress={showInitCallActionSheet}>
<Touchable
style={style.callToActionCallBack}
onPress={() =>
showActionSheet({
children: <CallAgainActionSheet rid={rid} />,
snaps
})
}
>
<Text style={style.callToActionCallBackText}>
{createdBy.username === username ? i18n.t('Call_back') : i18n.t('Call_again')}
</Text>

View File

@ -3,17 +3,18 @@ import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import i18n from '../../../../i18n';
import { videoConfJoin } from '../../../../lib/methods/videoConf';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import { CallParticipants, TCallUsers } from './CallParticipants';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
export default function VideoConferenceOutgoing({ users, blockId }: { users: TCallUsers; blockId: string }): React.ReactElement {
const style = useStyle();
const { joinCall } = useVideoConf();
return (
<VideoConferenceBaseContainer variant='outgoing'>
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(blockId)}>
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
</Touchable>
<CallParticipants users={users} />

View File

@ -100,8 +100,7 @@ export default function useStyle() {
actionSheetUsername: {
fontSize: 16,
...sharedStyles.textBold,
color: colors.passcodePrimary,
flexShrink: 1
color: colors.passcodePrimary
},
enabledBackground: {
backgroundColor: colors.conferenceCallEnabledIconBackground

View File

@ -2,7 +2,7 @@
import { BlockContext } from '@rocket.chat/ui-kit';
import React, { useContext, useState } from 'react';
import { videoConfJoin } from '../../lib/methods/videoConf';
import { useVideoConf } from '../../lib/hooks/useVideoConf';
import { IText } from './interfaces';
export const textParser = ([{ text }]: IText[]) => text;
@ -40,6 +40,7 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false);
const { joinCall } = useVideoConf();
const error = errors && actionId && errors[actionId];
@ -57,7 +58,7 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
try {
if (appId === 'videoconf-core' && blockId) {
setLoading(false);
return videoConfJoin(blockId);
return joinCall(blockId);
}
await action({
blockId,

View File

@ -16,13 +16,11 @@ const styles = StyleSheet.create({
},
bottomContainerText: {
...sharedStyles.textRegular,
fontSize: 13,
textAlign: 'center'
fontSize: 13
},
bottomContainerTextBold: {
...sharedStyles.textSemibold,
fontSize: 13,
textAlign: 'center'
fontSize: 13
}
});

View File

@ -18,29 +18,13 @@ import MarkdownContext from './MarkdownContext';
interface IParagraphProps {
value: ParagraphProps['value'];
forceTrim?: boolean;
}
const Inline = ({ value, forceTrim }: IParagraphProps): React.ReactElement | null => {
const Inline = ({ value }: IParagraphProps): React.ReactElement | null => {
const { useRealName, username, navToRoomInfo, mentions, channels } = useContext(MarkdownContext);
return (
<Text style={styles.inline}>
{value.map((block, index) => {
// We are forcing trim when is a `[ ](https://https://open.rocket.chat/) plain_text`
// to clean the empty spaces
if (forceTrim) {
if (index === 0 && block.type === 'LINK') {
block.value.label.value =
// Need to update the @rocket.chat/message-parser to understand that the label can be a Markup | Markup[]
// https://github.com/RocketChat/fuselage/blob/461ecf661d9ff4a46390957c915e4352fa942a7c/packages/message-parser/src/definitions.ts#L141
// @ts-ignore
block.value?.label?.value?.toString().trimLeft() || block?.value?.label?.[0]?.value?.toString().trimLeft();
}
if (index === 1 && block.type !== 'LINK') {
block.value = block.value?.toString().trimLeft();
}
}
{value.map(block => {
switch (block.type) {
case 'IMAGE':
return <Image value={block.value} />;

View File

@ -381,6 +381,27 @@ export const BlockQuote = () => (
</View>
);
const rocketChatLink = [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat'
},
label: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat'
}
}
}
]
}
];
const markdownLink = [
{
type: 'PARAGRAPH',
@ -466,6 +487,7 @@ const markdownLinkWithEmphasis = [
export const Links = () => (
<View style={styles.container}>
<NewMarkdown tokens={rocketChatLink} />
<NewMarkdown tokens={markdownLink} />
<NewMarkdown tokens={markdownLinkWithEmphasis} />
</View>
@ -784,128 +806,3 @@ export const InlineKatex = () => (
<NewMarkdown tokens={inlineKatex} />
</View>
);
const messageQuote = {
/**
# Hello head 1
[ ](https://google.com)
*/
headAndLink: [
{ type: 'HEADING', level: 1, value: [{ type: 'PLAIN_TEXT', value: 'Hello head 1' }] },
{ type: 'LINE_BREAK' },
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: { src: { type: 'PLAIN_TEXT', value: 'https://google.com' }, label: { type: 'PLAIN_TEXT', value: ' ' } }
}
]
}
],
/**
# Head 1 as the first line then line break and after paragraph
bla bla bla bla bla bla
bla bla bla bla bla bla
[ ](https://google.com)
*/
headTextAndLink: [
{
type: 'HEADING',
level: 1,
value: [{ type: 'PLAIN_TEXT', value: 'Head 1 as the first line then line break and after paragraph' }]
},
{ type: 'LINE_BREAK' },
{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'bla bla bla bla bla bla ' }] },
{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'bla bla bla bla bla bla ' }] },
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: { src: { type: 'PLAIN_TEXT', value: 'https://google.com' }, label: { type: 'PLAIN_TEXT', value: ' ' } }
}
]
}
],
/**
[ ](permalink from message)\n# Head 1 after a forced line break
asdas asd asd asd
*/
headTextAndQuote: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: { type: 'PLAIN_TEXT', value: 'https://open.rocket.chat/direct/subaru123?msg=QB42gWcaO6BgqtLTo' },
label: { type: 'PLAIN_TEXT', value: ' ' }
}
},
{ type: 'PLAIN_TEXT', value: ' ' }
]
},
{ type: 'HEADING', level: 1, value: [{ type: 'PLAIN_TEXT', value: 'Head 1 after a forced line break' }] },
{ type: 'LINE_BREAK' },
{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Description' }] }
],
/**
[ ](https://google.com) *There is a link before this bold separated by single space*
*/
linkAndBoldText: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: { src: { type: 'PLAIN_TEXT', value: 'https://google.com' }, label: { type: 'PLAIN_TEXT', value: ' ' } }
},
{ type: 'PLAIN_TEXT', value: ' ' },
{ type: 'BOLD', value: [{ type: 'PLAIN_TEXT', value: 'There is a link before this bold separated by single space' }] }
]
}
],
simpleQuote: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: {
type: 'PLAIN_TEXT',
value: 'https://open.rocket.chat/group/quoteeee9798789?msg=ZZp6t2dCRX4TqExht'
},
// format of label for servers greater or equal than 6.0
label: [
{
type: 'PLAIN_TEXT',
value: ' '
}
]
}
}
]
},
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value: 'Quoting a message wrote before'
}
]
}
]
};
export const MessageQuote = () => (
<View style={styles.container}>
<NewMarkdown tokens={messageQuote.headAndLink} />
<NewMarkdown tokens={messageQuote.headTextAndLink} />
<NewMarkdown tokens={messageQuote.headTextAndQuote} />
<NewMarkdown tokens={messageQuote.linkAndBoldText} />
<NewMarkdown tokens={messageQuote.simpleQuote} />
</View>
);

View File

@ -12,28 +12,10 @@ interface IParagraphProps {
}
const Paragraph = ({ value }: IParagraphProps) => {
let forceTrim = false;
const { theme } = useTheme();
if (
value?.[0]?.type === 'LINK' &&
// Need to update the @rocket.chat/message-parser to understand that the label can be a Markup | Markup[]
// https://github.com/RocketChat/fuselage/blob/461ecf661d9ff4a46390957c915e4352fa942a7c/packages/message-parser/src/definitions.ts#L141
// @ts-ignore
(value?.[0]?.value?.label?.value?.toString().trim() === '' || value?.[0]?.value?.label?.[0]?.value?.toString().trim() === '')
) {
// We are returning null when we receive a message like this: `[ ](https://open.rocket.chat/)\nplain_text`
// to avoid render a line empty above the the message
if (value.length === 1) {
return null;
}
if (value.length === 2 && value?.[1]?.type === 'PLAIN_TEXT' && value?.[1]?.value?.toString().trim() === '') {
return null;
}
forceTrim = true;
}
return (
<Text style={[styles.text, { color: themes[theme].bodyText }]}>
<Inline value={value} forceTrim={forceTrim} />
<Inline value={value} />
</Text>
);
};

View File

@ -8,7 +8,7 @@ interface IPlainProps {
value: PlainProps['value'];
}
const Plain = ({ value }: IPlainProps): React.ReactElement => (
const Plain = ({ value }: IPlainProps) => (
<Text accessibilityLabel={value} style={styles.plainText}>
{value}
</Text>

View File

@ -4,6 +4,7 @@ import { Tasks as TasksProps } from '@rocket.chat/message-parser';
import Inline from './Inline';
import styles from '../styles';
import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme';
interface ITasksProps {
@ -11,15 +12,13 @@ interface ITasksProps {
}
const TaskList = ({ value = [] }: ITasksProps) => {
const { colors } = useTheme();
const { theme } = useTheme();
return (
<View>
{value.map(item => (
<View style={styles.row}>
<Text style={[styles.text, { color: colors.bodyText }]}>{item.status ? '- [x] ' : '- [ ] '}</Text>
<Text style={[styles.inline, { color: colors.bodyText }]}>
<Inline value={item.value} />
</Text>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{item.status ? '- [x] ' : '- [ ] '}</Text>
<Inline value={item.value} />
</View>
))}
</View>

View File

@ -10,12 +10,12 @@ import { themes } from '../../lib/constants';
import { IMessageCallButton } from './interfaces';
import { useTheme } from '../../theme';
const CallButton = React.memo(({ handleEnterCall }: IMessageCallButton) => {
const CallButton = React.memo(({ callJitsi }: IMessageCallButton) => {
const { theme } = useTheme();
return (
<View style={styles.buttonContainer}>
<Touchable
onPress={handleEnterCall}
onPress={callJitsi}
background={Touchable.Ripple(themes[theme].bannerBackground)}
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
hitSlop={BUTTON_HIT_SLOP}

View File

@ -1,20 +1,14 @@
import React from 'react';
import { themes } from '../../../../lib/constants';
import { CustomIcon } from '../../../CustomIcon';
import styles from '../../styles';
import { useTheme } from '../../../../theme';
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => {
const { colors } = useTheme();
if (isReadReceiptEnabled) {
return (
<CustomIcon
name='check'
color={!unread && unread !== null ? colors.tintColor : colors.auxiliaryTintColor}
size={16}
style={styles.rightIcons}
/>
);
const { theme } = useTheme();
if (isReadReceiptEnabled && !unread && unread !== null) {
return <CustomIcon name='check' color={themes[theme].tintColor} size={16} style={styles.rightIcons} />;
}
return null;
});

View File

@ -53,7 +53,7 @@ const Content = React.memo(
content = (
<Markdown
msg={props.msg}
md={props.type !== 'e2e' ? props.md : undefined}
md={props.md}
getCustomEmoji={props.getCustomEmoji}
enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username}

View File

@ -18,7 +18,7 @@ const MessageAvatar = React.memo(({ isHeader, avatar, author, small, navToRoomIn
style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username}
size={small ? 20 : 36}
borderRadius={4}
borderRadius={small ? 2 : 4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
getCustomEmoji={getCustomEmoji}
avatar={avatar}

View File

@ -247,8 +247,6 @@ const Reply = React.memo(
>
<View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<UrlImage image={attachment.thumb_url} />
<Attachments
attachments={attachment.attachments}
getCustomEmoji={getCustomEmoji}
@ -257,6 +255,8 @@ const Reply = React.memo(
isReply
id={messageId}
/>
<UrlImage image={attachment.thumb_url} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
{loading ? (
<View style={[styles.backdrop]}>

View File

@ -50,7 +50,7 @@ interface IMessageContainerProps {
showAttachment: (file: IAttachment) => void;
onReactionLongPress?: (item: TAnyMessageModel) => void;
navToRoomInfo: (navParam: IRoomInfoParam) => void;
handleEnterCall?: () => void;
callJitsi?: () => void;
blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
threadBadgeColor?: string;
@ -69,6 +69,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
static defaultProps = {
getCustomEmoji: () => null,
onLongPress: () => {},
callJitsi: () => {},
blockAction: () => {},
archived: false,
broadcast: false,
@ -337,7 +338,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
navToRoomInfo,
getCustomEmoji,
isThreadRoom,
handleEnterCall,
callJitsi,
blockAction,
rid,
threadBadgeColor,
@ -455,7 +456,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo}
handleEnterCall={handleEnterCall}
callJitsi={callJitsi}
blockAction={blockAction}
highlighted={highlighted}
comment={comment}

View File

@ -40,7 +40,7 @@ export interface IMessageBroadcast {
}
export interface IMessageCallButton {
handleEnterCall?: () => void;
callJitsi?: () => void;
}
export interface IMessageContent {

View File

@ -4,34 +4,37 @@ import type { IRoom } from './IRoom';
import type { IUser } from './IUser';
import type { IMessage } from './IMessage';
export declare enum VideoConferenceStatus {
export enum VideoConferenceStatus {
CALLING = 0,
STARTED = 1,
EXPIRED = 2,
ENDED = 3,
DECLINED = 4
}
export declare type DirectCallInstructions = {
export type DirectCallInstructions = {
type: 'direct';
calleeId: IUser['_id'];
callee: IUser['_id'];
callId: string;
};
export declare type ConferenceInstructions = {
export type ConferenceInstructions = {
type: 'videoconference';
callId: string;
rid: IRoom['_id'];
};
export declare type LivechatInstructions = {
export type LivechatInstructions = {
type: 'livechat';
callId: string;
};
export declare type VideoConferenceType =
| DirectCallInstructions['type']
| ConferenceInstructions['type']
| LivechatInstructions['type'];
export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type'];
export interface IVideoConferenceUser extends Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> {
ts: Date;
}
export interface IVideoConference extends IRocketChatRecord {
type: VideoConferenceType;
rid: string;
@ -42,68 +45,51 @@ export interface IVideoConference extends IRocketChatRecord {
ended?: IMessage['_id'];
};
url?: string;
createdBy: Pick<Required<IUser>, '_id' | 'username' | 'name'>;
createdBy: Pick<IUser, '_id' | 'username' | 'name'>;
createdAt: Date;
endedBy?: Pick<Required<IUser>, '_id' | 'username' | 'name'>;
endedBy?: Pick<IUser, '_id' | 'username' | 'name'>;
endedAt?: Date;
providerName: string;
providerData?: Record<string, any>;
ringing?: boolean;
}
export interface IDirectVideoConference extends IVideoConference {
type: 'direct';
}
export interface IGroupVideoConference extends IVideoConference {
type: 'videoconference';
anonymousUsers: number;
title: string;
}
export interface ILivechatVideoConference extends IVideoConference {
type: 'livechat';
}
export declare type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
export declare type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
export declare const isDirectVideoConference: (call: VideoConference | undefined | null) => call is IDirectVideoConference;
export declare const isGroupVideoConference: (call: VideoConference | undefined | null) => call is IGroupVideoConference;
export declare const isLivechatVideoConference: (call: VideoConference | undefined | null) => call is ILivechatVideoConference;
declare type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & {
createdBy: IUser['_id'];
};
declare type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & {
createdBy: IUser['_id'];
};
declare type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & {
createdBy: IUser['_id'];
};
export declare type VideoConferenceCreateData = AtLeast<
export type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
export const isDirectVideoConference = (call: VideoConference | undefined | null): call is IDirectVideoConference =>
call?.type === 'direct';
export const isGroupVideoConference = (call: VideoConference | undefined | null): call is IGroupVideoConference =>
call?.type === 'videoconference';
export const isLivechatVideoConference = (call: VideoConference | undefined | null): call is ILivechatVideoConference =>
call?.type === 'livechat';
type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
export type VideoConferenceCreateData = AtLeast<
DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData,
'createdBy' | 'type' | 'rid' | 'providerName' | 'providerData'
>;
export type VideoConferenceCapabilities = {
mic?: boolean;
cam?: boolean;
title?: boolean;
};
export type VideoConfStartProps = { roomId: string; title?: string; allowRinging?: boolean };
export type VideoConfJoinProps = {
callId: string;
state?: {
mic?: boolean;
cam?: boolean;
};
};
export type VideoConfCancelProps = {
callId: string;
};
export type VideoConfListProps = {
roomId: string;
count?: number;
offset?: number;
};
export type VideoConfInfoProps = { callId: string };

View File

@ -1,3 +1,3 @@
export const STATUSES = ['offline', 'online', 'away', 'busy', 'disabled'] as const;
export const STATUSES = ['offline', 'online', 'away', 'busy'] as const;
export type TUserStatus = typeof STATUSES[number];

View File

@ -1,45 +1,27 @@
import {
VideoConfCancelProps,
VideoConference,
VideoConferenceCapabilities,
VideoConferenceInstructions,
VideoConfInfoProps,
VideoConfJoinProps,
VideoConfListProps,
VideoConfStartProps
} from '../../IVideoConference';
import { PaginatedResult } from '../helpers/PaginatedResult';
import { VideoConference } from '../../IVideoConference';
export type VideoConferenceEndpoints = {
'video-conference.start': {
POST: (params: VideoConfStartProps) => { data: VideoConferenceInstructions & { providerName: string } };
};
'video-conference.join': {
POST: (params: VideoConfJoinProps) => { url: string; providerName: string };
};
'video-conference.cancel': {
POST: (params: VideoConfCancelProps) => void;
};
'video-conference.info': {
GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
};
'video-conference.list': {
GET: (params: VideoConfListProps) => PaginatedResult<{ data: VideoConference[] }>;
};
'video-conference.capabilities': {
GET: () => { providerName: string; capabilities: VideoConferenceCapabilities };
};
'video-conference.providers': {
GET: () => { data: { key: string; label: string }[] };
};
'video-conference/jitsi.update-timeout': {
POST: (params: { roomId: string }) => void;
};
'video-conference.join': {
POST: (params: { callId: string; state: { cam: boolean } }) => { url: string; providerName: string };
};
'video-conference.start': {
POST: (params: { roomId: string }) => { url: string };
};
'video-conference.cancel': {
POST: (params: { callId: string }) => void;
};
'video-conference.info': {
GET: (params: { callId: string }) => VideoConference & {
capabilities: {
mic?: boolean;
cam?: boolean;
title?: boolean;
};
};
};
};

View File

@ -12,6 +12,3 @@ declare module 'react-native-restart';
declare module 'react-native-jitsi-meet';
declare module 'rn-root-view';
declare module 'react-native-math-view';
declare module '@env' {
export const RUNNING_E2E_TESTS: string;
}

View File

@ -876,20 +876,5 @@
"Call": "Call",
"Reply_in_direct_message": "Reply in Direct Message",
"room_archived": "archived room",
"room_unarchived": "unarchived room",
"no-videoconf-provider-app-header": "Conference call not available",
"no-videoconf-provider-app-body": "Conference call apps can be installed in the Rocket.Chat marketplace by a workspace admin.",
"admin-no-videoconf-provider-app-header": "Conference call not enabled",
"admin-no-videoconf-provider-app-body": "Conference call apps are available in the Rocket.Chat marketplace.",
"no-active-video-conf-provider-header": "Conference call not enabled",
"no-active-video-conf-provider-body": "A workspace admin needs to enable the conference call feature first.",
"admin-no-active-video-conf-provider-header": "Conference call not enabled",
"admin-no-active-video-conf-provider-body": "Configure conference calls in order to make it available on this workspace.",
"video-conf-provider-not-configured-header": "Conference call not enabled",
"video-conf-provider-not-configured-body": "A workspace admin needs to enable the conference calls feature first.",
"admin-video-conf-provider-not-configured-header": "Conference call not enabled",
"admin-video-conf-provider-not-configured-body": "Configure conference calls in order to make it available on this workspace.",
"Presence_Cap_Warning_Title": "User status temporarily disabled",
"Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.",
"Learn_more": "Learn more"
"room_unarchived": "unarchived room"
}

View File

@ -12,7 +12,6 @@
"error-could-not-change-email": "Não foi possível mudar e-mail",
"error-could-not-change-name": "Não foi possível mudar o nome",
"error-could-not-change-username": "Não foi possível alterar o nome de usuário",
"error-could-not-change-status": "Não foi possível alterar o status",
"error-delete-protected-role": "Não é possível remover um papel protegido",
"error-department-not-found": "Departamento não encontrado",
"error-direct-message-file-upload-not-allowed": "Compartilhamento de arquivos não está permitido em mensagens diretas",
@ -20,7 +19,6 @@
"error-email-domain-blacklisted": "O domínio de e-mail está na lista negra",
"error-email-send-failed": "Erro ao tentar enviar e-mail: {{message}}",
"error-save-image": "Erro ao salvar imagem",
"error-save-video": "Erro ao salvar vídeo",
"error-field-unavailable": "{{field}} já está sendo usado :(",
"error-file-too-large": "Arquivo é muito grande",
"error-not-permission-to-upload-file": "Você não tem permissão para enviar arquivos",
@ -90,7 +88,6 @@
"Add_Reaction": "Reagir",
"Add_Server": "Adicionar servidor",
"Add_users": "Adicionar usuário",
"Admin_Panel": "Painel de admin",
"Agent": "Agente",
"Alert": "Alerta",
"alert": "alerta",
@ -99,7 +96,6 @@
"All_users_in_the_team_can_write_new_messages": "Todos usuários no canal podem enviar mensagens novas",
"A_meaningful_name_for_the_discussion_room": "Um nome significativo para o canal de discussão",
"All": "Todos",
"All_Messages": "Todas as mensagens",
"Allow_Reactions": "Permitir reagir",
"Alphabetical": "Alfabético",
"and_more": "e mais",
@ -167,10 +163,7 @@
"Copied_to_clipboard": "Copiado para a área de transferência!",
"Copy": "Copiar",
"Conversation": "Conversação",
"Certificate_password": "Senha do certificado",
"Clear_cache": "Limpar cache da workspace",
"Clear_cache_loading": "Limpando cache.",
"Whats_the_password_for_your_certificate": "Qual é a senha para o seu certificado?",
"Create_account": "Criar conta",
"Create_Channel": "Criar Canal",
"Create_Direct_Messages": "Criar Mensagens Diretas",
@ -178,7 +171,6 @@
"Created_snippet": "criou um snippet",
"Create_a_new_workspace": "Criar nova área de trabalho",
"Create": "Criar",
"Custom_Status": "Status personalizado",
"Dark": "Escuro",
"Dark_level": "Nível escuro",
"Default": "Padrão",
@ -263,7 +255,6 @@
"Has_left_the_team": "saiu da equipe",
"Hide_System_Messages": "Esconder mensagens do sistema",
"Hide_type_messages": "Esconder mensagens de \"{{type}}\"",
"How_It_Works": "Como funciona",
"Message_HideType_uj": "Utilizador Entrou",
"Message_HideType_ul": "Utilizador Saiu",
"Message_HideType_ru": "Utilizador Removido",
@ -277,15 +268,11 @@
"Message_HideType_subscription_role_removed": "Papel removido",
"Message_HideType_room_archived": "Sala arquivada",
"Message_HideType_room_unarchived": "Sala desarquivada",
"I_Saved_My_E2E_Password": "Salvei minha senha ponta-a-ponta",
"IP": "IP",
"In_app": "No app",
"In_App_And_Desktop": "In-app e área de trabalho",
"In_App_and_Desktop_Alert_info": "Exibe um banner na parte superior da tela quando o aplicativo é aberto e exibe uma notificação na área de trabalho",
"Invisible": "Invisível",
"Invite": "Convidar",
"is_a_valid_RocketChat_instance": "é uma instância Rocket.Chat",
"is_not_a_valid_RocketChat_instance": "não é uma instância Rocket.Chat",
"is_typing": "está digitando",
"Invalid_or_expired_invite_token": "Token de convite inválido ou vencido",
"Invalid_server_version": "O servidor que você está conectando não é suportado mais por esta versão do aplicativo: {{currentVersion}}.\n\nEsta versão do aplicativo requer a versão {{minVersion}} do servidor para funcionar corretamente.",
@ -306,9 +293,7 @@
"leave": "sair",
"Legal": "Legal",
"Light": "Claro",
"License": "Licença",
"Livechat": "Livechat",
"Livechat_edit": "Editar livechat",
"Livechat_transfer_return_to_the_queue": "retornou conversa para a fila",
"Login": "Entrar",
"Login_error": "Suas credenciais foram rejeitadas. Tente novamente por favor!",
@ -317,7 +302,6 @@
"Logout": "Sair",
"Max_number_of_uses": "Número máximo de usos",
"Max_number_of_users_allowed_is_number": "Número máximo de usuários é {{maxUsers}}",
"members": "membros",
"Members": "Membros",
"Mentioned_Messages": "Mensagens mencionadas",
"mentioned": "mencionado",
@ -326,18 +310,14 @@
"Message_actions": "Ações",
"Message_pinned": "Fixou uma mensagem",
"Message_removed": "mensagem removida",
"Message_starred": "Mensagem adicionada aos favoritos",
"Message_unstarred": "Mensagem removida dos favoritos",
"message": "mensagem",
"messages": "mensagens",
"Message": "Mensagem",
"Messages": "Mensagens",
"Message_Reported": "Mensagem reportada",
"Microphone_Permission_Message": "Rocket.Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.",
"Microphone_Permission": "Acesso ao Microfone",
"Mute": "Mudo",
"muted": "mudo",
"My_servers": "Minhas workspaces",
"N_people_reacted": "{{n}} pessoas reagiram",
"N_users": "{{n}} usuários",
"N_channels": "{{n}} canais",
@ -346,7 +326,6 @@
"New_chat_transfer": "Nova transferência de conversa: {{agent}} retornou conversa para a fila",
"New_Message": "Nova Mensagem",
"New_Password": "Nova Senha",
"New_Server": "Nova workspace",
"Next": "Próximo",
"No_files": "Não há arquivos",
"No_limit": "Sem limite",
@ -360,8 +339,6 @@
"No_Message": "Não há mensagens",
"No_messages_yet": "Não há mensagens ainda",
"No_Reactions": "Sem reações",
"No_Read_Receipts": "Não lida",
"Not_logged": "Desconectado",
"Not_RC_Server": "Este não é um servidor Rocket.Chat.\n{{contact}}",
"Nothing": "Nada",
"Nothing_to_save": "Nada para salvar!",
@ -482,7 +459,6 @@
"Search_emoji": "Buscar emoji",
"Search_global_users": "Busca por usuários globais",
"Search_global_users_description": "Caso ativado, busca por usuários de outras empresas ou servidores.",
"Seconds": "{{second}} segundos",
"Security_and_privacy": "Segurança e privacidade",
"Select_Avatar": "Selecionar Avatar",
"Select_Server": "Selecionar Servidor",
@ -497,20 +473,13 @@
"Send_message": "Enviar mensagem",
"Send_me_the_code_again": "Envie-me o código novamente",
"Send_to": "Enviar para...",
"Sending_to": "Envio para",
"Sent_an_attachment": "Enviou um anexo",
"Server": "Servidor",
"Servers": "Workspaces",
"Server_version": "Versão da workspace: {{version}}",
"Set_username_subtitle": "O usuário é utilizado para permitir que você seja mencionado em mensagens",
"Set_custom_status": "Definir status personalizado",
"Set_status": "Definir status",
"Status_saved_successfully": "Status salvo com sucesso!",
"Settings": "Configurações",
"Settings_succesfully_changed": "Configurações salvas com sucesso!",
"Share": "Compartilhar",
"Share_Link": "Share Link",
"Share_this_app": "Compartilhar esse app",
"Show_more": "Mostrar mais..",
"Sign_in_your_server": "Entrar no seu servidor",
"Sign_Up": "Registrar",
@ -527,12 +496,9 @@
"Started_call": "Chamada iniciada por {{userBy}}",
"Submit": "Enviar",
"Table": "Tabela",
"Tags": "Tags",
"Take_a_photo": "Tirar uma foto",
"Take_a_video": "Gravar um vídeo",
"Take_it": "Pegue!",
"tap_to_change_status": "toque para mudar de status",
"Tap_to_view_servers_list": "Toque para visualizar as workspaces",
"Terms_of_Service": " Termos de Serviço ",
"Theme": "Tema",
"The_user_wont_be_able_to_type_in_roomName": "O usuário não poderá digitar em {{roomName}}",
@ -577,14 +543,10 @@
"User_has_been_removed": "removeu {{userRemoved}}",
"User_sent_an_attachment": "{{user}} enviou um anexo",
"User_has_been_unmuted": "permitiu que {{userUnmuted}} fale na sala",
"Defined_user_as_role": "definiu {{user}} como {{role}}",
"Removed_user_as_role": "removeu {{user}} como {{role}}",
"Username_is_empty": "Usuário está vazio",
"Username": "Usuário",
"Username_or_email": "Usuário ou email",
"Uses_server_configuration": "Usar configuração do servidor",
"Validating": "Validando...",
"Registration_Succeeded": "Registrado com sucesso!",
"Verify": "Verificar",
"Verify_email_title": "Registrado com sucesso!",
"Verify_email_desc": "Nós lhe enviamos um e-mail para confirmar o seu registro. Se você não receber um e-mail em breve, por favor retorne e tente novamente.",
@ -613,9 +575,7 @@
"You_were_removed_from_channel": "Você foi removido de {{channel}}",
"you": "você",
"You": "Você",
"Logged_out_by_server": "Você foi desconectado pela workspace. Por favor entre novamente.",
"Token_expired": "Sua sessão expirou. Por favor entre novamente.",
"You_need_to_access_at_least_one_RocketChat_server_to_share_something": "Você precisa acessar pelo menos uma workspace Rocket.Chat para compartilhar.",
"You_need_to_verifiy_your_email_address_to_get_notications": "Você precisa confirmar seu endereço de e-mail para obter notificações",
"Your_certificate": "Seu certificado",
"Your_invite_link_will_expire_after__usesLeft__uses": "Seu link de convite irá vencer depois de {{usesLeft}} usos.",
@ -623,8 +583,6 @@
"Your_invite_link_will_expire_on__date__": "Seu link de convite irá vencer em {{date}}.",
"Your_invite_link_will_never_expire": "Seu link de convite nunca irá vencer.",
"Your_workspace": "Sua workspace",
"Your_password_is": "Sua senha é",
"Version_no": "Versão: {{version}}",
"You_will_not_be_able_to_recover_this_message": "Você não será capaz de recuperar essa mensagem!",
"You_will_unset_a_certificate_for_this_server": "Você cancelará a configuração de um certificado para este servidor",
"Change_Language": "Alterar idioma",
@ -725,10 +683,6 @@
"Teams": "Times",
"No_team_channels_found": "Nenhum canal encontrado",
"Team_not_found": "Time não encontrado",
"Create_Team": "Criar time",
"Team_Name": "Nome do time",
"creating_team": "criando time",
"team-name-already-exists": "Um time com esse nome já existe",
"Add_Channel_to_Team": "Adicionar Canal ao Time",
"Left_The_Team_Successfully": "Saiu do time com sucesso",
"Create_New": "Criar",
@ -875,7 +829,5 @@
"Call": "Ligar",
"Reply_in_direct_message": "Responder por mensagem direta",
"room_archived": "{{username}} arquivou a sala",
"room_unarchived": "{{username}} desarquivou a sala",
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace."
"room_unarchived": "{{username}} desarquivou a sala"
}

View File

@ -3,8 +3,7 @@ export const STATUS_COLORS: any = {
busy: '#f5455c',
away: '#ffd21f',
offline: '#cbced1',
loading: '#9ea2a8',
disabled: '#F38C39'
loading: '#9ea2a8'
};
export const SWITCH_TRACK_COLOR = {

View File

@ -1,4 +1,3 @@
// 🚨🚨 48 settings after login. Pay attention not to reach 50 as that's the limit per request.
export const defaultSettings = {
Accounts_AllowEmailChange: {
type: 'valueAsBoolean'
@ -230,8 +229,5 @@ export const defaultSettings = {
},
Number_of_users_autocomplete_suggestions: {
type: 'valueAsNumber'
},
Presence_broadcast_disabled: {
type: 'valueAsBoolean'
}
} as const;

View File

@ -8,6 +8,5 @@ export * from './localAuthentication';
export * from './localPath';
export * from './messagesStatus';
export * from './messageTypeLoad';
export * from './notifications';
export * from './defaultSettings';
export * from './tablet';

View File

@ -1 +0,0 @@
export const NOTIFICATION_PRESENCE_CAP = 'NOTIFICATION_PRESENCE_CAP';

View File

@ -0,0 +1,27 @@
import { useCallback } from 'react';
import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet';
import i18n from '../../i18n';
import { videoConfJoin } from '../methods/videoConf';
export const useVideoConf = (): { joinCall: (blockId: string) => void } => {
const { showActionSheet } = useActionSheet();
const joinCall = useCallback(blockId => {
const options: TActionSheetOptionsItem[] = [
{
title: i18n.t('Video_call'),
icon: 'camera',
onPress: () => videoConfJoin(blockId, true)
},
{
title: i18n.t('Voice_call'),
icon: 'microphone',
onPress: () => videoConfJoin(blockId, false)
}
];
showActionSheet({ options });
}, []);
return { joinCall };
};

View File

@ -1,113 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Q } from '@nozbe/watermelondb';
import { useActionSheet } from '../../containers/ActionSheet';
import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet';
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions';
import i18n from '../../i18n';
import { getUserSelector } from '../../selectors/login';
import database from '../database';
import { getSubscriptionByRoomId } from '../database/services/Subscription';
import { callJitsi } from '../methods';
import { compareServerVersion, showErrorAlert } from '../methods/helpers';
import { videoConfStartAndJoin } from '../methods/videoConf';
import { Services } from '../services';
import { useAppSelector } from './useAppSelector';
import { useSnaps } from './useSnaps';
const availabilityErrors = {
NOT_CONFIGURED: 'video-conf-provider-not-configured',
NOT_ACTIVE: 'no-active-video-conf-provider',
NO_APP: 'no-videoconf-provider-app'
} as const;
const handleErrors = (isAdmin: boolean, error: typeof availabilityErrors[keyof typeof availabilityErrors]) => {
if (isAdmin) return showErrorAlert(i18n.t(`admin-${error}-body`), i18n.t(`admin-${error}-header`));
return showErrorAlert(i18n.t(`${error}-body`), i18n.t(`${error}-header`));
};
export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Promise<void>; showCallOption: boolean } => {
const [showCallOption, setShowCallOption] = useState(false);
const serverVersion = useAppSelector(state => state.server.version);
const jitsiEnabled = useAppSelector(state => state.settings.Jitsi_Enabled);
const jitsiEnableTeams = useAppSelector(state => state.settings.Jitsi_Enable_Teams);
const jitsiEnableChannels = useAppSelector(state => state.settings.Jitsi_Enable_Channels);
const user = useAppSelector(state => getUserSelector(state));
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
const { showActionSheet } = useActionSheet();
const snaps = useSnaps([1250]);
const handleShowCallOption = (room: TSubscriptionModel) => {
if (isServer5OrNewer) return setShowCallOption(true);
const isJitsiDisabledForTeams = room.teamMain && !jitsiEnableTeams;
const isJitsiDisabledForChannels = !room.teamMain && (room.t === 'p' || room.t === 'c') && !jitsiEnableChannels;
if (room.t === SubscriptionType.DIRECT) return setShowCallOption(!!jitsiEnabled);
if (room.t === SubscriptionType.CHANNEL) return setShowCallOption(!isJitsiDisabledForChannels);
if (room.t === SubscriptionType.GROUP) return setShowCallOption(!isJitsiDisabledForTeams);
return setShowCallOption(false);
};
const canInitAnCall = async () => {
if (isServer5OrNewer) {
try {
await Services.videoConferenceGetCapabilities();
return true;
} catch (error: any) {
const isAdmin = !!['admin'].find(role => user.roles?.includes(role));
switch (error?.error) {
case availabilityErrors.NOT_CONFIGURED:
return handleErrors(isAdmin, availabilityErrors.NOT_CONFIGURED);
case availabilityErrors.NOT_ACTIVE:
return handleErrors(isAdmin, availabilityErrors.NOT_ACTIVE);
case availabilityErrors.NO_APP:
return handleErrors(isAdmin, availabilityErrors.NO_APP);
default:
return handleErrors(isAdmin, availabilityErrors.NOT_CONFIGURED);
}
}
}
return true;
};
const initCall = async ({ cam, mic }: { cam: boolean; mic: boolean }) => {
if (isServer5OrNewer) return videoConfStartAndJoin({ rid, cam, mic });
const room = (await getSubscriptionByRoomId(rid)) as ISubscription;
callJitsi({ room, cam });
};
const showInitCallActionSheet = async () => {
const canInit = await canInitAnCall();
if (canInit) {
showActionSheet({
children: <StartACallActionSheet rid={rid} initCall={initCall} />,
snaps
});
}
};
const initSubscription = () => {
try {
const db = database.active;
const observeSubCollection = db.get('subscriptions').query(Q.where('rid', rid)).observe();
const subObserveQuery = observeSubCollection.subscribe(data => {
if (data[0]) {
handleShowCallOption(data[0]);
subObserveQuery.unsubscribe();
}
});
} catch (e) {
console.log("observeSubscriptions: Can't find subscription to observe");
}
};
useEffect(() => {
initSubscription();
}, []);
return { showInitCallActionSheet, showCallOption };
};

View File

@ -52,7 +52,7 @@ export const deleteAllAudioFiles = async (serverUrl: string): Promise<void> => {
try {
const serverUrlParsed = sanitizeString(serverUrl);
const path = `${FileSystem.documentDirectory}audios/${serverUrlParsed}`;
await FileSystem.deleteAsync(path, { idempotent: true });
await FileSystem.deleteAsync(path);
} catch (error) {
log(error);
}

View File

@ -46,8 +46,8 @@ export function callJitsiWithoutServer(path: string): void {
Navigation.navigate('JitsiMeetView', { url, onlyAudio: false });
}
export async function callJitsi({ room, cam = false }: { room: ISubscription; cam?: boolean }): Promise<void> {
logEvent(cam ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
export async function callJitsi(room: ISubscription, onlyAudio = false): Promise<void> {
logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO);
const url = await jitsiURL({ room });
Navigation.navigate('JitsiMeetView', { url, onlyAudio: cam, rid: room?.rid });
Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid: room?.rid });
}

View File

@ -11,7 +11,6 @@ import database from '../database';
import sdk from '../services/sdk';
import protectedFunction from './helpers/protectedFunction';
import { parseSettings, _prepareSettings } from './parseSettings';
import { setPresenceCap } from './getUsersPresence';
const serverInfoKeys = [
'Site_Name',
@ -158,11 +157,8 @@ export async function getSettings(): Promise<void> {
const data: IData[] = result.settings || [];
const filteredSettings: IPreparedSettings[] = _prepareSettings(data);
const filteredSettingsIds = filteredSettings.map(s => s._id);
const parsedSettings = parseSettings(filteredSettings);
reduxStore.dispatch(addSettings(parsedSettings));
setPresenceCap(parsedSettings.Presence_broadcast_disabled);
reduxStore.dispatch(addSettings(parseSettings(filteredSettings)));
// filter server info
const serverInfo = filteredSettings.filter(i1 => serverInfoKeys.includes(i1._id));

View File

@ -9,9 +9,6 @@ import database from '../database';
import { IUser } from '../../definitions';
import sdk from '../services/sdk';
import { compareServerVersion } from './helpers';
import userPreferences from './userPreferences';
import { NOTIFICATION_PRESENCE_CAP } from '../constants';
import { setNotificationPresenceCap } from '../../actions/app';
export const _activeUsersSubTimeout: { activeUsersSubTimeout: boolean | ReturnType<typeof setTimeout> | number } = {
activeUsersSubTimeout: false
@ -127,16 +124,3 @@ export function getUserPresence(uid: string) {
usersBatch.push(uid);
}
}
export const setPresenceCap = async (enabled: boolean) => {
if (enabled) {
const notificationPresenceCap = await userPreferences.getBool(NOTIFICATION_PRESENCE_CAP);
if (notificationPresenceCap !== false) {
userPreferences.setBool(NOTIFICATION_PRESENCE_CAP, true);
reduxStore.dispatch(setNotificationPresenceCap(true));
}
} else {
userPreferences.removeItem(NOTIFICATION_PRESENCE_CAP);
reduxStore.dispatch(setNotificationPresenceCap(false));
}
};

View File

@ -1,7 +1,5 @@
import { Alert, Linking } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
// eslint-disable-next-line import/no-unresolved
import { RUNNING_E2E_TESTS } from '@env';
import I18n from '../../../i18n';
import { isFDroidBuild, STORE_REVIEW_LINK } from '../../constants';
@ -88,15 +86,14 @@ class ReviewApp {
positiveEventCount = 0;
pushPositiveEvent = () => {
if (isFDroidBuild || RUNNING_E2E_TESTS === 'true') {
return;
}
if (this.positiveEventCount >= numberOfPositiveEvent) {
return;
}
this.positiveEventCount += 1;
if (this.positiveEventCount === numberOfPositiveEvent) {
tryReview();
if (!isFDroidBuild) {
if (this.positiveEventCount >= numberOfPositiveEvent) {
return;
}
this.positiveEventCount += 1;
if (this.positiveEventCount === numberOfPositiveEvent) {
tryReview();
}
}
};
}

View File

@ -38,8 +38,7 @@ export const localSearchSubscription = async ({ text = '', filterUsers = true, f
t: item.t,
encrypted: item.encrypted,
lastMessage: item.lastMessage,
status: item.status,
teamMain: item.teamMain
status: item.status
})) as ISearchLocal[];
return search;

View File

@ -138,23 +138,6 @@ export default class RoomSubscription {
reduxStore.dispatch(removeUserTyping(name));
}
}
} else if (ev === 'user-activity') {
const { user } = reduxStore.getState().login;
const { UI_Use_Real_Name } = reduxStore.getState().settings;
const { subscribedRoom } = reduxStore.getState().room;
if (subscribedRoom !== _rid) {
return;
}
const [name, activities] = ddpMessage.fields.args;
const key = UI_Use_Real_Name ? 'name' : 'username';
if (name !== user[key]) {
if (activities.includes('user-typing')) {
reduxStore.dispatch(addUserTyping(name));
}
if (!activities.length) {
reduxStore.dispatch(removeUserTyping(name));
}
}
} else if (ev === 'deleteMessage') {
InteractionManager.runAfterInteractions(async () => {
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {

View File

@ -16,43 +16,42 @@ class UserPreferences {
getString(key: string): string | null {
try {
return this.mmkv.getString(key) ?? null;
return this.mmkv.getString(key) || null;
} catch {
return null;
}
}
setString(key: string, value: string): boolean | undefined {
return this.mmkv.setString(key, value) ?? undefined;
return this.mmkv.setString(key, value) || undefined;
}
getBool(key: string): boolean | null {
try {
console.log(this.mmkv.getBool(key));
return this.mmkv.getBool(key) ?? null;
return this.mmkv.getBool(key) || null;
} catch {
return null;
}
}
setBool(key: string, value: boolean): boolean | undefined {
return this.mmkv.setBool(key, value) ?? undefined;
return this.mmkv.setBool(key, value) || undefined;
}
getMap(key: string): object | null {
try {
return this.mmkv.getMap(key) ?? null;
return this.mmkv.getMap(key) || null;
} catch {
return null;
}
}
setMap(key: string, value: object): boolean | undefined {
return this.mmkv.setMap(key, value) ?? undefined;
return this.mmkv.setMap(key, value) || undefined;
}
removeItem(key: string): boolean | undefined {
return this.mmkv.removeItem(key) ?? undefined;
return this.mmkv.removeItem(key) || undefined;
}
}

View File

@ -19,9 +19,9 @@ const handleBltPermission = async (): Promise<Permission[]> => {
return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION];
};
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise<void> => {
export const videoConfJoin = async (callId: string, cam: boolean) => {
try {
const result = await Services.videoConferenceJoin(callId, cam, mic);
const result = await Services.videoConferenceJoin(callId, cam);
if (result.success) {
if (isAndroid) {
const bltPermission = await handleBltPermission();
@ -44,11 +44,11 @@ export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean
}
};
export const videoConfStartAndJoin = async ({ rid, cam, mic }: { rid: string; cam?: boolean; mic?: boolean }): Promise<void> => {
export const videoConfStartAndJoin = async (rid: string, cam: boolean) => {
try {
const videoConfResponse = await Services.videoConferenceStart(rid);
const videoConfResponse: any = await Services.videoConferenceStart(rid);
if (videoConfResponse.success) {
videoConfJoin(videoConfResponse.data.callId, cam, mic);
videoConfJoin(videoConfResponse.data.callId, cam);
}
} catch (e) {
showErrorAlert(i18n.t('error-init-video-conf'));

View File

@ -1,27 +0,0 @@
import BackgroundTimer from 'react-native-background-timer';
import { Services } from '../services';
let interval: number | null = null;
export const initVideoConfTimer = (rid: string): void => {
if (rid) {
Services.updateJitsiTimeout(rid).catch((e: unknown) => console.log(e));
if (interval) {
BackgroundTimer.clearInterval(interval);
BackgroundTimer.stopBackgroundTimer();
interval = null;
}
interval = BackgroundTimer.setInterval(() => {
Services.updateJitsiTimeout(rid).catch((e: unknown) => console.log(e));
}, 10000);
}
};
export const endVideoConfTimer = (): void => {
if (interval) {
BackgroundTimer.clearInterval(interval);
interval = null;
BackgroundTimer.stopBackgroundTimer();
}
};

View File

@ -20,16 +20,7 @@ import { updatePermission } from '../../actions/permissions';
import EventEmitter from '../methods/helpers/events';
import { updateSettings } from '../../actions/settings';
import { defaultSettings, MIN_ROCKETCHAT_VERSION } from '../constants';
import {
getSettings,
IActiveUsers,
unsubscribeRooms,
_activeUsers,
_setUser,
_setUserTimer,
onRolesChanged,
setPresenceCap
} from '../methods';
import { getSettings, IActiveUsers, unsubscribeRooms, _activeUsers, _setUser, _setUserTimer, onRolesChanged } from '../methods';
import { compareServerVersion, isIOS, isSsl } from '../methods/helpers';
interface IServices {
@ -153,10 +144,6 @@ function connect({ server, logoutOnError = false }: { server: string; logoutOnEr
});
}
store.dispatch(updateSettings(_id, value));
if (_id === 'Presence_broadcast_disabled') {
setPresenceCap(value);
}
} catch (e) {
log(e);
}

View File

@ -812,14 +812,10 @@ export const addUsersToRoom = (rid: string): Promise<boolean> => {
};
export const emitTyping = (room: IRoom, typing = true) => {
const { login, settings, server } = reduxStore.getState();
const { login, settings } = reduxStore.getState();
const { UI_Use_Real_Name } = settings;
const { version: serverVersion } = server;
const { user } = login;
const name = UI_Use_Real_Name ? user.name : user.username;
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '4.0.0')) {
return sdk.methodCall('stream-notify-room', `${room}/user-activity`, name, typing ? ['user-typing'] : []);
}
return sdk.methodCall('stream-notify-room', `${room}/typing`, name, typing);
};
@ -936,10 +932,8 @@ export function getUserInfo(userId: string) {
export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite });
export const videoConferenceJoin = (callId: string, cam?: boolean, mic?: boolean) =>
sdk.post('video-conference.join', { callId, state: { cam: !!cam, mic: mic === undefined ? true : mic } });
export const videoConferenceGetCapabilities = () => sdk.get('video-conference.capabilities');
export const videoConferenceJoin = (callId: string, cam: boolean) =>
sdk.post('video-conference.join', { callId, state: { cam } });
export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId });

View File

@ -6,7 +6,7 @@ import { twoFactor } from './twoFactor';
import { isSsl } from '../methods/helpers/url';
import { store as reduxStore } from '../store/auxStore';
import { Serialized, MatchPathPattern, OperationParams, PathFor, ResultFor } from '../../definitions/rest/helpers';
import { compareServerVersion, random } from '../methods/helpers';
import { random } from '../methods/helpers';
class Sdk {
private sdk: typeof Rocketchat;
@ -162,22 +162,7 @@ class Sdk {
}
subscribeRoom(...args: any[]) {
const { server } = reduxStore.getState();
const { version: serverVersion } = server;
const topic = 'stream-notify-room';
let eventUserTyping;
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '4.0.0')) {
eventUserTyping = this.subscribe(topic, `${args[0]}/user-activity`, ...args);
} else {
eventUserTyping = this.subscribe(topic, `${args[0]}/typing`, ...args);
}
// Taken from https://github.com/RocketChat/Rocket.Chat.js.SDK/blob/454b4ba784095057b8de862eb99340311b672e15/lib/drivers/ddp.ts#L555
return Promise.all([
this.subscribe('stream-room-messages', args[0], ...args),
eventUserTyping,
this.subscribe(topic, `${args[0]}/deleteMessage`, ...args)
]);
return this.current.subscribeRoom(...args);
}
unsubscribe(subscription: any[]) {

View File

@ -1,4 +1,4 @@
import { appStart, appInit, setMasterDetail, setNotificationPresenceCap, appReady } from '../actions/app';
import { appStart, appInit, setMasterDetail } from '../actions/app';
import { initialState } from './app';
import { mockedStore } from './mockedStore';
import { RootEnum } from '../definitions';
@ -20,9 +20,6 @@ describe('test reducer', () => {
mockedStore.dispatch(appInit());
const { ready } = mockedStore.getState().app;
expect(ready).toEqual(false);
mockedStore.dispatch(appReady());
const { ready: ready2 } = mockedStore.getState().app;
expect(ready2).toEqual(true);
});
it('should return ready state after dispatch setMasterDetail action', () => {
@ -44,13 +41,4 @@ describe('test reducer', () => {
expect(foreground).toEqual(false);
expect(background).toEqual(true);
});
it('should return correct state after dispatch setNotificationPresenceCap action', () => {
mockedStore.dispatch(setNotificationPresenceCap(true));
const { notificationPresenceCap } = mockedStore.getState().app;
expect(notificationPresenceCap).toEqual(true);
mockedStore.dispatch(setNotificationPresenceCap(false));
const { notificationPresenceCap: notificationPresenceCap2 } = mockedStore.getState().app;
expect(notificationPresenceCap2).toEqual(false);
});
});

View File

@ -9,7 +9,6 @@ export interface IApp {
ready: boolean;
foreground: boolean;
background: boolean;
notificationPresenceCap: boolean;
}
export const initialState: IApp = {
@ -18,8 +17,7 @@ export const initialState: IApp = {
text: undefined,
ready: false,
foreground: true,
background: false,
notificationPresenceCap: false
background: false
};
export default function app(state = initialState, action: TActionApp): IApp {
@ -57,11 +55,6 @@ export default function app(state = initialState, action: TActionApp): IApp {
...state,
isMasterDetail: action.isMasterDetail
};
case APP.SET_NOTIFICATION_PRESENCE_CAP:
return {
...state,
notificationPresenceCap: action.show
};
default:
return state;
}

View File

@ -105,7 +105,11 @@ const ChatsStackNavigator = () => {
/>
<ChatsStack.Screen name='SelectedUsersView' component={SelectedUsersView} />
<ChatsStack.Screen name='InviteUsersView' component={InviteUsersView} />
<ChatsStack.Screen name='InviteUsersEditView' component={InviteUsersEditView} />
<ChatsStack.Screen
name='InviteUsersEditView'
component={InviteUsersEditView}
options={InviteUsersEditView.navigationOptions}
/>
<ChatsStack.Screen name='MessagesView' component={MessagesView} />
<ChatsStack.Screen name='AutoTranslateView' component={AutoTranslateView} options={AutoTranslateView.navigationOptions} />
<ChatsStack.Screen name='DirectoryView' component={DirectoryView} options={DirectoryView.navigationOptions} />

View File

@ -142,7 +142,11 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
component={AddExistingChannelView}
options={AddExistingChannelView.navigationOptions}
/>
<ModalStack.Screen name='InviteUsersEditView' component={InviteUsersEditView} />
<ModalStack.Screen
name='InviteUsersEditView'
component={InviteUsersEditView}
options={InviteUsersEditView.navigationOptions}
/>
<ModalStack.Screen name='MessagesView' component={MessagesView} />
<ModalStack.Screen name='AutoTranslateView' component={AutoTranslateView} options={AutoTranslateView.navigationOptions} />
<ModalStack.Screen

View File

@ -94,7 +94,6 @@ export type ChatsStackParamList = {
showButton?: boolean;
title?: string;
buttonText?: string;
showSkipText?: boolean;
nextAction?(): void;
};
InviteUsersView: {

View File

@ -243,7 +243,7 @@ class DirectoryView extends React.Component<IDirectoryViewProps, IDirectoryViewS
title: item.name as string,
onPress: () => this.onPressItem(item),
baseUrl,
testID: `directory-view-item-${item.name}`,
testID: `directory-view-item-${item.name}`.toLowerCase(),
style,
user,
theme,

View File

@ -1,92 +0,0 @@
import React from 'react';
import { TextInputProps } from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
import { useDispatch } from 'react-redux';
import { inviteLinksSetParams } from '../../actions/inviteLinks';
import { useTheme } from '../../theme';
import { useAppSelector } from '../../lib/hooks';
import I18n from '../../i18n';
import styles from './styles';
import { events, logEvent } from '../../lib/methods/helpers/log';
const OPTIONS = {
days: [
{
label: '1',
value: 1
},
{
label: '7',
value: 7
},
{
label: '15',
value: 15
},
{
label: '30',
value: 30
}
],
maxUses: [
{
label: '1',
value: 1
},
{
label: '5',
value: 5
},
{
label: '10',
value: 10
},
{
label: '25',
value: 25
},
{
label: '50',
value: 50
},
{
label: '100',
value: 100
}
]
};
const Picker = ({ param, first }: { param: 'days' | 'maxUses'; first: string }): JSX.Element => {
const { colors } = useTheme();
const inviteLinkParam = useAppSelector(state => state.inviteLinks[param]);
const dispatch = useDispatch();
const onValueChangePicker = (value: number) => {
logEvent(events.IU_EDIT_SET_LINK_PARAM);
const params = {
[param]: value
};
dispatch(inviteLinksSetParams(params));
};
const textInputStyle: TextInputProps = { style: { ...styles.pickerText, color: colors.actionTintColor } };
const firstEl = [
{
label: I18n.t(first),
value: 0
}
];
return (
<RNPickerSelect
style={{ viewContainer: styles.viewContainer }}
value={inviteLinkParam}
textInputProps={textInputStyle}
useNativeAndroidPickerStyle={false}
onValueChange={value => onValueChangePicker(value)}
items={firstEl.concat(OPTIONS[param])}
/>
);
};
export default Picker;

View File

@ -1,54 +1,149 @@
import { StackNavigationProp } from '@react-navigation/stack';
import React, { useLayoutEffect } from 'react';
import { View } from 'react-native';
import { useDispatch } from 'react-redux';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { StackNavigationOptions } from '@react-navigation/stack';
import React from 'react';
import { TextInputProps, View } from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
import { connect } from 'react-redux';
import { inviteLinksCreate } from '../../actions/inviteLinks';
import { inviteLinksCreate, inviteLinksSetParams } from '../../actions/inviteLinks';
import { themes } from '../../lib/constants';
import Button from '../../containers/Button';
import * as List from '../../containers/List';
import SafeAreaView from '../../containers/SafeAreaView';
import StatusBar from '../../containers/StatusBar';
import { IApplicationState, IBaseScreen } from '../../definitions';
import I18n from '../../i18n';
import { ChatsStackParamList } from '../../stacks/types';
import { withTheme } from '../../theme';
import { events, logEvent } from '../../lib/methods/helpers/log';
import styles from './styles';
import Picker from './Picker';
const InviteUsersEditView = () => {
const navigation = useNavigation<StackNavigationProp<ChatsStackParamList, 'InviteUsersEditView'>>();
const { rid } = useRoute<RouteProp<ChatsStackParamList, 'InviteUsersEditView'>>().params;
const dispatch = useDispatch();
const OPTIONS = {
days: [
{
label: '1',
value: 1
},
{
label: '7',
value: 7
},
{
label: '15',
value: 15
},
{
label: '30',
value: 30
}
],
maxUses: [
{
label: '1',
value: 1
},
{
label: '5',
value: 5
},
{
label: '10',
value: 10
},
{
label: '25',
value: 25
},
{
label: '50',
value: 50
},
{
label: '100',
value: 100
}
]
};
useLayoutEffect(() => {
navigation.setOptions({
title: I18n.t('Invite_users')
});
}, [navigation]);
interface IInviteUsersEditViewProps extends IBaseScreen<ChatsStackParamList, 'InviteUsersEditView'> {
days: number;
maxUses: number;
}
const createInviteLink = () => {
class InviteUsersEditView extends React.Component<IInviteUsersEditViewProps, any> {
static navigationOptions = (): StackNavigationOptions => ({
title: I18n.t('Invite_users')
});
private rid: string;
constructor(props: IInviteUsersEditViewProps) {
super(props);
this.rid = props.route.params?.rid;
}
onValueChangePicker = (key: string, value: number) => {
const { dispatch } = this.props;
logEvent(events.IU_EDIT_SET_LINK_PARAM);
const params = {
[key]: value
};
dispatch(inviteLinksSetParams(params));
};
createInviteLink = () => {
const { dispatch, navigation } = this.props;
logEvent(events.IU_EDIT_CREATE_LINK);
dispatch(inviteLinksCreate(rid));
dispatch(inviteLinksCreate(this.rid));
navigation.pop();
};
return (
<SafeAreaView>
<List.Container>
<StatusBar />
<List.Section>
<List.Separator />
<List.Item title='Expiration_Days' right={() => <Picker param={'days'} first={'Never'} />} />
<List.Separator />
<List.Item title='Max_number_of_uses' right={() => <Picker param='maxUses' first='No_limit' />} />
<List.Separator />
</List.Section>
<View style={styles.innerContainer}>
<Button title={I18n.t('Generate_New_Link')} type='primary' onPress={createInviteLink} />
</View>
</List.Container>
</SafeAreaView>
);
};
renderPicker = (key: 'days' | 'maxUses', first: string) => {
const { props } = this;
const { theme } = props;
const textInputStyle: TextInputProps = { style: { ...styles.pickerText, color: themes[theme].actionTintColor } };
const firstEl = [
{
label: I18n.t(first),
value: 0
}
];
return (
<RNPickerSelect
style={{ viewContainer: styles.viewContainer }}
value={props[key]}
textInputProps={textInputStyle}
useNativeAndroidPickerStyle={false}
placeholder={{}}
onValueChange={value => this.onValueChangePicker(key, value)}
items={firstEl.concat(OPTIONS[key])}
/>
);
};
export default InviteUsersEditView;
render() {
return (
<SafeAreaView>
<List.Container>
<StatusBar />
<List.Section>
<List.Separator />
<List.Item title='Expiration_Days' right={() => this.renderPicker('days', 'Never')} />
<List.Separator />
<List.Item title='Max_number_of_uses' right={() => this.renderPicker('maxUses', 'No_limit')} />
<List.Separator />
</List.Section>
<View style={styles.innerContainer}>
<Button title={I18n.t('Generate_New_Link')} type='primary' onPress={this.createInviteLink} />
</View>
</List.Container>
</SafeAreaView>
);
}
}
const mapStateToProps = (state: IApplicationState) => ({
days: state.inviteLinks.days,
maxUses: state.inviteLinks.maxUses
});
export default connect(mapStateToProps)(withTheme(InviteUsersEditView));

View File

@ -1,13 +1,14 @@
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import React from 'react';
import { BackHandler, NativeEventSubscription } from 'react-native';
import BackgroundTimer from 'react-native-background-timer';
import { isAppInstalled, openAppWithUri } from 'react-native-send-intent';
import WebView from 'react-native-webview';
import { WebViewMessage, WebViewNavigation } from 'react-native-webview/lib/WebViewTypes';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import { IBaseScreen } from '../definitions';
import { events, logEvent } from '../lib/methods/helpers/log';
import { endVideoConfTimer, initVideoConfTimer } from '../lib/methods/videoConfTimer';
import { Services } from '../lib/services';
import { ChatsStackParamList } from '../stacks/types';
import { withTheme } from '../theme';
@ -19,6 +20,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
private rid: string;
private url: string;
private videoConf: boolean;
private jitsiTimeout: number | null;
private backHandler!: NativeEventSubscription;
constructor(props: TJitsiMeetViewProps) {
@ -26,6 +28,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
this.rid = props.route.params?.rid;
this.url = props.route.params?.url;
this.videoConf = !!props.route.params?.videoConf;
this.jitsiTimeout = null;
}
componentDidMount() {
@ -47,8 +50,10 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
componentWillUnmount() {
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
if (!this.videoConf) {
endVideoConfTimer();
if (this.jitsiTimeout && !this.videoConf) {
BackgroundTimer.clearInterval(this.jitsiTimeout);
this.jitsiTimeout = null;
BackgroundTimer.stopBackgroundTimer();
}
this.backHandler.remove();
deactivateKeepAwake();
@ -59,7 +64,15 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
onConferenceJoined = () => {
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN);
if (this.rid && !this.videoConf) {
initVideoConfTimer(this.rid);
Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
if (this.jitsiTimeout) {
BackgroundTimer.clearInterval(this.jitsiTimeout);
BackgroundTimer.stopBackgroundTimer();
this.jitsiTimeout = null;
}
this.jitsiTimeout = BackgroundTimer.setInterval(() => {
Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
}, 10000);
}
};
@ -77,7 +90,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
render() {
return (
<WebView
source={{ uri: `${this.url}${this.url.includes('#config') ? '&' : '#'}config.disableDeepLinking=true` }}
source={{ uri: `${this.url}&config.disableDeepLinking=true` }}
onMessage={({ nativeEvent }) => this.onNavigationStateChange(nativeEvent)}
onNavigationStateChange={this.onNavigationStateChange}
style={{ flex: 1 }}

View File

@ -9,7 +9,6 @@ import { useAppSelector } from '../lib/hooks';
import { events, logEvent } from '../lib/methods/helpers/log';
import { getUserSelector } from '../selectors/login';
import { ChatsStackParamList } from '../stacks/types';
import { endVideoConfTimer, initVideoConfTimer } from '../lib/methods/videoConfTimer';
const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLFragment: string) =>
`${baseUrl}/avatar/${url}?format=png&width=${uriSize}&height=${uriSize}${avatarAuthURLFragment}`;
@ -17,7 +16,7 @@ const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLF
const JitsiMeetView = (): React.ReactElement => {
const { goBack } = useNavigation();
const {
params: { url, onlyAudio, videoConf, rid }
params: { url, onlyAudio, videoConf }
} = useRoute<RouteProp<ChatsStackParamList, 'JitsiMeetView'>>();
const user = useAppSelector(state => getUserSelector(state));
const baseUrl = useAppSelector(state => state.server.server);
@ -39,8 +38,8 @@ const JitsiMeetView = (): React.ReactElement => {
const regex = /(?:\/.*\/)(.*)/;
const urlWithoutServer = regex.exec(url)![1];
const serverUrl = url.replace(`/${urlWithoutServer}`, '');
const room = (url.includes('jwt=') ? urlWithoutServer.split('jwt=')[0] : urlWithoutServer.split('#')[0]).replace('?', '');
const jwtToken = url.includes('jwt=') ? url.substring(url.indexOf('jwt=') + 4, url.lastIndexOf('#config')) : undefined;
const room = urlWithoutServer.split('#')[0];
const conferenceOptions = {
room,
serverUrl,
@ -52,19 +51,24 @@ const JitsiMeetView = (): React.ReactElement => {
audioOnly,
audioMuted: false,
videoMuted: audioOnly,
token: jwtToken,
featureFlags: {
'calendar.enabled': false
'live-streaming.enabled': false,
'calendar.enabled': false,
'call-integration.enabled': false,
'pip.enabled': false,
'invite.enabled': false,
'welcomepage.enabled': false,
'add-people.enabled': false
},
configOverrides: {
'breakoutRooms.hideAddRoomButton': false
'breakoutRooms.hideAddRoomButton': false,
'breakoutRooms.hideAutoAssignButton': false,
'breakoutRooms.hideJoinRoomButton': false
}
};
logEvent(videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN);
if (!videoConf) initVideoConfTimer(rid);
await JitsiMeet.launchJitsiMeetView(conferenceOptions);
logEvent(videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
if (!videoConf) endVideoConfTimer();
goBack();
};

View File

@ -1,25 +0,0 @@
import React from 'react';
import * as List from '../../../containers/List';
import i18n from '../../../i18n';
import { useVideoConf } from '../../../lib/hooks/useVideoConf';
export default function CallSection({ rid }: { rid: string }): React.ReactElement | null {
const { showCallOption, showInitCallActionSheet } = useVideoConf(rid);
if (showCallOption)
return (
<List.Section>
<List.Separator />
<List.Item
title={i18n.t('Call')}
onPress={showInitCallActionSheet}
testID='room-actions-call'
left={() => <List.Icon name='phone' />}
showActionIndicator
/>
<List.Separator />
</List.Section>
);
return null;
}

View File

@ -33,7 +33,7 @@ import sharedStyles from '../Styles';
import styles from './styles';
import { ERoomType } from '../../definitions/ERoomType';
import { E2E_ROOM_TYPES, SWITCH_TRACK_COLOR, themes } from '../../lib/constants';
import { getPermalinkChannel } from '../../lib/methods';
import { callJitsi, getPermalinkChannel } from '../../lib/methods';
import {
canAutoTranslate as canAutoTranslateMethod,
getRoomAvatar,
@ -48,9 +48,9 @@ import { getSubscriptionByRoomId } from '../../lib/database/services/Subscriptio
import { IActionSheetProvider, withActionSheet } from '../../containers/ActionSheet';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { closeLivechat } from '../../lib/methods/helpers/closeLivechat';
import { videoConfStartAndJoin } from '../../lib/methods/videoConf';
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
import { ILivechatTag } from '../../definitions/ILivechatTag';
import CallSection from './components/CallSection';
interface IOnPressTouch {
<T extends keyof ChatsStackParamList>(item: { route?: T; params?: ChatsStackParamList[T]; event?: Function }): void;
@ -730,6 +730,16 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
}
};
startVideoConf = ({ video }: { video: boolean }): void => {
const { room } = this.state;
const { serverVersion } = this.props;
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0')) {
videoConfStartAndJoin(room.rid, video);
} else {
callJitsi(room, !video);
}
};
renderRoomInfo = () => {
const { room, member } = this.state;
const { rid, name, t, topic, source } = room;
@ -805,6 +815,63 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
);
};
renderJitsi = () => {
const { room } = this.state;
const {
jitsiEnabled,
jitsiEnableTeams,
jitsiEnableChannels,
serverVersion,
videoConf_Enable_DMs,
videoConf_Enable_Channels,
videoConf_Enable_Groups,
videoConf_Enable_Teams
} = this.props;
const isJitsiDisabledForTeams = room.teamMain && !jitsiEnableTeams;
const isJitsiDisabledForChannels = !room.teamMain && (room.t === 'p' || room.t === 'c') && !jitsiEnableChannels;
const isVideoConfDisabledForTeams = !!room.teamMain && !videoConf_Enable_Teams;
const isVideoConfDisabledForChannels = !room.teamMain && room.t === 'c' && !videoConf_Enable_Channels;
const isVideoConfDisabledForGroups = !room.teamMain && room.t === 'p' && !videoConf_Enable_Groups;
const isVideoConfDisabledForDirect = !room.teamMain && room.t === 'd' && !videoConf_Enable_DMs;
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0')) {
if (
isVideoConfDisabledForTeams ||
isVideoConfDisabledForChannels ||
isVideoConfDisabledForGroups ||
isVideoConfDisabledForDirect
) {
return null;
}
} else if (!jitsiEnabled || isJitsiDisabledForTeams || isJitsiDisabledForChannels) {
return null;
}
return (
<List.Section>
<List.Separator />
<List.Item
title='Voice_call'
onPress={() => this.startVideoConf({ video: false })}
testID='room-actions-voice'
left={() => <List.Icon name='phone' />}
showActionIndicator
/>
<List.Separator />
<List.Item
title='Video_call'
onPress={() => this.startVideoConf({ video: true })}
testID='room-actions-video'
left={() => <List.Icon name='camera' />}
showActionIndicator
/>
<List.Separator />
</List.Section>
);
};
renderE2EEncryption = () => {
const { room } = this.state;
const { encryptionEnabled } = this.props;
@ -1041,7 +1108,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
<StatusBar />
<List.Container testID='room-actions-scrollview'>
{this.renderRoomInfo()}
<CallSection rid={rid} />
{this.renderJitsi()}
{this.renderE2EEncryption()}
<List.Section>
<List.Separator />
@ -1232,6 +1299,13 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
const mapStateToProps = (state: IApplicationState) => ({
userId: getUserSelector(state).id,
jitsiEnabled: (state.settings.Jitsi_Enabled || false) as boolean,
jitsiEnableTeams: (state.settings.Jitsi_Enable_Teams || false) as boolean,
jitsiEnableChannels: (state.settings.Jitsi_Enable_Channels || false) as boolean,
videoConf_Enable_DMs: (state.settings.VideoConf_Enable_DMs ?? true) as boolean,
videoConf_Enable_Channels: (state.settings.VideoConf_Enable_Channels ?? true) as boolean,
videoConf_Enable_Groups: (state.settings.VideoConf_Enable_Groups ?? true) as boolean,
videoConf_Enable_Teams: (state.settings.VideoConf_Enable_Teams ?? true) as boolean,
encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version,
isMasterDetail: state.app.isMasterDetail,

View File

@ -655,6 +655,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
label={I18n.t('Password')}
value={joinCode}
onChangeText={value => this.setState({ joinCode: value })}
onSubmitEditing={this.submit}
secureTextEntry
testID='room-info-edit-view-password'
/>

View File

@ -1,48 +0,0 @@
import React from 'react';
import { Text } from 'react-native';
import { BorderlessButton } from 'react-native-gesture-handler';
import { CustomIcon, TIconsName } from '../../../containers/CustomIcon';
import styles from '../styles';
import { useTheme } from '../../../theme';
import { useVideoConf } from '../../../lib/hooks/useVideoConf';
import i18n from '../../../i18n';
import { useAppSelector } from '../../../lib/hooks';
import { compareServerVersion } from '../../../lib/methods/helpers';
// TODO: change other icons on future
function UserInfoButton({
danger,
iconName,
onPress,
label,
showIcon
}: {
danger?: boolean;
iconName: TIconsName;
onPress?: (prop: any) => void;
label: string;
showIcon?: boolean;
}): React.ReactElement | null {
const { colors } = useTheme();
const color = danger ? colors.dangerColor : colors.actionTintColor;
if (showIcon)
return (
<BorderlessButton testID={`room-info-view-${iconName}`} onPress={onPress} style={styles.roomButton}>
<CustomIcon name={iconName} size={30} color={color} />
<Text style={[styles.roomButtonText, { color }]}>{label}</Text>
</BorderlessButton>
);
return null;
}
export function CallButton({ rid, isDirect }: { rid: string; isDirect: boolean }): React.ReactElement | null {
const { showCallOption, showInitCallActionSheet } = useVideoConf(rid);
const serverVersion = useAppSelector(state => state.server.version);
const greaterThanFive = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
const showIcon = greaterThanFive ? showCallOption : showCallOption && isDirect;
return <UserInfoButton onPress={showInitCallActionSheet} iconName='phone' label={i18n.t('Call')} showIcon={showIcon} />;
}

View File

@ -1,43 +1,43 @@
import { CompositeNavigationProp, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import isEmpty from 'lodash/isEmpty';
import React from 'react';
import { ScrollView, Text, View } from 'react-native';
import { BorderlessButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux';
import { Observable, Subscription } from 'rxjs';
import UAParser from 'ua-parser-js';
import isEmpty from 'lodash/isEmpty';
import { StackNavigationProp } from '@react-navigation/stack';
import { CompositeNavigationProp, RouteProp } from '@react-navigation/native';
import { Observable, Subscription } from 'rxjs';
import Avatar from '../../containers/Avatar';
import { CustomIcon, TIconsName } from '../../containers/CustomIcon';
import * as HeaderButton from '../../containers/HeaderButton';
import { MarkdownPreview } from '../../containers/markdown';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import SafeAreaView from '../../containers/SafeAreaView';
import Status from '../../containers/Status';
import StatusBar from '../../containers/StatusBar';
import { LISTENER } from '../../containers/Toast';
import { IApplicationState, ISubscription, IUser, SubscriptionType, TSubscriptionModel } from '../../definitions';
import { ILivechatVisitor } from '../../definitions/ILivechatVisitor';
import I18n from '../../i18n';
import { themes } from '../../lib/constants';
import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription';
import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers';
import EventEmitter from '../../lib/methods/helpers/events';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import { handleIgnore } from '../../lib/methods/helpers/handleIgnore';
import log, { events, logEvent } from '../../lib/methods/helpers/log';
import Navigation from '../../lib/navigation/appNavigation';
import { Services } from '../../lib/services';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { ChatsStackParamList } from '../../stacks/types';
import { TSupportedThemes, withTheme } from '../../theme';
import Avatar from '../../containers/Avatar';
import sharedStyles from '../Styles';
import Channel from './Channel';
import { CallButton } from './components/UserInfoButton';
import Direct from './Direct';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n';
import * as HeaderButton from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
import log, { events, logEvent } from '../../lib/methods/helpers/log';
import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme';
import { MarkdownPreview } from '../../containers/markdown';
import { LISTENER } from '../../containers/Toast';
import EventEmitter from '../../lib/methods/helpers/events';
import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import Navigation from '../../lib/navigation/appNavigation';
import Livechat from './Livechat';
import Channel from './Channel';
import Direct from './Direct';
import styles from './styles';
import { ChatsStackParamList } from '../../stacks/types';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { SubscriptionType, TSubscriptionModel, ISubscription, IUser, IApplicationState } from '../../definitions';
import { ILivechatVisitor } from '../../definitions/ILivechatVisitor';
import { callJitsi } from '../../lib/methods';
import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription';
import { handleIgnore } from '../../lib/methods/helpers/handleIgnore';
interface IGetRoomTitle {
room: ISubscription;
@ -386,6 +386,11 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
}
};
videoCall = () => {
const { room } = this.state;
callJitsi(room);
};
handleBlockUser = async (rid: string, blocked: string, block: boolean) => {
logEvent(events.RI_TOGGLE_BLOCK_USER);
try {
@ -420,7 +425,8 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
};
renderButtons = () => {
const { roomFromRid, roomUser, room } = this.state;
const { roomFromRid, roomUser } = this.state;
const { jitsiEnabled } = this.props;
const isFromDm = roomFromRid?.rid ? new RegExp(roomUser._id).test(roomFromRid.rid) : false;
const isDirectFromSaved = this.isDirect && this.fromRid && roomFromRid;
@ -436,7 +442,9 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
return (
<View style={styles.roomButtonsContainer}>
{this.renderButton(() => this.handleCreateDirectMessage(this.goRoom), 'message', I18n.t('Message'))}
<CallButton isDirect={this.isDirect} rid={room.rid} />
{jitsiEnabled && this.isDirect
? this.renderButton(() => this.handleCreateDirectMessage(this.videoCall), 'camera', I18n.t('Video_call'))
: null}
{isDirectFromSaved && !isFromDm && !isDmWithMyself
? this.renderButton(
() => handleIgnore(roomUser._id, !isIgnored, roomFromRid.rid),

View File

@ -75,8 +75,7 @@ export default function ActionsSection({ rid, t, joined }: IActionsSection): Rea
route: 'SelectedUsersView',
params: {
title: i18n.t('Add_users'),
nextAction: addUser,
showSkipText: false
nextAction: addUser
}
})
}

View File

@ -10,7 +10,8 @@ import * as HeaderButton from '../../containers/HeaderButton';
import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login';
import { events, logEvent } from '../../lib/methods/helpers/log';
import { IApplicationState, ISubscription, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions';
import { isTeamRoom } from '../../lib/methods/helpers/room';
import { IApplicationState, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions';
import { ChatsStackParamList } from '../../stacks/types';
import { TActionSheetOptionsItem } from '../../containers/ActionSheet';
import i18n from '../../i18n';
@ -19,11 +20,12 @@ import { onHoldLivechat, returnLivechat } from '../../lib/services/restApi';
import { closeLivechat as closeLivechatService } from '../../lib/methods/helpers/closeLivechat';
import { Services } from '../../lib/services';
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
import HeaderCallButton from './components/HeaderCallButton';
interface IRightButtonsProps extends Pick<ISubscription, 't'> {
interface IRightButtonsProps {
userId?: string;
threadsEnabled: boolean;
rid?: string;
t: string;
tmid?: string;
teamId?: string;
isMasterDetail: boolean;
@ -41,7 +43,6 @@ interface IRightButtonsProps extends Pick<ISubscription, 't'> {
livechatRequestComment: boolean;
showActionSheet: Function;
departmentId?: string;
rid?: string;
}
interface IRigthButtonsState {
@ -337,7 +338,7 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
render() {
const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state;
const { t, tmid, threadsEnabled, rid } = this.props;
const { t, tmid, threadsEnabled, teamId, joined } = this.props;
if (t === 'l') {
if (!this.isOmnichannelPreview()) {
@ -362,13 +363,15 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
}
return (
<HeaderButton.Container>
{rid ? <HeaderCallButton rid={rid} /> : null}
{isTeamRoom({ teamId, joined }) ? (
<HeaderButton.Item iconName='channel-public' onPress={this.goTeamChannels} testID='room-view-header-team-channels' />
) : null}
{threadsEnabled ? (
<HeaderButton.Item
iconName='threads'
onPress={this.goThreadsView}
testID='room-view-header-threads'
badge={() => <HeaderButton.BadgeUnread tunread={tunread} tunreadUser={tunreadUser} tunreadGroup={tunreadGroup} />}
badge={() => <HeaderButton.Badge tunread={tunread} tunreadUser={tunreadUser} tunreadGroup={tunreadGroup} />}
/>
) : null}
<HeaderButton.Item iconName='search' onPress={this.goSearchView} testID='room-view-search' />

View File

@ -1,12 +0,0 @@
import React from 'react';
import * as HeaderButton from '../../../containers/HeaderButton';
import { useVideoConf } from '../../../lib/hooks/useVideoConf';
export default function HeaderCallButton({ rid }: { rid: string }): React.ReactElement | null {
const { showInitCallActionSheet, showCallOption } = useVideoConf(rid);
if (showCallOption)
return <HeaderButton.Item iconName='phone' onPress={showInitCallActionSheet} testID='room-view-header-call' />;
return null;
}

View File

@ -627,7 +627,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
joined={joined}
status={room.status}
omnichannelPermissions={omnichannelPermissions}
t={(this.t || t) as SubscriptionType}
t={this.t || t}
encrypted={encrypted}
navigation={navigation}
toggleFollowThread={this.toggleFollowThread}
@ -785,22 +785,13 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
});
};
handleCloseEmoji = (action?: Function, params?: any) => {
if (this.messagebox?.current) {
return this.messagebox?.current.closeEmojiAndAction(action, params);
}
if (action) {
return action(params);
}
};
errorActionsShow = (message: TAnyMessageModel) => {
this.handleCloseEmoji(this.messageErrorActions?.showMessageErrorActions, message);
this.messagebox?.current?.closeEmojiAndAction(this.messageErrorActions?.showMessageErrorActions, message);
};
showActionSheet = (options: any) => {
const { showActionSheet } = this.props;
this.handleCloseEmoji(showActionSheet, options);
this.messagebox?.current?.closeEmojiAndAction(showActionSheet, options);
};
onEditInit = (message: TAnyMessageModel) => {
@ -859,7 +850,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
};
onReactionInit = (message: TAnyMessageModel) => {
this.handleCloseEmoji(() => {
this.messagebox?.current?.closeEmojiAndAction(() => {
this.setState({ selectedMessage: message }, this.showReactionPicker);
});
};
@ -874,7 +865,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
if (message.tmid && !this.tmid) {
return;
}
this.handleCloseEmoji(this.messageActions?.showMessageActions, message);
this.messagebox?.current?.closeEmojiAndAction(this.messageActions?.showMessageActions, message);
};
showAttachment = (attachment: IAttachment) => {
@ -903,7 +894,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
this.setState({ selectedMessage: message });
const { showActionSheet } = this.props;
const { selectedMessage } = this.state;
this.handleCloseEmoji(showActionSheet, {
this.messagebox?.current?.closeEmojiAndAction(showActionSheet, {
children: <ReactionsList reactions={selectedMessage?.reactions} getCustomEmoji={this.getCustomEmoji} />,
snaps: ['50%', '80%'],
enableContentPanningGesture: false
@ -1197,11 +1188,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
if ('id' in item && item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
name = I18n.t('Encrypted_message');
}
if (!jumpToMessageId) {
setTimeout(() => {
sendLoadingEvent({ visible: false });
}, 300);
}
return navigation.push('RoomView', {
rid: this.rid,
tmid: item.tmid,
@ -1234,15 +1220,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
});
};
// OLD METHOD - support versions before 5.0.0
handleEnterCall = () => {
handleCallJitsi = () => {
const { room } = this.state;
if ('id' in room) {
const { jitsiTimeout } = room;
if (jitsiTimeout && jitsiTimeout < new Date()) {
showErrorAlert(I18n.t('Call_already_ended'));
} else {
callJitsi({ room });
callJitsi(room);
}
}
};
@ -1388,14 +1373,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}
navToRoomInfo={this.navToRoomInfo}
getCustomEmoji={this.getCustomEmoji}
handleEnterCall={this.handleEnterCall}
callJitsi={this.handleCallJitsi}
blockAction={this.blockAction}
threadBadgeColor={this.getBadgeColor(item?.id)}
toggleFollowThread={this.toggleFollowThread}
jumpToMessage={this.jumpToMessageByUrl}
highlighted={highlightedMessage === item.id}
theme={theme}
closeEmojiAndAction={this.handleCloseEmoji}
closeEmojiAndAction={this.messagebox?.current?.closeEmojiAndAction}
/>
);
}

View File

@ -86,7 +86,6 @@ interface IRoomsListViewProps {
StoreLastMessage: boolean;
useRealName: boolean;
isMasterDetail: boolean;
notificationPresenceCap: boolean;
subscribedRoom: string;
width: number;
insets: {
@ -147,7 +146,6 @@ const shouldUpdateProps = [
'StoreLastMessage',
'theme',
'isMasterDetail',
'notificationPresenceCap',
'refreshing',
'queueSize',
'inquiryEnabled',
@ -257,18 +255,21 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
shouldComponentUpdate(nextProps: IRoomsListViewProps, nextState: IRoomsListViewState) {
const { chatsUpdate, searching, item, canCreateRoom, omnichannelsUpdate } = this.state;
// eslint-disable-next-line react/destructuring-assignment
const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]);
if (propsUpdated) {
return true;
}
// check if some display props are changed to force update when focus this view again
// eslint-disable-next-line react/destructuring-assignment
const displayUpdated = displayPropsShouldUpdate.some(key => nextProps[key] !== this.props[key]);
if (displayUpdated) {
this.shouldUpdate = true;
}
// check if some sort preferences are changed to getSubscription() when focus this view again
// eslint-disable-next-line react/destructuring-assignment
const sortPreferencesUpdate = sortPreferencesShouldUpdate.some(key => nextProps[key] !== this.props[key]);
if (sortPreferencesUpdate) {
this.sortPreferencesChanged = true;
@ -338,7 +339,6 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
showUnread,
subscribedRoom,
isMasterDetail,
notificationPresenceCap,
insets,
createTeamPermission,
createPublicChannelPermission,
@ -366,11 +366,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
if (isMasterDetail && item?.rid !== subscribedRoom && subscribedRoom !== prevProps.subscribedRoom) {
this.setState({ item: { rid: subscribedRoom } as ISubscription });
}
if (
insets.left !== prevProps.insets.left ||
insets.right !== prevProps.insets.right ||
notificationPresenceCap !== prevProps.notificationPresenceCap
) {
if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) {
this.setHeader();
}
@ -425,7 +421,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
getHeader = (): StackNavigationOptions => {
const { searching, canCreateRoom } = this.state;
const { navigation, isMasterDetail, notificationPresenceCap } = this.props;
const { navigation, isMasterDetail } = this.props;
if (searching) {
return {
headerTitleAlign: 'left',
@ -455,7 +451,6 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
: // @ts-ignore
() => navigation.toggleDrawer()
}
badge={() => (notificationPresenceCap ? <HeaderButton.BadgeWarn /> : null)}
/>
),
headerTitle: () => <RoomsListHeaderView />,
@ -1039,7 +1034,6 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
const mapStateToProps = (state: IApplicationState) => ({
user: getUserSelector(state),
isMasterDetail: state.app.isMasterDetail,
notificationPresenceCap: state.app.notificationPresenceCap,
server: state.server.server,
changingServer: state.server.changingServer,
searchText: state.rooms.searchText,

View File

@ -34,7 +34,7 @@ const SelectedUsersView = () => {
const [chats, setChats] = useState<ISelectedUser[]>([]);
const [search, setSearch] = useState<TSearch[]>([]);
const { maxUsers, showButton, title, buttonText, showSkipText = true, nextAction } = useRoute<TRoute>().params;
const { maxUsers, showButton, title, buttonText, nextAction } = useRoute<TRoute>().params;
const navigation = useNavigation<TNavigation>();
const { colors } = useTheme();
@ -58,25 +58,20 @@ const SelectedUsersView = () => {
const isGroupChat = () => maxUsers && maxUsers > 2;
const handleButtonTitle = (buttonTextHeader: string) => {
if (users.length > 0) {
return buttonTextHeader;
}
return showSkipText ? I18n.t('Skip') : '';
};
useLayoutEffect(() => {
const titleHeader = title ?? I18n.t('Select_Members');
const buttonTextHeader = buttonText ?? I18n.t('Next');
const nextActionHeader = nextAction ?? (() => {});
const buttonTitle = handleButtonTitle(buttonTextHeader);
const options = {
title: titleHeader,
headerRight: () =>
(!maxUsers || showButton || (isGroupChat() && users.length > 1)) &&
!!buttonTitle && (
(!maxUsers || showButton || (isGroupChat() && users.length > 1)) && (
<HeaderButton.Container>
<HeaderButton.Item title={buttonTitle} onPress={nextActionHeader} testID='selected-users-view-submit' />
<HeaderButton.Item
title={users.length > 0 ? buttonTextHeader : I18n.t('Skip')}
onPress={nextActionHeader}
testID='selected-users-view-submit'
/>
</HeaderButton.Container>
)
};

View File

@ -250,8 +250,7 @@ class ShareListView extends React.Component<IShareListViewProps, IState> {
prid: item.prid,
uids: item.uids,
usernames: item.usernames,
topic: item.topic,
teamMain: item.teamMain
topic: item.topic
}));
};
@ -381,7 +380,6 @@ class ShareListView extends React.Component<IShareListViewProps, IState> {
type={item.prid ? 'discussion' : item.t}
onPress={() => this.shareMessage(item)}
testID={`share-extension-item-${item.name}`}
teamMain={item.teamMain}
/>
);
};

View File

@ -1,19 +1,17 @@
import React, { Component } from 'react';
import { DrawerNavigationProp } from '@react-navigation/drawer';
import { DrawerNavigationState } from '@react-navigation/native';
import { Alert, ScrollView, Text, TouchableWithoutFeedback, View, Linking } from 'react-native';
import { ScrollView, Text, TouchableWithoutFeedback, View } from 'react-native';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
import { Dispatch } from 'redux';
import Avatar from '../../containers/Avatar';
import Status from '../../containers/Status/Status';
import { events, logEvent } from '../../lib/methods/helpers/log';
import I18n from '../../i18n';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import userPreferences from '../../lib/methods/userPreferences';
import { CustomIcon } from '../../containers/CustomIcon';
import { NOTIFICATION_PRESENCE_CAP, STATUS_COLORS, themes } from '../../lib/constants';
import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView';
@ -23,7 +21,6 @@ import styles from './styles';
import { DrawerParamList } from '../../stacks/types';
import { IApplicationState, IUser } from '../../definitions';
import * as List from '../../containers/List';
import { setNotificationPresenceCap } from '../../actions/app';
interface ISidebarState {
showStatus: boolean;
@ -32,7 +29,6 @@ interface ISidebarState {
interface ISidebarProps {
baseUrl: string;
navigation?: DrawerNavigationProp<DrawerParamList>;
dispatch: Dispatch;
state?: DrawerNavigationState<DrawerParamList>;
Site_Name: string;
user: IUser;
@ -40,8 +36,6 @@ interface ISidebarProps {
loadingServer: boolean;
useRealName: boolean;
allowStatusMessage: boolean;
notificationPresenceCap: boolean;
Presence_broadcast_disabled: boolean;
isMasterDetail: boolean;
viewStatisticsPermission: string[];
viewRoomAdministrationPermission: string[];
@ -65,10 +59,8 @@ class Sidebar extends Component<ISidebarProps, ISidebarState> {
baseUrl,
state,
isMasterDetail,
notificationPresenceCap,
useRealName,
theme,
Presence_broadcast_disabled,
viewStatisticsPermission,
viewRoomAdministrationPermission,
viewUserAdministrationPermission,
@ -99,15 +91,9 @@ class Sidebar extends Component<ISidebarProps, ISidebarState> {
if (nextProps.isMasterDetail !== isMasterDetail) {
return true;
}
if (nextProps.notificationPresenceCap !== notificationPresenceCap) {
return true;
}
if (nextProps.useRealName !== useRealName) {
return true;
}
if (nextProps.Presence_broadcast_disabled !== Presence_broadcast_disabled) {
return true;
}
if (!dequal(nextProps.viewStatisticsPermission, viewStatisticsPermission)) {
return true;
}
@ -170,33 +156,6 @@ class Sidebar extends Component<ISidebarProps, ISidebarState> {
navigation?.closeDrawer();
};
onPressLearnMorePresenceCap = () => {
Linking.openURL('https://go.rocket.chat/i/presence-cap-learn-more');
};
onPressPresenceLearnMore = () => {
const { dispatch } = this.props;
dispatch(setNotificationPresenceCap(false));
userPreferences.setBool(NOTIFICATION_PRESENCE_CAP, false);
Alert.alert(
I18n.t('Presence_Cap_Warning_Title'),
I18n.t('Presence_Cap_Warning_Description'),
[
{
text: I18n.t('Learn_more'),
onPress: this.onPressLearnMorePresenceCap,
style: 'cancel'
},
{
text: I18n.t('Close'),
style: 'default'
}
],
{ cancelable: false }
);
};
renderAdmin = () => {
const { theme, isMasterDetail } = this.props;
if (!this.getIsAdmin()) {
@ -260,27 +219,14 @@ class Sidebar extends Component<ISidebarProps, ISidebarState> {
};
renderCustomStatus = () => {
const { user, theme, Presence_broadcast_disabled, notificationPresenceCap } = this.props;
let status = user?.status;
if (Presence_broadcast_disabled) {
status = 'disabled';
}
let right: React.ReactElement | undefined = <CustomIcon name='edit' size={20} color={themes[theme!].titleText} />;
if (notificationPresenceCap) {
right = <View style={[styles.customStatusDisabled, { backgroundColor: STATUS_COLORS.disabled }]} />;
} else if (Presence_broadcast_disabled) {
right = undefined;
}
const { user, theme } = this.props;
return (
<SidebarItem
text={user.statusText || I18n.t('Edit_Status')}
left={<Status size={24} status={status} />}
left={<Status size={24} status={user?.status} />}
theme={theme!}
right={right}
onPress={() => (Presence_broadcast_disabled ? this.onPressPresenceLearnMore() : this.sidebarNavigate('StatusView'))}
right={<CustomIcon name='edit' size={20} color={themes[theme!].titleText} />}
onPress={() => this.sidebarNavigate('StatusView')}
testID={`sidebar-custom-status-${user.status}`}
/>
);
@ -348,8 +294,6 @@ const mapStateToProps = (state: IApplicationState) => ({
loadingServer: state.server.loading,
useRealName: state.settings.UI_Use_Real_Name as boolean,
allowStatusMessage: state.settings.Accounts_AllowUserStatusMessageChange as boolean,
Presence_broadcast_disabled: state.settings.Presence_broadcast_disabled as boolean,
notificationPresenceCap: state.app.notificationPresenceCap,
isMasterDetail: state.app.isMasterDetail,
viewStatisticsPermission: state.permissions['view-statistics'] as string[],
viewRoomAdministrationPermission: state.permissions['view-room-administration'] as string[],

View File

@ -63,10 +63,5 @@ export default StyleSheet.create({
},
inverted: {
transform: [{ scaleY: -1 }]
},
customStatusDisabled: {
width: 10,
height: 10,
borderRadius: 5
}
});

View File

@ -1,11 +1,16 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { StyleSheet, Text, View } from 'react-native';
import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress';
import FastImage from 'react-native-fast-image';
import sharedStyles from '../Styles';
import { themes } from '../../lib/constants';
import { isTablet } from '../../lib/methods/helpers';
import { TSupportedThemes } from '../../theme';
const ImageProgress = createImageProgress(FastImage);
const SIZE = 96;
const MARGIN_TOP = isTablet ? 0 : 64;
const BORDER_RADIUS = 8;
@ -22,20 +27,50 @@ const styles = StyleSheet.create({
width: SIZE,
height: SIZE,
borderRadius: BORDER_RADIUS
},
fallback: {
width: SIZE,
height: SIZE,
borderRadius: BORDER_RADIUS,
alignItems: 'center',
justifyContent: 'center'
},
initial: {
...sharedStyles.textBold,
fontSize: 42
}
});
const getInitial = (url: string) => url && url.replace(/http(s?):\/\//, '').slice(0, 1);
interface IFallback {
theme: TSupportedThemes;
initial: string;
}
const Fallback = ({ theme, initial }: IFallback) => (
<View style={[styles.container, styles.fallback, { backgroundColor: themes[theme].dangerColor }]}>
<Text style={[styles.initial, { color: themes[theme].buttonText }]}>{initial}</Text>
</View>
);
interface IServerAvatar {
theme: TSupportedThemes;
url: string;
image: string;
}
// TODO: missing skeleton
const ServerAvatar = React.memo(({ theme, url, image }: IServerAvatar) => (
<View style={styles.container}>
{image && (
<FastImage style={[styles.image, { borderColor: themes[theme].borderColor }]} source={{ uri: `${url}/${image}` }} />
<ImageProgress
style={[styles.image, { borderColor: themes[theme].borderColor }]}
source={{ uri: `${url}/${image}` }}
resizeMode={FastImage.resizeMode.cover}
indicator={Progress.Pie}
indicatorProps={{
color: themes[theme].actionTintColor
}}
renderError={() => <Fallback theme={theme} initial={getInitial(url)} />}
/>
)}
</View>
));

View File

@ -3,8 +3,7 @@ module.exports = {
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
'react-native-reanimated/plugin',
'@babel/plugin-transform-named-capturing-groups-regex',
['module:react-native-dotenv']
'@babel/plugin-transform-named-capturing-groups-regex'
],
env: {
production: {

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