Merge branch 'develop' into feat.new-audio-player

This commit is contained in:
Reinaldo Neto 2023-09-21 14:04:40 -03:00 committed by GitHub
commit 9854f1626d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 604 additions and 438 deletions

View File

@ -7,7 +7,7 @@ orbs:
macos: &macos
macos:
xcode: "14.2.0"
resource_class: macos.m1.large.gen1
resource_class: macos.m1.medium.gen1
bash-env: &bash-env
BASH_ENV: "~/.nvm/nvm.sh"
@ -453,7 +453,7 @@ jobs:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
resource-class: large
tag: 2022.12.1
environment:
<<: *android-env
@ -500,7 +500,7 @@ jobs:
<<: *defaults
executor:
name: android/android-machine
resource-class: xlarge
resource-class: large
tag: 2022.12.1
parallelism: 4
steps:

View File

@ -33,8 +33,10 @@ const getStories = () => {
require("../app/containers/ReactionsList/ReactionsList.stories.tsx"),
require("../app/containers/RoomHeader/RoomHeader.stories.tsx"),
require("../app/containers/RoomItem/RoomItem.stories.tsx"),
require("../app/containers/RoomTypeIcon/RoomTypeIcon.stories.tsx"),
require("../app/containers/SearchBox/SearchBox.stories.tsx"),
require("../app/containers/ServerItem/ServerItem.stories.tsx"),
require("../app/containers/Status/Status.stories.tsx"),
require("../app/containers/TextInput/TextInput.stories.tsx"),
require("../app/containers/UIKit/UiKitMessage.stories.tsx"),
require("../app/containers/UIKit/UiKitModal.stories.tsx"),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots RoomTypeIcon All 1`] = `"[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#cbced1\\"},[{\\"width\\":30,\\"height\\":30,\\"textAlignVertical\\":\\"center\\"},[{\\"marginRight\\":4},null]],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#2de0a5\\"},[{\\"width\\":30,\\"height\\":30,\\"textAlignVertical\\":\\"center\\"},[{\\"marginRight\\":4},null]],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#0d0e12\\"},[{\\"marginRight\\":4},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#0d0e12\\"},[{\\"marginRight\\":4},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#0d0e12\\"},[{\\"marginRight\\":4},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#0d0e12\\"},[{\\"marginRight\\":4},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#0d0e12\\"},[{\\"marginRight\\":4},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#0d0e12\\"},[{\\"marginRight\\":4},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#ffd21f\\"},[{\\"marginRight\\":4},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":30,\\"color\\":\\"#0d0e12\\"},[{\\"marginRight\\":4},{\\"margin\\":10}],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]"`;

View File

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Status All 1`] = `"[{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":32,\\"color\\":\\"#2de0a5\\"},[{\\"width\\":32,\\"height\\":32,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":32,\\"color\\":\\"#f5455c\\"},[{\\"width\\":32,\\"height\\":32,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":32,\\"color\\":\\"#ffd21f\\"},[{\\"width\\":32,\\"height\\":32,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":32,\\"color\\":\\"#9ea2a8\\"},[{\\"width\\":32,\\"height\\":32,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":32,\\"color\\":\\"#F38C39\\"},[{\\"width\\":32,\\"height\\":32,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":32,\\"color\\":\\"#cbced1\\"},[{\\"width\\":32,\\"height\\":32,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":32,\\"color\\":\\"#cbced1\\"},[{\\"width\\":32,\\"height\\":32,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":60,\\"color\\":\\"#2de0a5\\"},[{\\"width\\":60,\\"height\\":60,\\"textAlignVertical\\":\\"center\\"},null],{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]"`;

View File

@ -14,6 +14,8 @@ exports[`Storyshots UIKit/UiKitModal Modal - Images 1`] = `"{\\"type\\":\\"RCTSc
exports[`Storyshots UIKit/UiKitModal Modal - Input with error 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":[{\\"flex\\":1,\\"backgroundColor\\":\\"#fff\\"},{\\"paddingHorizontal\\":16}],\\"keyboardShouldPersistTaps\\":\\"always\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":null}]}"`;
exports[`Storyshots UIKit/UiKitModal Modal - Multi Select Input 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":[{\\"flex\\":1,\\"backgroundColor\\":\\"#fff\\"},{\\"paddingHorizontal\\":16}],\\"keyboardShouldPersistTaps\\":\\"always\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":null}]}"`;
exports[`Storyshots UIKit/UiKitModal Modal - Multilne with error 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":[{\\"flex\\":1,\\"backgroundColor\\":\\"#fff\\"},{\\"paddingHorizontal\\":16}],\\"keyboardShouldPersistTaps\\":\\"always\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":null}]}"`;
exports[`Storyshots UIKit/UiKitModal Modal - Section and Accessories 1`] = `"{\\"type\\":\\"RCTScrollView\\",\\"props\\":{\\"style\\":[{\\"flex\\":1,\\"backgroundColor\\":\\"#fff\\"},{\\"paddingHorizontal\\":16}],\\"keyboardShouldPersistTaps\\":\\"always\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{},\\"children\\":null}]}"`;

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.40.0"
versionName "4.41.0"
vectorDrawables.useSupportLibrary = true
if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

View File

@ -1,22 +1,16 @@
import { useBackHandler } from '@react-native-community/hooks';
import * as Haptics from 'expo-haptics';
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react';
import { Keyboard } from 'react-native';
import { Easing } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Keyboard, useWindowDimensions } from 'react-native';
import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { useDimensions, useOrientation } from '../../dimensions';
import { useTheme } from '../../theme';
import { isIOS, isTablet } from '../../lib/methods/helpers';
import { Handle } from './Handle';
import { TActionSheetOptions } from './Provider';
import BottomSheetContent from './BottomSheetContent';
import styles, { ITEM_HEIGHT } from './styles';
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MIN_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
import styles from './styles';
export const ACTION_SHEET_ANIMATION_DURATION = 250;
@ -32,33 +26,34 @@ const ActionSheet = React.memo(
const bottomSheetRef = useRef<BottomSheet>(null);
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
const [isVisible, setVisible] = useState(false);
const { height } = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
const animatedContentHeight = useSharedValue(0);
const animatedHandleHeight = useSharedValue(0);
const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]);
const animatedSnapPoints = useDerivedValue(() => {
if (animatedDataSnaps.value?.length) {
return animatedDataSnaps.value;
}
const contentWithHandleHeight = animatedContentHeight.value + animatedHandleHeight.value;
// Bottom sheet requires a default value to work
if (contentWithHandleHeight === 0) {
return ['25%'];
}
return [contentWithHandleHeight];
}, [data]);
const maxSnap = Math.min(
// Items height
ITEM_HEIGHT * (data?.options?.length || 0) +
// Handle height
HANDLE_HEIGHT +
// Custom header height
(data?.headerHeight || 0) +
// Insets bottom height (Notch devices)
insets.bottom +
// Cancel button height
(data?.hasCancel ? CANCEL_HEIGHT : 0),
height - MIN_SNAP_HEIGHT
const handleContentLayout = useCallback(
({
nativeEvent: {
layout: { height }
}
}) => {
animatedContentHeight.value = height;
},
[animatedContentHeight]
);
/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps = maxSnap > height * 0.6 && !isLandscape && !data.snaps ? [height * 0.5, maxSnap] : [maxSnap];
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
@ -67,6 +62,9 @@ const ActionSheet = React.memo(
const show = (options: TActionSheetOptions) => {
setData(options);
if (options.snaps?.length) {
animatedDataSnaps.value = options.snaps;
}
toggleVisible();
};
@ -104,6 +102,7 @@ const ActionSheet = React.memo(
const onClose = () => {
toggleVisible();
data?.onClose && data?.onClose();
animatedDataSnaps.value = [];
};
const renderBackdrop = useCallback(
@ -131,7 +130,10 @@ const ActionSheet = React.memo(
{isVisible && (
<BottomSheet
ref={bottomSheetRef}
snapPoints={data?.snaps ? data.snaps : snaps}
snapPoints={animatedSnapPoints}
handleHeight={animatedHandleHeight}
// We need undefined to enable vertical swipe gesture inside the bottom sheet like in reaction picker
contentHeight={data.snaps?.length ? undefined : animatedContentHeight}
animationConfigs={ANIMATION_CONFIG}
animateOnMount={true}
backdropComponent={renderBackdrop}
@ -144,7 +146,13 @@ const ActionSheet = React.memo(
enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet}
>
<BottomSheetContent options={data?.options} hide={hide} children={data?.children} hasCancel={data?.hasCancel} />
<BottomSheetContent
options={data?.options}
hide={hide}
children={data?.children}
hasCancel={data?.hasCancel}
onLayout={handleContentLayout}
/>
</BottomSheet>
)}
</>

View File

@ -1,6 +1,7 @@
import { Text } from 'react-native';
import { Text, ViewProps } from 'react-native';
import React from 'react';
import { BottomSheetView, BottomSheetFlatList } from '@gorhom/bottom-sheet';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import I18n from '../../i18n';
import { useTheme } from '../../theme';
@ -15,10 +16,12 @@ interface IBottomSheetContentProps {
options?: TActionSheetOptionsItem[];
hide: () => void;
children?: React.ReactElement | null;
onLayout: ViewProps['onLayout'];
}
const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: IBottomSheetContentProps) => {
const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onLayout }: IBottomSheetContentProps) => {
const { colors } = useTheme();
const { bottom } = useSafeAreaInsets();
const renderFooter = () =>
hasCancel ? (
@ -42,18 +45,19 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: I
keyExtractor={item => item.title}
bounces={true}
renderItem={renderItem}
style={{ backgroundColor: colors.focusedBackground }}
style={{ backgroundColor: colors.focusedBackground, paddingBottom: bottom }}
keyboardDismissMode='interactive'
indicatorStyle='black'
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
onLayout={onLayout}
/>
);
}
return (
<BottomSheetView testID='action-sheet' style={styles.contentContainer}>
<BottomSheetView testID='action-sheet' style={[styles.contentContainer, { paddingBottom: bottom }]} onLayout={onLayout}>
{children}
</BottomSheetView>
);

View File

@ -15,7 +15,6 @@ export type TActionSheetOptionsItem = {
export type TActionSheetOptions = {
options?: TActionSheetOptionsItem[];
headerHeight?: number;
customHeader?: React.ReactElement | null;
hasCancel?: boolean;
type?: string;

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Text, View, useWindowDimensions } from 'react-native';
import { Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
@ -37,21 +37,16 @@ const IncomingCallHeader = React.memo(
const [mic, setMic] = useState(true);
const [cam, setCam] = useState(false);
const dispatch = useDispatch();
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const styles = useStyle();
const insets = useSafeAreaInsets();
const { height, width } = useWindowDimensions();
const isLandscape = width > height;
const { colors } = useTheme();
return (
<View
style={[
styles.container,
(isMasterDetail || isLandscape) && styles.small,
isMasterDetail && styles.small,
{
marginTop: insets.top
}

View File

@ -11,7 +11,6 @@ import { themes } from '../../lib/constants';
import { useTheme } from '../../theme';
import { ROW_HEIGHT } from '../RoomItem';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
import { hideNotification } from '../../lib/methods/helpers/notifications';
@ -76,8 +75,6 @@ const styles = StyleSheet.create({
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
const { theme } = useTheme();
const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation();
const { text, payload } = notification;
const { type, rid } = payload;
const name = type === 'd' ? payload.sender.username : payload.name;
@ -104,7 +101,7 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifie
<View
style={[
styles.container,
(isMasterDetail || isLandscape) && styles.small,
isMasterDetail && styles.small,
{
backgroundColor: themes[theme].focusedBackground,
borderColor: themes[theme].separatorColor,

View File

@ -1,5 +1,5 @@
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { FlatList, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants';
@ -8,7 +8,6 @@ import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed } from '../../lib/methods';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles';
import { IEmoji, TAnyMessageModel } from '../../definitions';
import Touch from '../Touch';
@ -32,7 +31,6 @@ interface THeaderFooter {
theme: TSupportedThemes;
}
export const HEADER_HEIGHT = 36;
const ITEM_SIZE = 36;
const CONTAINER_MARGIN = 8;
const ITEM_MARGIN = 8;
@ -86,11 +84,10 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
);
const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => {
const { width, height } = useDimensions();
const { width } = useWindowDimensions();
const { theme } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const size = (isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = Math.trunc(size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1);
const onReaction: TOnReaction = ({ emoji }) => {

View File

@ -13,7 +13,7 @@ import { LISTENER } from '../Toast';
import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header';
import Header, { IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods';
@ -511,7 +511,6 @@ const MessageActions = React.memo(
await getPermissions();
showActionSheet({
options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader:
!isReadOnly || room.reactWhenReadOnly ? (
<Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />

View File

@ -1,10 +1,9 @@
import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { Col, Grid, Row } from 'react-native-easy-grid';
import range from 'lodash/range';
import { View } from 'react-native';
import * as Animatable from 'react-native-animatable';
import * as Haptics from 'expo-haptics';
import Orientation from 'react-native-orientation-locker';
import styles from './styles';
import Button from './Button';
@ -16,7 +15,6 @@ import LockIcon from './LockIcon';
import Title from './Title';
import Subtitle from './Subtitle';
import { useDimensions } from '../../../dimensions';
import { isTablet } from '../../../lib/methods/helpers';
interface IPasscodeBase {
type: string;
@ -37,18 +35,6 @@ export interface IBase {
const Base = forwardRef<IBase, IPasscodeBase>(
({ type, onEndProcess, previousPasscode, title, subtitle, onError, showBiometry, onBiometryPress }, ref) => {
useLayoutEffect(() => {
if (!isTablet) {
Orientation.lockToPortrait();
}
return () => {
if (!isTablet) {
Orientation.unlockAllOrientations();
}
};
}, []);
const { theme } = useTheme();
const { height } = useDimensions();

View File

@ -7,7 +7,7 @@ import { IReaction } from '../../definitions';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import I18n from '../../i18n';
import styles, { MIN_TAB_WIDTH } from './styles';
import { useDimensions, useOrientation } from '../../dimensions';
import { useDimensions } from '../../dimensions';
interface ITabBarItem {
getCustomEmoji: TGetCustomEmoji;
@ -55,10 +55,8 @@ const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
};
const ReactionsTabBar = ({ tabs, activeTab, goToPage, getCustomEmoji }: IReactionsTabBar): React.ReactElement => {
const { isLandscape } = useOrientation();
const { width } = useDimensions();
const reactionsListWidth = isLandscape ? width / 2 : width;
const tabWidth = tabs && Math.max(reactionsListWidth / tabs.length, MIN_TAB_WIDTH);
const tabWidth = tabs && Math.max(width / tabs.length, MIN_TAB_WIDTH);
const { colors } = useTheme();
return (
<View testID='reactionsTabBar'>

View File

@ -66,14 +66,6 @@ export const Typing = () => (
</>
);
export const Landscape = () => (
<>
<HeaderExample title={() => <RoomHeader width={height} height={width} />} />
<HeaderExample title={() => <RoomHeader width={height} height={width} subtitle='subtitle' />} />
<HeaderExample title={() => <RoomHeader width={height} height={width} title={longText} subtitle={longText} />} />
</>
);
export const Thread = () => (
<>
<HeaderExample title={() => <RoomHeader tmid='123' parentTitle='parent title' />} />

View File

@ -63,10 +63,11 @@ interface IRoomHeader {
type: string;
width: number;
height: number;
roomUserId?: string | null;
prid?: string;
tmid?: string;
teamMain?: boolean;
status: TUserStatus;
status?: TUserStatus;
usersTyping: [];
isGroupChat?: boolean;
parentTitle?: string;
@ -130,6 +131,7 @@ const Header = React.memo(
status,
width,
height,
roomUserId,
prid,
tmid,
onPress,
@ -154,7 +156,13 @@ const Header = React.memo(
if (tmid) {
renderFunc = () => (
<View style={styles.titleContainer}>
<RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} />
<RoomTypeIcon
userId={roomUserId}
type={prid ? 'discussion' : type}
isGroupChat={isGroupChat}
status={status}
teamMain={teamMain}
/>
<Text style={[styles.subtitle, { color: colors.auxiliaryText }]} numberOfLines={1}>
{parentTitle}
</Text>
@ -176,6 +184,7 @@ const Header = React.memo(
<View style={styles.titleContainer}>
{tmid ? null : (
<RoomTypeIcon
userId={roomUserId}
type={prid ? 'discussion' : type}
isGroupChat={isGroupChat}
status={status}

View File

@ -39,14 +39,13 @@ const RoomHeaderContainer = React.memo(
visitor
}: IRoomHeaderContainerProps) => {
let subtitle: string | undefined;
let status: TUserStatus = 'offline';
let statusVisitor: TUserStatus | undefined;
let statusText: string | undefined;
const { width, height } = useDimensions();
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,28 +61,23 @@ 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 { statusText: statusTextActiveUser } = activeUser;
statusText = statusTextActiveUser;
} else if (type === 'l' && visitor?.status) {
const { status: statusVisitor } = visitor;
status = statusVisitor;
({ status: statusVisitor } = visitor);
}
}
return (
<RoomHeader
roomUserId={roomUserId}
prid={prid}
tmid={tmid}
title={title}
subtitle={type === 'd' ? statusText : subtitle}
type={type}
teamMain={teamMain}
status={status}
status={statusVisitor}
width={width}
height={height}
usersTyping={usersTyping}

View File

@ -12,6 +12,7 @@ const IconOrAvatar = ({
type,
rid,
showAvatar,
userId,
prid,
status,
isGroupChat,
@ -30,6 +31,7 @@ const IconOrAvatar = ({
return (
<View style={styles.typeIcon}>
<TypeIcon
userId={userId}
type={type}
prid={prid}
status={status}

View File

@ -41,14 +41,14 @@ export const Touch = () => <RoomItem onPress={() => alert('on press')} onLongPre
export const User = () => (
<>
<RoomItem name='diego.mello' avatar='diego.mello' />
<RoomItem name={longText} />
<RoomItem name='diego.mello' avatar='diego.mello' userId='abc' />
<RoomItem name={longText} userId='abc' />
</>
);
export const Type = () => (
<>
<RoomItem type='d' />
<RoomItem type='d' userId='abc' />
<RoomItem type='c' />
<RoomItem type='p' />
<RoomItem type='l' />
@ -58,18 +58,6 @@ export const Type = () => (
</>
);
export const UserStatus = () => (
<>
<RoomItem status='online' />
<RoomItem status='away' />
<RoomItem status='busy' />
<RoomItem status='offline' />
<RoomItem status='loading' />
<RoomItem status='disabled' />
<RoomItem status='wrong' />
</>
);
export const Alerts = () => (
<>
<RoomItem alert />
@ -159,7 +147,6 @@ export const ExpandedRoomItemWithoutAvatar = () => (
showAvatar={false}
/>
<RoomItem
status='online'
showLastMessage
alert
tunread={[1]}
@ -167,14 +154,7 @@ export const ExpandedRoomItemWithoutAvatar = () => (
displayMode={DisplayMode.Expanded}
showAvatar={false}
/>
<RoomItem
status='online'
showLastMessage
alert
lastMessage={lastMessage}
displayMode={DisplayMode.Expanded}
showAvatar={false}
/>
<RoomItem showLastMessage alert lastMessage={lastMessage} displayMode={DisplayMode.Expanded} showAvatar={false} />
</>
);

View File

@ -16,6 +16,7 @@ import { IRoomItemProps } from './interfaces';
const RoomItem = ({
rid,
userId,
type,
prid,
name,
@ -74,6 +75,7 @@ const RoomItem = ({
accessibilityLabel={accessibilityLabel}
avatar={avatar}
type={type}
userId={userId}
rid={rid}
prid={prid}
status={status}
@ -89,6 +91,7 @@ const RoomItem = ({
<View style={styles.titleContainer}>
{showAvatar ? (
<TypeIcon
userId={userId}
type={type}
prid={prid}
status={status}
@ -125,6 +128,7 @@ const RoomItem = ({
) : (
<View style={[styles.titleContainer, styles.flex]}>
<TypeIcon
userId={userId}
type={type}
prid={prid}
status={status}

View File

@ -3,8 +3,9 @@ import React from 'react';
import RoomTypeIcon from '../RoomTypeIcon';
import { ITypeIconProps } from './interfaces';
const TypeIcon = React.memo(({ type, prid, status, isGroupChat, teamMain, size, style, sourceType }: ITypeIconProps) => (
const TypeIcon = React.memo(({ userId, type, prid, status, isGroupChat, teamMain, size, style, sourceType }: ITypeIconProps) => (
<RoomTypeIcon
userId={userId}
type={prid ? 'discussion' : type}
isGroupChat={isGroupChat}
status={status}

View File

@ -2,13 +2,11 @@ import React, { useEffect, useReducer, useRef } from 'react';
import { Subscription } from 'rxjs';
import I18n from '../../i18n';
import { getUserPresence } from '../../lib/methods';
import { isGroupChat } from '../../lib/methods/helpers';
import { formatDate } from '../../lib/methods/helpers/room';
import { IRoomItemContainerProps } from './interfaces';
import RoomItem from './RoomItem';
import { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from './styles';
import { useUserStatus } from './useUserStatus';
export { ROW_HEIGHT, ROW_HEIGHT_CONDENSED };
@ -44,8 +42,7 @@ const RoomItemContainer = React.memo(
const alert = item.alert || item.tunread?.length;
const [_, forceUpdate] = useReducer(x => x + 1, 1);
const roomSubscription = useRef<Subscription | null>(null);
const { connected, status } = useUserStatus(item.t, item?.visitor?.status, id);
const userId = item.t === 'd' && id && !isGroupChat(item) ? id : null;
useEffect(() => {
const init = () => {
@ -61,13 +58,6 @@ const RoomItemContainer = React.memo(
return () => roomSubscription.current?.unsubscribe();
}, []);
useEffect(() => {
const isDirect = !!(item.t === 'd' && id && !isGroupChat(item));
if (connected && isDirect) {
getUserPresence(id);
}
}, [connected]);
const handleOnPress = () => onPress(item);
const handleOnLongPress = () => onLongPress && onLongPress(item);
@ -98,6 +88,7 @@ const RoomItemContainer = React.memo(
width={width}
favorite={item.f}
rid={item.rid}
userId={userId}
toggleFav={toggleFav}
toggleRead={toggleRead}
hideChannel={hideChannel}
@ -105,7 +96,6 @@ const RoomItemContainer = React.memo(
type={item.t}
isFocused={isFocused}
prid={item.prid}
status={status}
hideUnreadStatus={item.hideUnreadStatus}
hideMentionStatus={item.hideMentionStatus}
alert={alert}
@ -124,7 +114,8 @@ const RoomItemContainer = React.memo(
autoJoin={autoJoin}
showAvatar={showAvatar}
displayMode={displayMode}
sourceType={item.source}
status={item.t === 'l' ? item?.visitor?.status : null}
sourceType={item.t === 'l' ? item.source : null}
/>
);
},

View File

@ -37,6 +37,7 @@ export interface IWrapperProps {
accessibilityLabel: string;
avatar: string;
type: string;
userId: string | null;
rid: string;
children: React.ReactElement;
displayMode: string;
@ -50,6 +51,7 @@ export interface IWrapperProps {
}
export interface ITypeIconProps {
userId: string | null;
type: string;
status: TUserStatus;
prid: string;
@ -144,6 +146,7 @@ export interface IIconOrAvatar {
avatar: string;
type: string;
rid: string;
userId: string | null;
showAvatar: boolean;
displayMode: string;
prid: string;

View File

@ -1,30 +0,0 @@
import { TUserStatus } from '../../definitions';
import { useAppSelector } from '../../lib/hooks';
import { RoomTypes } from '../../lib/methods';
export const useUserStatus = (
type: RoomTypes,
liveChatStatus?: TUserStatus,
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';
}
} else if (type === 'l' && liveChatStatus) {
status = liveChatStatus;
}
}
return {
connected,
status: status as TUserStatus
};
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import { OmnichannelSourceType } from '../../definitions';
import RoomTypeIcon from '.';
export default {
title: 'RoomTypeIcon'
};
export const All = () => (
<>
<RoomTypeIcon size={30} type='d' userId='asd' />
<RoomTypeIcon size={30} type='d' userId='asd' status='online' />
<RoomTypeIcon size={30} type='d' isGroupChat />
<RoomTypeIcon size={30} type='c' />
<RoomTypeIcon size={30} type='p' />
<RoomTypeIcon size={30} type='c' teamMain />
<RoomTypeIcon size={30} type='p' teamMain />
<RoomTypeIcon size={30} type='discussion' />
<RoomTypeIcon size={30} type='l' status='away' sourceType={{ type: OmnichannelSourceType.SMS }} />
<RoomTypeIcon size={30} type='p' style={{ margin: 10 }} />
</>
);

View File

@ -3,8 +3,8 @@ import { StyleSheet, ViewStyle } from 'react-native';
import { OmnichannelRoomIcon } from './OmnichannelRoomIcon';
import { CustomIcon, TIconsName } from '../CustomIcon';
import { STATUS_COLORS, themes } from '../../lib/constants';
import Status from '../Status/Status';
import { themes } from '../../lib/constants';
import Status from '../Status';
import { useTheme } from '../../theme';
import { TUserStatus, IOmnichannelSource } from '../../definitions';
@ -15,6 +15,7 @@ const styles = StyleSheet.create({
});
interface IRoomTypeIcon {
userId?: string | null;
type: string;
isGroupChat?: boolean;
teamMain?: boolean;
@ -24,44 +25,38 @@ interface IRoomTypeIcon {
sourceType?: IOmnichannelSource;
}
const RoomTypeIcon = React.memo(({ type, isGroupChat, status, style, teamMain, size = 16, sourceType }: IRoomTypeIcon) => {
const { theme } = useTheme();
const RoomTypeIcon = React.memo(
({ userId, type, isGroupChat, status, style, teamMain, size = 16, sourceType }: IRoomTypeIcon) => {
const { theme } = useTheme();
if (!type) {
return null;
}
const color = themes[theme].titleText;
const iconStyle = [styles.icon, { color }, style];
if (type === 'd' && !isGroupChat) {
if (!status) {
status = 'offline';
if (!type) {
return null;
}
return <Status style={[iconStyle, { color: STATUS_COLORS[status] }]} size={size} status={status} />;
}
if (type === 'l') {
return <OmnichannelRoomIcon style={[styles.icon, style]} size={size} type={type} status={status} sourceType={sourceType} />;
}
const iconStyle = [styles.icon, style];
// TODO: move this to a separate function
let icon: TIconsName = 'channel-private';
if (teamMain) {
icon = `teams${type === 'p' ? '-private' : ''}`;
} else if (type === 'discussion') {
icon = 'discussions';
} else if (type === 'c') {
icon = 'channel-public';
} else if (type === 'd') {
if (isGroupChat) {
if (type === 'd' && !isGroupChat && userId) {
return <Status id={userId} style={iconStyle} size={size} status={status} />;
}
if (type === 'l') {
return <OmnichannelRoomIcon style={iconStyle} size={size} type={type} status={status} sourceType={sourceType} />;
}
// TODO: move this to a separate function
let icon: TIconsName = 'channel-private';
if (teamMain) {
icon = `teams${type === 'p' ? '-private' : ''}`;
} else if (type === 'discussion') {
icon = 'discussions';
} else if (type === 'c') {
icon = 'channel-public';
} else if (type === 'd' && isGroupChat) {
icon = 'message';
} else {
icon = 'mention';
}
}
return <CustomIcon name={icon} size={size} color={color} style={iconStyle} />;
});
return <CustomIcon name={icon} size={size} color={themes[theme].titleText} style={iconStyle} />;
}
);
export default RoomTypeIcon;

View File

@ -6,8 +6,6 @@ import { useTheme } from '../theme';
import sharedStyles from '../views/Styles';
import { themes } from '../lib/constants';
import { TextInput } from './TextInput';
import { isIOS, isTablet } from '../lib/methods/helpers';
import { useOrientation } from '../dimensions';
const styles = StyleSheet.create({
container: {
@ -16,7 +14,8 @@ const styles = StyleSheet.create({
marginLeft: 0
},
title: {
...sharedStyles.textSemibold
...sharedStyles.textSemibold,
fontSize: 16
}
});
@ -28,15 +27,12 @@ interface ISearchHeaderProps {
const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.Element => {
const { theme } = useTheme();
const isLight = theme === 'light';
const { isLandscape } = useOrientation();
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
const titleFontSize = 16 * scale;
return (
<View style={styles.container}>
<TextInput
autoFocus
style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
style={[styles.title, isLight && { color: themes[theme].headerTitleColor }]}
placeholder={I18n.t('Search')}
onChangeText={onSearchChangeText}
testID={testID}

View File

@ -0,0 +1,20 @@
import React from 'react';
import Status from './Status';
export default {
title: 'Status'
};
export const All = () => (
<>
<Status status='online' />
<Status status='busy' />
<Status status='away' />
<Status status='loading' />
<Status status='disabled' />
<Status status='offline' />
<Status />
<Status status='online' size={60} />
</>
);

View File

@ -3,9 +3,9 @@ import { StyleProp, TextStyle } from 'react-native';
import { CustomIcon, IconSet, TIconsName } from '../CustomIcon';
import { STATUS_COLORS } from '../../lib/constants';
import { IStatus } from './definition';
import { IStatusComponentProps } from './definition';
const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: Omit<IStatus, 'id'>) => {
const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatusComponentProps) => {
const name: TIconsName = `status-${status}`;
const isNameValid = IconSet.hasIcon(name);
const iconName = isNameValid ? name : 'status-offline';

View File

@ -5,5 +5,10 @@ import { TUserStatus } from '../../definitions';
export interface IStatus extends TextProps {
id: string;
size: number;
status: TUserStatus;
status?: TUserStatus | null;
}
export interface IStatusComponentProps extends Omit<IStatus, 'id' | 'size' | 'status'> {
size?: number;
status?: TUserStatus;
}

View File

@ -1,21 +1,32 @@
import React from 'react';
import React, { useEffect } from 'react';
import { TUserStatus } from '../../definitions';
import Status from './Status';
import { IStatus } from './definition';
import { useAppSelector } from '../../lib/hooks';
import { getUserPresence } from '../../lib/methods';
const StatusContainer = ({ id, style, size = 32, ...props }: Omit<IStatus, 'status'>): React.ReactElement => {
const status = useAppSelector(state => {
const StatusContainer = ({ id, style, status, size = 32, ...props }: IStatus): React.ReactElement => {
const connected = useAppSelector(state => state.meteor.connected);
const statusState = useAppSelector(state => {
if (state.settings.Presence_broadcast_disabled) {
return 'disabled';
}
if (state.meteor.connected) {
return state.activeUsers[id] && state.activeUsers[id].status;
if (state.meteor.connected && state.activeUsers[id]) {
return state.activeUsers[id].status;
}
if (!state.meteor.connected) {
return 'offline';
}
return 'loading';
}) as TUserStatus;
return <Status size={size} style={style} status={status} {...props} />;
});
useEffect(() => {
if (connected && statusState === 'loading') {
getUserPresence(id);
}
}, [connected, statusState]);
return <Status size={size} style={style} status={status ?? statusState} {...props} />;
};
export default StatusContainer;

View File

@ -69,7 +69,11 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
<View style={styles.detailsContainer}>
<View style={styles.detailContainer}>
<CustomIcon name='threads' size={24} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
<Text
testID={`thread-count-${count}`}
style={[styles.detailText, { color: themes[theme].auxiliaryText }]}
numberOfLines={1}
>
{count}
</Text>
</View>

View File

@ -87,8 +87,7 @@ export const MultiSelect = React.memo(
selectedItems={selected}
/>
),
onClose,
headerHeight: 275
onClose
});
};
const onHide = () => {

View File

@ -252,6 +252,47 @@ export const SectionMultiSelect = () =>
emoji: true
}
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'Section + select with value undefined'
},
accessory: {
type: 'multi_static_select',
appId: 'app-id',
blockId: 'block-id',
actionId: 'action-id',
initialValue: undefined,
options: [
{
value: 'option_1',
text: {
type: 'plain_text',
text: 'lorem ipsum 🚀',
emoji: true
}
},
{
value: 'option_2',
text: {
type: 'plain_text',
text: 'lorem ipsum 🚀',
emoji: true
}
}
],
placeholder: {
type: 'plain_text',
text: 'Select an item'
},
label: {
type: 'plain_text',
text: 'Label',
emoji: true
}
}
}
]);
SectionMultiSelect.storyName = 'Section + Multi Select';

View File

@ -174,7 +174,12 @@ export const ModalFormInput = () =>
text: 'Set a date',
emoji: true
}
},
}
]);
ModalFormInput.storyName = 'Modal - Form Input';
export const ModalMultiSelect = () =>
UiKitModal([
{
type: 'input',
element: {
@ -200,10 +205,48 @@ export const ModalFormInput = () =>
type: 'plain_text',
text: 'Share with...',
emoji: true
},
hint: {
type: 'plain_text',
text: 'Initial Value Undefined',
emoji: true
}
},
{
type: 'input',
element: {
type: 'multi_static_select',
initialValue: [1],
options: [
{
text: {
type: 'plain_text',
text: 'John'
},
value: 1
},
{
text: {
type: 'plain_text',
text: 'Dog'
},
value: 2
}
]
},
label: {
type: 'plain_text',
text: 'Share with...',
emoji: true
},
hint: {
type: 'plain_text',
text: 'Initial Value as John',
emoji: true
}
}
]);
ModalFormInput.storyName = 'Modal - Form Input';
ModalMultiSelect.storyName = 'Modal - Multi Select Input';
export const ModalFormTextArea = () =>
UiKitModal([

View File

@ -138,7 +138,7 @@ class MessageParser extends UiKitParserMessage<React.ReactElement> {
multiStaticSelect(element: IElement, context: BlockContext) {
const [{ loading, value }, action] = useBlockContext(element, context);
const valueFiltered = element.options?.filter(option => value.includes(option.value));
const valueFiltered = element?.options?.filter(option => value?.includes(option.value));
return <MultiSelect {...element} value={valueFiltered} onChange={action} context={context} loading={loading} multiselect />;
}

View File

@ -14,7 +14,7 @@ const Thread = React.memo(
const { theme } = useTheme();
const { threadBadgeColor, toggleFollowThread, user, replies } = useContext(MessageContext);
if (!tlm || isThreadRoom || tcount === 0) {
if (!tlm || isThreadRoom || tcount === null) {
return null;
}

View File

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

View File

@ -3,6 +3,6 @@ export type SubscriptionsEndpoints = {
POST: (params: { firstUnreadMessage: { _id: string } } | { roomId: string }) => {};
};
'subscriptions.read': {
POST: (params: { rid: string }) => {};
POST: (params: { rid: string; readThreads?: boolean }) => {};
};
};

View File

@ -555,7 +555,6 @@
"Enabled_E2E_Encryption_for_this_room": "enabled E2E encryption for this room",
"Disabled_E2E_Encryption_for_this_room": "disabled E2E encryption for this room",
"Teams": "Teams",
"No_team_channels_found": "No channels found",
"Team_not_found": "Team not found",
"Create_Team": "Create team",
"Team_Name": "Team name",
@ -744,5 +743,6 @@
"accept": "Accept",
"Incoming_call_from": "Incoming call from",
"Call_started": "Call started",
"Message_has_been_shared":"Message has been shared"
}
"Message_has_been_shared":"Message has been shared",
"No_channels_in_team": "No Channels on this team"
}

View File

@ -731,5 +731,6 @@
"Select": "Selecionar",
"Nickname": "Apelido",
"Bio": "Biografia",
"Message_has_been_shared":"Menssagem foi compartilhada"
"Message_has_been_shared":"Menssagem foi compartilhada",
"No_channels_in_team": "Nenhum canal nesta equipe"
}

View File

@ -5,6 +5,7 @@ import { initialWindowMetrics, SafeAreaProvider } from 'react-native-safe-area-c
import RNScreens from 'react-native-screens';
import { Provider } from 'react-redux';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Orientation from 'react-native-orientation-locker';
import { appInit, appInitLocalSettings, setMasterDetail as setMasterDetailAction } from './actions/app';
import { deepLinkingOpen } from './actions/deepLinking';
@ -104,6 +105,9 @@ export default class Root extends React.Component<{}, IState> {
};
if (isTablet) {
this.initTablet();
Orientation.unlockAllOrientations();
} else {
Orientation.lockToPortrait();
}
setNativeTheme(theme);
}

View File

@ -141,7 +141,7 @@ export const colors = {
headerTintColor: '#f9f9f9',
headerTitleColor: '#f9f9f9',
headerSecondaryText: '#9297a2',
toastBackground: '#0C0D0F',
toastBackground: '#54585e',
videoBackground: '#1f2329',
favoriteBackground: '#ffbb00',
hideBackground: '#54585e',
@ -228,7 +228,7 @@ export const colors = {
headerTintColor: '#f9f9f9',
headerTitleColor: '#f9f9f9',
headerSecondaryText: '#b2b8c6',
toastBackground: '#0C0D0F',
toastBackground: '#54585e',
videoBackground: '#1f2329',
favoriteBackground: '#ffbb00',
hideBackground: '#54585e',

View File

@ -102,6 +102,9 @@ export const defaultSettings = {
E2E_Enable: {
type: 'valueAsBoolean'
},
E2E_Enabled_Default_PrivateRooms: {
type: 'valueAsBoolean'
},
Accounts_Directory_DefaultView: {
type: 'valueAsString'
},

View File

@ -199,10 +199,16 @@ class Encryption {
// Encode the private key
const encodedPrivateKey = await this.encodePrivateKey(EJSON.stringify(privateKey), password, this.userId as string);
// This public key is already encoded using EJSON.stringify in the `persistKeys` method
const publicKey = UserPreferences.getString(`${server}-${E2E_PUBLIC_KEY}`);
if (!publicKey) {
throw new Error('Public key not found in local storage, password not changed');
}
// Send the new keys to the server
await Services.e2eSetUserPublicAndPrivateKeys(EJSON.stringify(publicKey), encodedPrivateKey);
await Services.e2eSetUserPublicAndPrivateKeys(publicKey, encodedPrivateKey);
};
// get a encryption room instance

View File

@ -1,22 +0,0 @@
import { StatusBar } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { isIOS, isTablet } from '../methods/helpers';
// Not sure if it's worth adding this here in the context of the actionSheet
/**
* Return the snaps based on the size you pass (aka: Size of action sheet)
* @param {number} componentSize size of the component that will be rendered in the action sheet
*/
export const useSnaps = (componentSize: number): number[] | string[] => {
const insets = useSafeAreaInsets();
if (isIOS) {
const fixTabletInset = isTablet ? 2 : 1;
return [componentSize + (insets.bottom || insets.top) * fixTabletInset];
}
let statusHeight = 0;
if (StatusBar.currentHeight) {
statusHeight = StatusBar.currentHeight;
}
return [componentSize + statusHeight];
};

View File

@ -1,7 +1,6 @@
import { Camera, CameraType } from 'expo-camera';
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
import { useAppSelector } from '..';
@ -13,7 +12,6 @@ import Ringer, { ERingerSounds } from '../../../containers/Ringer';
import i18n from '../../../i18n';
import { getUserSelector } from '../../../selectors/login';
import { useTheme } from '../../../theme';
import { isIOS } from '../../methods/helpers';
import useUserData from '../useUserData';
export default function StartACallActionSheet({ rid }: { rid: string }): React.ReactElement {
@ -28,10 +26,6 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R
const user = useUserData(rid);
// fix safe area bottom padding on iOS
const insets = useSafeAreaInsets();
const paddingBottom = isIOS && insets.bottom ? 8 : 0;
React.useEffect(
() => () => {
if (calling) {
@ -42,10 +36,7 @@ export default function StartACallActionSheet({ rid }: { rid: string }): React.R
);
return (
<View
style={[style.actionSheetContainer, { paddingBottom }]}
onLayout={e => setContainerWidth(e.nativeEvent.layout.width / 2)}
>
<View style={style.actionSheetContainer} onLayout={e => setContainerWidth(e.nativeEvent.layout.width / 2)}>
{calling ? <Ringer ringer={ERingerSounds.DIALTONE} /> : null}
<CallHeader
title={calling && user.direct ? i18n.t('Calling') : i18n.t('Start_a_call')}

View File

@ -1,14 +1,14 @@
import { Camera } from 'expo-camera';
import React from 'react';
import React, { useMemo } from 'react';
import { useActionSheet } from '../../../containers/ActionSheet';
import i18n from '../../../i18n';
import { getUserSelector } from '../../../selectors/login';
import { compareServerVersion, showErrorAlert } from '../../methods/helpers';
import log from '../../methods/helpers/log';
import { handleAndroidBltPermission } from '../../methods/videoConf';
import { Services } from '../../services';
import { useAppSelector } from '../useAppSelector';
import { useSnaps } from '../useSnaps';
import StartACallActionSheet from './StartACallActionSheet';
import { useVideoConfCall } from './useVideoConfCall';
@ -18,9 +18,9 @@ const availabilityErrors = {
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`));
const handleErrors = (isAdmin: boolean, error: keyof typeof availabilityErrors) => {
const key = isAdmin ? `admin-${error}` : error;
showErrorAlert(i18n.t(`${key}-body`), i18n.t(`${key}-header`));
};
export const useVideoConf = (
@ -32,48 +32,42 @@ export const useVideoConf = (
const { callEnabled, disabledTooltip } = useVideoConfCall(rid);
const [permission, requestPermission] = Camera.useCameraPermissions();
const { showActionSheet } = useActionSheet();
const snaps = useSnaps(404);
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
const isServer5OrNewer = useMemo(() => compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0'), [serverVersion]);
const canInitAnCall = async () => {
if (callEnabled) {
if (isServer5OrNewer) {
try {
await Services.videoConferenceGetCapabilities();
return true;
} catch (error: any) {
const isAdmin = !!user.roles?.includes('admin');
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);
}
}
const canInitAnCall = async (): Promise<boolean> => {
if (!callEnabled) return false;
if (isServer5OrNewer) {
try {
await Services.videoConferenceGetCapabilities();
return true;
} catch (error: any) {
const isAdmin = !!user.roles?.includes('admin');
handleErrors(isAdmin, error?.error || 'NOT_CONFIGURED');
return false;
}
return true;
}
return false;
return true;
};
const showInitCallActionSheet = async () => {
const canInit = await canInitAnCall();
if (canInit) {
showActionSheet({
children: <StartACallActionSheet rid={rid} />,
snaps
});
if (!permission?.granted) {
requestPermission();
handleAndroidBltPermission();
try {
const canInit = await canInitAnCall();
if (canInit) {
showActionSheet({
children: <StartACallActionSheet rid={rid} />,
snaps: [480]
});
if (!permission?.granted) {
requestPermission();
handleAndroidBltPermission();
}
}
} catch (error) {
log(error);
}
};

View File

@ -21,7 +21,8 @@ const serverInfoKeys = [
'Force_Screen_Lock',
'Force_Screen_Lock_After',
'uniqueID',
'E2E_Enable'
'E2E_Enable',
'E2E_Enabled_Default_PrivateRooms'
];
// these settings are used only on onboarding process
@ -85,6 +86,9 @@ const serverInfoUpdate = async (serverInfo: IPreparedSettings[], iconSetting: IS
if (setting._id === 'E2E_Enable') {
return { ...allSettings, E2E_Enable: setting.valueAsBoolean };
}
if (setting._id === 'E2E_Enabled_Default_PrivateRooms') {
return { ...allSettings, E2E_Enabled_Default_PrivateRooms: setting.valueAsBoolean };
}
return allSettings;
}, {});

View File

@ -1,5 +1,14 @@
import { URL } from 'react-native-url-polyfill';
import { LOCAL_DOCUMENT_DIRECTORY } from '../handleMediaDownload';
function setParamInUrl({ url, token, userId }: { url: string; token: string; userId: string }) {
const urlObj = new URL(url);
urlObj.searchParams.set('rc_token', token);
urlObj.searchParams.set('rc_uid', userId);
return urlObj.toString();
}
export const formatAttachmentUrl = (attachmentUrl: string | undefined, userId: string, token: string, server: string): string => {
if (LOCAL_DOCUMENT_DIRECTORY && attachmentUrl?.startsWith(LOCAL_DOCUMENT_DIRECTORY)) {
return attachmentUrl;
@ -8,7 +17,7 @@ export const formatAttachmentUrl = (attachmentUrl: string | undefined, userId: s
if (attachmentUrl.includes('rc_token')) {
return encodeURI(attachmentUrl);
}
return encodeURI(`${attachmentUrl}?rc_uid=${userId}&rc_token=${token}`);
return setParamInUrl({ url: attachmentUrl, token, userId });
}
return encodeURI(`${server}${attachmentUrl}?rc_uid=${userId}&rc_token=${token}`);
return setParamInUrl({ url: `${server}${attachmentUrl}`, token, userId });
};

View File

@ -254,6 +254,8 @@ const debouncedUpdate = (subscription: ISubscription) => {
if (batch[key]) {
if (/SUB/.test(key)) {
const sub = batch[key] as ISubscription;
// When calling the api subscriptions.read passing readThreads as true it does not return this prop
if (!sub.tunread) sub.tunread = [];
const roomQueueId = getRoomQueueId(sub.rid);
const room = batch[roomQueueId] as IRoom;
delete batch[roomQueueId];

View File

@ -1,26 +1,28 @@
import {
IAvatarSuggestion,
IMessage,
INotificationPreferences,
IPreviewItem,
IProfileParams,
IRoom,
IRoomNotifications,
SubscriptionType,
IServerRoom,
IUser,
IAvatarSuggestion,
IProfileParams,
RoomType,
IServerRoom
SubscriptionType
} from '../../definitions';
import { TParams } from '../../definitions/ILivechatEditView';
import { ILivechatTag } from '../../definitions/ILivechatTag';
import { ISpotlight } from '../../definitions/ISpotlight';
import { TEAM_TYPE } from '../../definitions/ITeam';
import { OperationParams, ResultFor } from '../../definitions/rest/helpers';
import { SubscriptionsEndpoints } from '../../definitions/rest/v1/subscriptions';
import { Encryption } from '../encryption';
import { TParams } from '../../definitions/ILivechatEditView';
import { store as reduxStore } from '../store/auxStore';
import { getDeviceToken } from '../notifications';
import { RoomTypes, roomTypeToApiType, unsubscribeRooms } from '../methods';
import sdk from './sdk';
import { compareServerVersion, getBundleId, isIOS } from '../methods/helpers';
import { ILivechatTag } from '../../definitions/ILivechatTag';
import { getDeviceToken } from '../notifications';
import { store as reduxStore } from '../store/auxStore';
import sdk from './sdk';
export const createChannel = ({
name,
@ -310,11 +312,33 @@ export const setReaction = (emoji: string, messageId: string) =>
// RC 0.62.2
sdk.post('chat.react', { emoji, messageId });
export const toggleRead = (read: boolean, roomId: string) => {
if (read) {
return sdk.post('subscriptions.unread', { roomId });
/**
* Toggles the read status of a room.
*
* @param isRead - Whether to mark the room as read or unread.
* @param roomId - The ID of the room.
* @param includeThreads - Optional flag to include threads when marking as read.
* @returns A promise from the sdk post method.
*/
export const toggleReadStatus = (
isRead: boolean,
roomId: string,
includeThreads?: boolean
): Promise<ResultFor<'POST', keyof SubscriptionsEndpoints>> => {
let endpoint: keyof SubscriptionsEndpoints;
let payload: OperationParams<'POST', keyof SubscriptionsEndpoints> = { roomId };
if (isRead) {
endpoint = 'subscriptions.unread';
} else {
endpoint = 'subscriptions.read';
payload = { rid: roomId };
if (includeThreads) {
payload.readThreads = includeThreads;
}
}
return sdk.post('subscriptions.read', { rid: roomId });
return sdk.post(endpoint, payload);
};
export const getRoomCounters = (

View File

@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import useDeepCompareEffect from 'use-deep-compare-effect';
import isEmpty from 'lodash/isEmpty';
import Modal from 'react-native-modal';
import Touchable from 'react-native-platform-touchable';
import { useTheme } from '../theme';
import { hasNotch, isTablet } from '../lib/methods/helpers';
import { hasNotch } from '../lib/methods/helpers';
import { PasscodeChoose } from '../containers/Passcode';
import EventEmitter from '../lib/methods/helpers/events';
import { CustomIcon } from '../containers/CustomIcon';
@ -65,14 +64,8 @@ const ChangePasscodeView = React.memo(() => {
};
useEffect(() => {
if (!isTablet) {
Orientation.lockToPortrait();
}
const listener = EventEmitter.addEventListener(CHANGE_PASSCODE_EMITTER, showChangePasscode);
return () => {
if (!isTablet) {
Orientation.unlockAllOrientations();
}
EventEmitter.removeListener(CHANGE_PASSCODE_EMITTER, listener);
};
}, []);

View File

@ -13,16 +13,18 @@ export const RoomSettings = ({
isTeam,
setValue,
createChannelPermission,
createPrivateChannelPermission
createPrivateChannelPermission,
e2eEnabledDefaultPrivateRooms
}: {
isTeam: boolean;
setValue: UseFormSetValue<IFormData>;
createChannelPermission: boolean;
createPrivateChannelPermission: boolean;
e2eEnabledDefaultPrivateRooms: boolean;
}) => {
const [type, setType] = useState(true);
const [readOnly, setReadOnly] = useState(false);
const [encrypted, setEncrypted] = useState(false);
const [encrypted, setEncrypted] = useState(e2eEnabledDefaultPrivateRooms);
const [broadcast, setBroadcast] = useState(false);
const { encryptionEnabled } = useAppSelector(state => ({

View File

@ -68,13 +68,29 @@ export interface IFormData {
const CreateChannelView = () => {
const [createChannelPermission, createPrivateChannelPermission] = usePermissions(['create-c', 'create-p']);
const { isFetching, useRealName, users, e2eEnabledDefaultPrivateRooms } = useAppSelector(
state => ({
isFetching: state.createChannel.isFetching,
users: state.selectedUsers.users,
useRealName: state.settings.UI_Use_Real_Name as boolean,
e2eEnabledDefaultPrivateRooms: state.encryption.enabled && (state.settings.E2E_Enabled_Default_PrivateRooms as boolean)
}),
shallowEqual
);
const {
control,
handleSubmit,
formState: { isDirty },
setValue
} = useForm<IFormData>({
defaultValues: { channelName: '', broadcast: false, encrypted: false, readOnly: false, type: createPrivateChannelPermission }
defaultValues: {
channelName: '',
broadcast: false,
encrypted: e2eEnabledDefaultPrivateRooms,
readOnly: false,
type: createPrivateChannelPermission
}
});
const navigation = useNavigation<StackNavigationProp<ChatsStackParamList, 'CreateChannelView'>>();
@ -84,15 +100,6 @@ const CreateChannelView = () => {
const { colors } = useTheme();
const dispatch = useDispatch();
const { isFetching, useRealName, users } = useAppSelector(
state => ({
isFetching: state.createChannel.isFetching,
users: state.selectedUsers.users,
useRealName: state.settings.UI_Use_Real_Name as boolean
}),
shallowEqual
);
useEffect(() => {
sendLoadingEvent({ visible: isFetching });
}, [isFetching]);
@ -154,6 +161,7 @@ const CreateChannelView = () => {
createPrivateChannelPermission={createPrivateChannelPermission}
isTeam={isTeam}
setValue={setValue}
e2eEnabledDefaultPrivateRooms={e2eEnabledDefaultPrivateRooms}
/>
</View>
{users.length > 0 ? (

View File

@ -3,7 +3,6 @@ import { Base64 } from 'js-base64';
import React from 'react';
import { BackHandler, Image, Keyboard, StyleSheet, Text, View } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import Orientation from 'react-native-orientation-locker';
import { connect } from 'react-redux';
import parse from 'url-parse';
@ -88,9 +87,6 @@ interface ISubmitParams {
class NewServerView extends React.Component<INewServerViewProps, INewServerViewState> {
constructor(props: INewServerViewProps) {
super(props);
if (!isTablet) {
Orientation.lockToPortrait();
}
this.setHeader();
this.state = {

View File

@ -1,7 +1,6 @@
import { sha256 } from 'js-sha256';
import React from 'react';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
import { deleteAccount } from '../../../../actions/login';
@ -18,7 +17,6 @@ import sharedStyles from '../../../Styles';
export function DeleteAccountActionSheetContent(): React.ReactElement {
const { hideActionSheet, showActionSheet } = useActionSheet();
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const { colors } = useTheme();
const handleDeleteAccount = async (password: string) => {
@ -40,8 +38,7 @@ export function DeleteAccountActionSheetContent(): React.ReactElement {
removedRooms={removedRooms}
password={sha256(password)}
/>
),
headerHeight: 225 + insets.bottom
)
});
}, 250); // timeout for hide effect
} else if (error.data.errorType === 'error-invalid-password') {

View File

@ -237,8 +237,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
}}
onCancel={this.props.hideActionSheet}
/>
),
headerHeight: 225
)
});
return;
}
@ -417,8 +416,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
deleteOwnAccount = () => {
logEvent(events.DELETE_OWN_ACCOUNT);
this.props.showActionSheet({
children: <DeleteAccountActionSheetContent />,
headerHeight: 225
children: <DeleteAccountActionSheetContent />
});
};

View File

@ -1109,7 +1109,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
{teamId && isTeamRoom({ teamId, joined }) ? (
<>
<List.Item
title='Teams'
title='Channels'
onPress={() => {
logEvent(events.ROOM_GO_TEAM_CHANNELS);
if (isMasterDetail) {

View File

@ -857,7 +857,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
children: (
<ReactionPicker message={selectedMessage} onEmojiSelected={this.onReactionPress} reactionClose={this.onReactionClose} />
),
snaps: [400],
snaps: ['50%'],
enableContentPanningGesture: false
});
}, 100);

View File

@ -4,8 +4,6 @@ import { StyleSheet, Text, TextInputProps, TouchableOpacity, TouchableOpacityPro
import I18n from '../../../i18n';
import sharedStyles from '../../Styles';
import { CustomIcon } from '../../../containers/CustomIcon';
import { isIOS, isTablet } from '../../../lib/methods/helpers';
import { useOrientation } from '../../../dimensions';
import { useTheme } from '../../../theme';
import SearchHeader from '../../../containers/SearchHeader';
@ -20,9 +18,11 @@ const styles = StyleSheet.create({
},
title: {
flexShrink: 1,
fontSize: 16,
...sharedStyles.textSemibold
},
subtitle: {
fontSize: 14,
...sharedStyles.textRegular
},
upsideDown: {
@ -55,10 +55,6 @@ const Header = React.memo(
onPress
}: IRoomHeader) => {
const { colors } = useTheme();
const { isLandscape } = useOrientation();
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
const titleFontSize = 16 * scale;
const subTitleFontSize = 14 * scale;
if (showSearchHeader) {
return <SearchHeader onSearchChangeText={onSearchChangeText} testID='rooms-list-view-search-input' />;
@ -77,7 +73,7 @@ const Header = React.memo(
<View style={styles.container}>
<TouchableOpacity onPress={onPress} testID='rooms-list-header-server-dropdown-button'>
<View style={styles.button}>
<Text style={[styles.title, { fontSize: titleFontSize, color: colors.headerTitleColor }]} numberOfLines={1}>
<Text style={[styles.title, { color: colors.headerTitleColor }]} numberOfLines={1}>
{serverName}
</Text>
<CustomIcon
@ -90,7 +86,7 @@ const Header = React.memo(
{subtitle ? (
<Text
testID='rooms-list-header-server-subtitle'
style={[styles.subtitle, { color: colors.auxiliaryText, fontSize: subTitleFontSize }]}
style={[styles.subtitle, { color: colors.auxiliaryText }]}
numberOfLines={1}
>
{subtitle}

View File

@ -2,7 +2,6 @@ import React from 'react';
import { BackHandler, FlatList, Keyboard, NativeEventSubscription, RefreshControl, Text, View } from 'react-native';
import { batch, connect } from 'react-redux';
import { dequal } from 'dequal';
import Orientation from 'react-native-orientation-locker';
import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { Subscription } from 'rxjs';
@ -56,7 +55,8 @@ import {
isRead,
debounce,
isIOS,
isTablet
isTablet,
compareServerVersion
} from '../../lib/methods/helpers';
import { E2E_BANNER_TYPE, DisplayMode, SortBy, MAX_SIDEBAR_WIDTH, themes } from '../../lib/constants';
import { Services } from '../../lib/services';
@ -104,6 +104,7 @@ interface IRoomsListViewProps {
createPublicChannelPermission: [];
createPrivateChannelPermission: [];
createDiscussionPermission: [];
serverVersion: string;
}
interface IRoomsListViewState {
@ -216,7 +217,6 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
EventEmitter.addEventListener(KEY_COMMAND, this.handleCommands);
}
this.unsubscribeFocus = navigation.addListener('focus', () => {
Orientation.unlockAllOrientations();
this.animated = true;
// Check if there were changes with sort preference, then call getSubscription to remount the list
if (this.sortPreferencesChanged) {
@ -706,9 +706,11 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
toggleRead = async (rid: string, tIsRead: boolean) => {
logEvent(tIsRead ? events.RL_UNREAD_CHANNEL : events.RL_READ_CHANNEL);
const { serverVersion } = this.props;
try {
const db = database.active;
const result = await Services.toggleRead(tIsRead, rid);
const includeThreads = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.4.0');
const result = await Services.toggleReadStatus(tIsRead, rid, includeThreads);
if (result.success) {
const subCollection = db.get('subscriptions');
@ -718,6 +720,9 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
await subRecord.update(sub => {
sub.alert = tIsRead;
sub.unread = 0;
if (includeThreads) {
sub.tunread = [];
}
});
} catch (e) {
log(e);
@ -955,7 +960,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
showAvatar,
displayMode
} = this.props;
const id = getUidDirectMessage(item);
const id = item.search && item.t === 'd' ? item._id : getUidDirectMessage(item);
const swipeEnabled = this.isSwipeEnabled(item);
return (
@ -1067,7 +1072,8 @@ const mapStateToProps = (state: IApplicationState) => ({
createDirectMessagePermission: state.permissions['create-d'],
createPublicChannelPermission: state.permissions['create-c'],
createPrivateChannelPermission: state.permissions['create-p'],
createDiscussionPermission: state.permissions['start-discussion']
createDiscussionPermission: state.permissions['start-discussion'],
serverVersion: state.server.version
});
export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomsListView))));

View File

@ -2,7 +2,6 @@ import isEmpty from 'lodash/isEmpty';
import React, { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import Modal from 'react-native-modal';
import Orientation from 'react-native-orientation-locker';
import Touchable from 'react-native-platform-touchable';
import useDeepCompareEffect from 'use-deep-compare-effect';
@ -10,7 +9,7 @@ import { PasscodeEnter } from '../containers/Passcode';
import { LOCAL_AUTHENTICATE_EMITTER } from '../lib/constants';
import { CustomIcon } from '../containers/CustomIcon';
import { useTheme } from '../theme';
import { hasNotch, isTablet } from '../lib/methods/helpers';
import { hasNotch } from '../lib/methods/helpers';
import EventEmitter from '../lib/methods/helpers/events';
interface IData {
@ -46,14 +45,8 @@ const ScreenLockedView = (): JSX.Element => {
};
useEffect(() => {
if (!isTablet) {
Orientation.lockToPortrait();
}
const listener = EventEmitter.addEventListener(LOCAL_AUTHENTICATE_EMITTER, showScreenLock);
return () => {
if (!isTablet) {
Orientation.unlockAllOrientations();
}
EventEmitter.removeListener(LOCAL_AUTHENTICATE_EMITTER, listener);
};
}, []);

View File

@ -2,7 +2,6 @@ import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import { useDispatch } from 'react-redux';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
@ -18,7 +17,7 @@ import I18n from '../i18n';
import KeyboardView from '../containers/KeyboardView';
import { getUserSelector } from '../selectors/login';
import { useTheme } from '../theme';
import { isTablet, showErrorAlert } from '../lib/methods/helpers';
import { showErrorAlert } from '../lib/methods/helpers';
import scrollPersistTaps from '../lib/methods/helpers/scrollPersistTaps';
import sharedStyles from './Styles';
import { Services } from '../lib/services';
@ -56,9 +55,6 @@ const SetUsernameView = () => {
useLayoutEffect(() => {
navigation.setOptions({ title: server });
if (!isTablet) {
Orientation.lockToPortrait();
}
}, [navigation, server]);
useEffect(() => {

View File

@ -122,9 +122,5 @@ export default StyleSheet.create({
},
inputLastChild: {
marginBottom: 15
},
notchLandscapeContainer: {
marginTop: -34,
paddingHorizontal: 30
}
});

View File

@ -520,10 +520,10 @@ class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChan
return <BackgroundContainer loading />;
}
if (isSearching && !search.length) {
return <BackgroundContainer text={searchText ? I18n.t('No_team_channels_found') : ''} />;
return <BackgroundContainer text={searchText ? I18n.t('No_channels_in_team') : ''} />;
}
if (!isSearching && !data.length) {
return <BackgroundContainer text={I18n.t('No_team_channels_found')} />;
return <BackgroundContainer text={I18n.t('No_channels_in_team')} />;
}
return (

View File

@ -9,18 +9,20 @@ import {
TTextMatcher,
tapAndWaitFor,
navigateToRoom,
mockMessage
mockMessage,
tryTapping
} from '../../helpers/app';
import { createRandomRoom, createRandomUser } from '../../helpers/data_setup';
describe('Threads', () => {
let room: string;
let textMatcher: TTextMatcher;
let alertButtonType: string;
beforeAll(async () => {
const user = await createRandomUser();
({ name: room } = await createRandomRoom(user));
({ textMatcher } = platformTypes[device.getPlatform()]);
({ textMatcher, alertButtonType } = platformTypes[device.getPlatform()]);
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(user.username, user.password);
@ -219,6 +221,30 @@ describe('Threads', () => {
await tapAndWaitFor(element(by.id(`message-thread-button-${thread}`)), element(by.id(`room-view-title-${thread}`)), 2000);
await expect(element(by.id('messagebox-input-thread'))).toHaveText('');
await tapBack();
});
it('should create thread delete the message and the thread show the correct number of messages', async () => {
thread = await mockMessage('thread-message-count');
await element(by[textMatcher](thread)).atIndex(0).tap();
await element(by[textMatcher](thread)).atIndex(0).longPress();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by[textMatcher]('Reply in thread')).atIndex(0).tap();
await element(by.id('messagebox-input')).replaceText('replied');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.id(`thread-count-1`)))
.toExist()
.withTimeout(5000);
await element(by.id(`message-thread-button-${thread}`)).tap();
await tryTapping(element(by[textMatcher]('replied')).atIndex(0), 2000, true);
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await sleep(300); // wait for animation
await element(by[textMatcher]('Delete')).atIndex(0).tap();
await element(by[textMatcher]('Delete').and(by.type(alertButtonType))).tap();
await tapBack();
await waitFor(element(by.id(`thread-count-0`)))
.toExist()
.withTimeout(5000);
});
});
});

View File

@ -1,11 +1,18 @@
import { by, device, element, expect, waitFor } from 'detox';
import { TTextMatcher, login, navigateToLogin, platformTypes, searchRoom, tapBack, tryTapping } from '../../helpers/app';
import { TTextMatcher, login, navigateToLogin, platformTypes, searchRoom, sleep, tapBack, tryTapping } from '../../helpers/app';
import { ITestUser, createRandomRoom, createRandomUser, initApi } from '../../helpers/data_setup';
import random from '../../helpers/random';
const roomId = '64b846e4760e618aa9f91ab7';
function getIndex() {
if (device.getPlatform() === 'android') {
return 1;
}
return 0;
}
const sendMessageOnTranslationTestRoom = async (msg: string): Promise<{ user: ITestUser; msgId: string }> => {
const user = await createRandomUser();
const api = await initApi(user.username, user.password);
@ -36,22 +43,37 @@ async function navigateToRoom(roomName: string) {
.withTimeout(5000);
}
async function searchMessage(msg: string, textMatcher: TTextMatcher) {
await sleep(1000); // wait for proper load the room
await element(by.id('room-view-search')).tap();
await waitFor(element(by.id('search-messages-view')))
.toExist()
.withTimeout(5000);
await element(by.id('search-message-view-input')).replaceText(msg);
await waitFor(element(by[textMatcher](msg)).atIndex(getIndex()))
.toExist()
.withTimeout(30000);
await sleep(1000);
await element(by[textMatcher](msg)).atIndex(getIndex()).tap();
await sleep(10000);
}
export function waitForVisible(id: string) {
return waitFor(element(by.id(id)))
.toBeVisible()
.withTimeout(5000);
.withTimeout(10000);
}
export function waitForVisibleTextMatcher(msg: string, textMatcher: TTextMatcher) {
return waitFor(element(by[textMatcher](msg)).atIndex(0))
.toExist()
.withTimeout(5000);
.withTimeout(10000);
}
export function waitForNotVisible(id: string) {
return waitFor(element(by.id(id)))
.not.toBeVisible()
.withTimeout(5000);
.withTimeout(10000);
}
describe('Auto Translate', () => {
@ -97,6 +119,7 @@ describe('Auto Translate', () => {
});
it('should see old message not translated before enable auto translate', async () => {
await searchMessage(oldMessage[languages.default] as string, textMatcher);
await waitForVisibleTextMatcher(oldMessage[languages.default] as string, textMatcher);
await waitForVisibleTextMatcher(attachmentMessage[languages.default] as string, textMatcher);
});
@ -141,6 +164,7 @@ describe('Auto Translate', () => {
});
it('should see old message translated after enable auto translate', async () => {
await searchMessage(oldMessage[languages.default] as string, textMatcher);
await waitForVisibleTextMatcher(oldMessage[languages.translated] as string, textMatcher);
await waitForVisibleTextMatcher(attachmentMessage[languages.translated] as string, textMatcher);
});
@ -148,6 +172,7 @@ describe('Auto Translate', () => {
it('should see new message translated', async () => {
const randomMatcher = random();
const data = await sendMessageOnTranslationTestRoom(`${newMessage[languages.default]} - ${randomMatcher}`);
await searchMessage(`${newMessage[languages.default]} - ${randomMatcher}`, textMatcher); // will scroll the messages list to the last one
await waitForVisibleTextMatcher(`${newMessage[languages.translated]} - ${randomMatcher}`, textMatcher);
await deleteMessageOnTranslationTestRoom(data);
});

View File

@ -1760,7 +1760,7 @@
INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 4.40.0;
MARKETING_VERSION = 4.41.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
@ -1799,7 +1799,7 @@
INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 4.40.0;
MARKETING_VERSION = 4.41.0;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;

View File

@ -26,7 +26,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>4.40.0</string>
<string>4.41.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@ -26,7 +26,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>4.40.0</string>
<string>4.41.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>KeychainGroup</key>

View File

@ -1,6 +1,6 @@
{
"name": "rocket-chat-reactnative",
"version": "4.40.0",
"version": "4.41.0",
"private": true,
"scripts": {
"start": "react-native start",
@ -128,11 +128,12 @@
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "3.13.1",
"react-native-scrollable-tab-view": "ptomasroos/react-native-scrollable-tab-view",
"react-native-simple-crypto": "RocketChat/react-native-simple-crypto#0.5.1",
"react-native-simple-crypto": "RocketChat/react-native-simple-crypto#0.5.2",
"react-native-skeleton-placeholder": "^5.2.3",
"react-native-slowlog": "^1.0.2",
"react-native-svg": "^13.8.0",
"react-native-ui-lib": "RocketChat/react-native-ui-lib#ef50151b8d9c1627ef527c620a1472868f9f4df8",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "9.1.0",
"react-native-webview": "11.26.1",
"react-redux": "^8.0.5",

View File

@ -3139,6 +3139,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.21.0":
version "7.22.11"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4"
integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
@ -8330,7 +8337,7 @@ buffer@^5.2.0:
base64-js "^1.0.2"
ieee754 "^1.1.4"
buffer@^5.5.0:
buffer@^5.4.3, buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@ -9594,9 +9601,11 @@ damerau-levenshtein@^1.0.7:
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
date-fns@^2.29.3:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@^1.8.15:
version "1.11.7"
@ -17044,6 +17053,11 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
punycode@^2.1.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
qr-image@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/qr-image/-/qr-image-3.2.0.tgz#9fa8295beae50c4a149cf9f909a1db464a8672e8"
@ -17614,9 +17628,9 @@ react-native-scrollable-tab-view@ptomasroos/react-native-scrollable-tab-view:
prop-types "^15.6.0"
react-timer-mixin "^0.13.3"
react-native-simple-crypto@RocketChat/react-native-simple-crypto#0.5.1:
react-native-simple-crypto@RocketChat/react-native-simple-crypto#0.5.2:
version "0.5.1"
resolved "https://codeload.github.com/RocketChat/react-native-simple-crypto/tar.gz/dcf6eef5359c739d521371918e13a73f2ea6cb42"
resolved "https://codeload.github.com/RocketChat/react-native-simple-crypto/tar.gz/227e1a106e0507551a49df86c8d61ddbaf1e285c"
dependencies:
base64-js "^1.3.0"
hex-lite "^1.5.0"
@ -17669,6 +17683,13 @@ react-native-ui-lib@RocketChat/react-native-ui-lib#ef50151b8d9c1627ef527c620a147
tinycolor2 "^1.4.2"
url-parse "^1.2.0"
react-native-url-polyfill@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589"
integrity sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==
dependencies:
whatwg-url-without-unicode "8.0.0-3"
react-native-vector-icons@9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-9.1.0.tgz#52eaa205f5954d567b7048eb93d58ac854a2c59e"
@ -18045,6 +18066,11 @@ regenerator-runtime@^0.13.7:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
regenerator-transform@^0.14.2:
version "0.14.4"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
@ -18584,7 +18610,7 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
"semver@2 || 3 || 4 || 5", semver@^5.2.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -18599,6 +18625,11 @@ semver@7.3.2:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
semver@^5.2.0:
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@ -20817,6 +20848,11 @@ webidl-conversions@^3.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
webpack-dev-middleware@^3.7.3:
version "3.7.3"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5"
@ -20905,6 +20941,15 @@ whatwg-fetch@^3.0.0:
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
whatwg-url-without-unicode@8.0.0-3:
version "8.0.0-3"
resolved "https://registry.yarnpkg.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b"
integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==
dependencies:
buffer "^5.4.3"
punycode "^2.1.1"
webidl-conversions "^5.0.0"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"