main #2

Merged
Administrator merged 12 commits from Ivasoft/mattermost-mobile:main into main 2023-05-29 09:03:05 +00:00
33 changed files with 276 additions and 255 deletions

View File

@@ -110,7 +110,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 471
versionCode 472
versionName "2.4.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -4,6 +4,7 @@
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {getLicense} from '@queries/servers/system';
import {getTeamById} from '@queries/servers/team';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
@@ -12,25 +13,14 @@ import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
export const fetchGroup = async (serverUrl: string, id: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client: Client = NetworkManager.getClient(serverUrl);
const group = await client.getGroup(id);
// Save locally
return operator.handleGroups({groups: [group], prepareRecordsOnly: fetchOnly});
} catch (error) {
logDebug('error on fetchGroup', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const fetchGroupsForAutocomplete = async (serverUrl: string, query: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
return [];
}
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getGroups({query, includeMemberCount: true});
@@ -48,7 +38,11 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin
export const fetchGroupsByNames = async (serverUrl: string, names: string[], fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
return [];
}
const client: Client = NetworkManager.getClient(serverUrl);
const promises: Array <Promise<Group[]>> = [];
@@ -74,7 +68,12 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
return {groups: [], groupChannels: []};
}
const client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToChannel(channelId);
@@ -101,7 +100,11 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
return {groups: [], groupTeams: []};
}
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToTeam(teamId);
@@ -128,7 +131,11 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
return {groups: [], groupMemberships: []};
}
const client: Client = NetworkManager.getClient(serverUrl);
const response = await client.getAllGroupsAssociatedToMembership(userId);

View File

@@ -8,24 +8,13 @@ import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientGroupsMix {
getGroup: (id: string) => Promise<Group>;
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean) => Promise<Group[]>;
getAllGroupsAssociatedToTeam: (teamId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
getAllChannelsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupChannels: GroupChannel[]}>;
getAllMembershipsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupMemberships: UserProfile[]; total_member_count: number}>;
getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>;
}
const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getGroup = async (id: string) => {
return this.doFetch(
`${this.urlVersion}/groups/${id}`,
{method: 'get'},
);
};
getGroups = async ({query = '', filterAllowReference = true, page = 0, perPage = PER_PAGE_DEFAULT, since = 0, includeMemberCount = false}) => {
return this.doFetch(
`${this.urlVersion}/groups${buildQueryString({
@@ -64,27 +53,6 @@ const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase)
{method: 'get'},
);
};
getAllTeamsAssociatedToGroup = async (groupId: string, filterAllowReference = false) => {
return this.doFetch(
`${this.urlVersion}/groups/${groupId}/teams${buildQueryString({filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllChannelsAssociatedToGroup = async (groupId: string, filterAllowReference = false) => {
return this.doFetch(
`${this.urlVersion}/groups/${groupId}/channels${buildQueryString({filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllMembershipsAssociatedToGroup = async (groupId: string, filterAllowReference = false) => {
return this.doFetch(
`${this.urlVersion}/groups/${groupId}/members${buildQueryString({filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
};
export default ClientGroups;

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {type LayoutChangeEvent, Platform, ScrollView, useWindowDimensions, View} from 'react-native';
import {type LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
@@ -10,7 +10,7 @@ import Button from '@components/button';
import {USER_CHIP_BOTTOM_MARGIN, USER_CHIP_HEIGHT} from '@components/selected_chip';
import Toast from '@components/toast';
import {useTheme} from '@context/theme';
import {useIsTablet, useKeyboardHeightWithDuration} from '@hooks/device';
import {useKeyboardHeightWithDuration} from '@hooks/device';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import SelectedUser from './selected_user';
@@ -28,14 +28,9 @@ type Props = {
buttonText: string;
/**
* the height of the parent container
* the overlap of the keyboard with this list
*/
containerHeight?: number;
/**
* the Y position of the first view in the parent container
*/
modalPosition?: number;
keyboardOverlap?: number;
/**
* A handler function that will select or deselect a user when clicked on.
@@ -145,8 +140,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
export default function SelectedUsers({
buttonIcon,
buttonText,
containerHeight = 0,
modalPosition = 0,
keyboardOverlap = 0,
onPress,
onRemove,
selectedIds,
@@ -160,15 +154,12 @@ export default function SelectedUsers({
}: Props) {
const theme = useTheme();
const style = getStyleFromTheme(theme);
const isTablet = useIsTablet();
const keyboard = useKeyboardHeightWithDuration();
const insets = useSafeAreaInsets();
const dimensions = useWindowDimensions();
const usersChipsHeight = useSharedValue(0);
const [isVisible, setIsVisible] = useState(false);
const numberSelectedIds = Object.keys(selectedIds).length;
const bottomSpace = (dimensions.height - containerHeight - modalPosition);
const users = useMemo(() => {
const u = [];
@@ -196,34 +187,6 @@ export default function SelectedUsers({
0
), [isVisible]);
const marginBottom = useMemo(() => {
let margin = keyboard.height && Platform.OS === 'ios' ? keyboard.height - insets.bottom : 0;
if (isTablet) {
margin = keyboard.height ? Math.max((keyboard.height - bottomSpace - insets.bottom), 0) : 0;
}
return margin;
}, [keyboard, isTablet, insets.bottom, bottomSpace]);
const paddingBottom = useMemo(() => {
if (Platform.OS === 'android') {
return TABLET_MARGIN_BOTTOM + insets.bottom;
}
if (!isVisible) {
return 0;
}
if (isTablet) {
return TABLET_MARGIN_BOTTOM + insets.bottom;
}
if (!keyboard.height) {
return insets.bottom;
}
return TABLET_MARGIN_BOTTOM + insets.bottom;
}, [isTablet, isVisible, insets.bottom, keyboard.height]);
const handlePress = useCallback(() => {
onPress();
}, [onPress]);
@@ -242,11 +205,10 @@ export default function SelectedUsers({
});
const animatedContainerStyle = useAnimatedStyle(() => ({
marginBottom: withTiming(marginBottom, {duration: keyboard.duration}),
paddingBottom: withTiming(paddingBottom, {duration: keyboard.duration}),
marginBottom: withTiming(keyboardOverlap + TABLET_MARGIN_BOTTOM, {duration: keyboard.duration}),
backgroundColor: isVisible ? theme.centerChannelBg : 'transparent',
...androidMaxHeight,
}), [marginBottom, paddingBottom, keyboard.duration, isVisible, theme.centerChannelBg]);
}), [keyboardOverlap, keyboard.duration, isVisible, theme.centerChannelBg]);
const animatedToastStyle = useAnimatedStyle(() => {
return {

View File

@@ -7,9 +7,13 @@ import {MENTIONS_REGEX} from '@constants/autocomplete';
export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Post[], excludeUsername?: string) => {
const usernamesToLoad = new Set<string>();
posts.forEach((p) => {
const findNeededUsernames = (text?: string) => {
if (!text || !text.includes('@')) {
return;
}
let match;
while ((match = MENTIONS_REGEX.exec(p.message)) !== null) {
while ((match = MENTIONS_REGEX.exec(text)) !== null) {
const lowercaseMatch = match[1].toLowerCase();
if (General.SPECIAL_MENTIONS.has(lowercaseMatch)) {
@@ -26,7 +30,19 @@ export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Pos
usernamesToLoad.add(lowercaseMatch);
}
});
};
for (const post of posts) {
// These correspond to the fields searched by getMentionsEnabledFields on the server
findNeededUsernames(post.message);
if (post.props?.attachments) {
for (const attachment of post.props.attachments) {
findNeededUsernames(attachment.pretext);
findNeededUsernames(attachment.text);
}
}
}
return usernamesToLoad;
};

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {type RefObject, useEffect, useRef, useState} from 'react';
import {AppState, Keyboard, NativeEventEmitter, NativeModules, Platform, View} from 'react-native';
import {AppState, Keyboard, NativeEventEmitter, NativeModules, Platform, useWindowDimensions, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Device} from '@constants';
@@ -110,7 +110,7 @@ export function useKeyboardHeight(keyboardTracker?: React.RefObject<KeyboardTrac
return height;
}
export function useModalPosition(viewRef: RefObject<View>, deps: React.DependencyList = []) {
export function useViewPosition(viewRef: RefObject<View>, deps: React.DependencyList = []) {
const [modalPosition, setModalPosition] = useState(0);
const isTablet = useIsTablet();
const height = useKeyboardHeight();
@@ -127,3 +127,21 @@ export function useModalPosition(viewRef: RefObject<View>, deps: React.Dependenc
return modalPosition;
}
export function useKeyboardOverlap(viewRef: RefObject<View>, containerHeight: number) {
const keyboardHeight = useKeyboardHeight();
const isTablet = useIsTablet();
const viewPosition = useViewPosition(viewRef, [containerHeight]);
const dimensions = useWindowDimensions();
const insets = useSafeAreaInsets();
const bottomSpace = (dimensions.height - containerHeight - viewPosition);
const tabletOverlap = Math.max(0, keyboardHeight - bottomSpace);
const phoneOverlap = keyboardHeight || insets.bottom;
const overlap = Platform.select({
ios: isTablet ? tabletOverlap : phoneOverlap,
default: 0,
});
return overlap;
}

View File

@@ -90,7 +90,7 @@ class PushNotifications {
if (isCRTEnabled && payload.root_id) {
const thread = await getThreadById(database, payload.root_id);
if (thread?.isFollowing) {
markThreadAsRead(serverUrl, payload.team_id, payload.post_id);
markThreadAsRead(serverUrl, payload.team_id, payload.root_id);
}
} else {
markChannelAsViewed(serverUrl, payload.channel_id);

View File

@@ -17,7 +17,7 @@ import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useModalPosition} from '@hooks/device';
import {useKeyboardOverlap} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {t} from '@i18n';
import {dismissModal} from '@screens/navigation';
@@ -126,12 +126,12 @@ export default function ChannelAddMembers({
const {formatMessage} = intl;
const mainView = useRef<View>(null);
const modalPosition = useModalPosition(mainView);
const [containerHeight, setContainerHeight] = useState(0);
const keyboardOverlap = useKeyboardOverlap(mainView, containerHeight);
const [term, setTerm] = useState('');
const [addingMembers, setAddingMembers] = useState(false);
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
const [containerHeight, setContainerHeight] = useState(0);
const clearSearch = useCallback(() => {
setTerm('');
@@ -281,8 +281,7 @@ export default function ChannelAddMembers({
createFilter={createUserFilter}
/>
<SelectedUsers
containerHeight={containerHeight}
modalPosition={modalPosition}
keyboardOverlap={keyboardOverlap}
selectedIds={selectedIds}
onRemove={handleRemoveProfile}
teammateNameDisplay={teammateNameDisplay}

View File

@@ -17,7 +17,7 @@ import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useModalPosition} from '@hooks/device';
import {useKeyboardOverlap} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {dismissModal, setButtons} from '@screens/navigation';
import {alertErrorWithFallback} from '@utils/draft';
@@ -116,13 +116,13 @@ export default function CreateDirectMessage({
const {formatMessage} = intl;
const mainView = useRef<View>(null);
const modalPosition = useModalPosition(mainView);
const [containerHeight, setContainerHeight] = useState(0);
const keyboardOverlap = useKeyboardOverlap(mainView, containerHeight);
const [term, setTerm] = useState('');
const [startingConversation, setStartingConversation] = useState(false);
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
const [showToast, setShowToast] = useState(false);
const [containerHeight, setContainerHeight] = useState(0);
const selectedCount = Object.keys(selectedIds).length;
const clearSearch = useCallback(() => {
@@ -327,8 +327,7 @@ export default function CreateDirectMessage({
createFilter={createUserFilter}
/>
<SelectedUsers
containerHeight={containerHeight}
modalPosition={modalPosition}
keyboardOverlap={keyboardOverlap}
showToast={showToast}
setShowToast={setShowToast}
toastIcon={'check'}

View File

@@ -12,10 +12,9 @@ import {
type NativeSyntheticEvent,
type NativeScrollEvent,
Platform,
useWindowDimensions,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import {SafeAreaView} from 'react-native-safe-area-context';
import Autocomplete from '@components/autocomplete';
import ErrorText from '@components/error_text';
@@ -26,7 +25,7 @@ import OptionItem from '@components/option_item';
import {General, Channel} from '@constants';
import {useTheme} from '@context/theme';
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device';
import {useKeyboardHeight, useKeyboardOverlap} from '@hooks/device';
import {useInputPropagation} from '@hooks/input';
import {t} from '@i18n';
import {
@@ -108,7 +107,6 @@ export default function ChannelInfoForm({
}: Props) {
const intl = useIntl();
const {formatMessage} = intl;
const insets = useSafeAreaInsets();
const theme = useTheme();
const styles = getStyleSheet(theme);
@@ -122,10 +120,8 @@ export default function ChannelInfoForm({
const updateScrollTimeout = useRef<NodeJS.Timeout>();
const mainView = useRef<View>(null);
const modalPosition = useModalPosition(mainView);
const dimensions = useWindowDimensions();
const isTablet = useIsTablet();
const [wrapperHeight, setWrapperHeight] = useState(0);
const keyboardOverlap = useKeyboardOverlap(mainView, wrapperHeight);
const [propagateValue, shouldProcessEvent] = useInputPropagation();
@@ -133,7 +129,6 @@ export default function ChannelInfoForm({
const [keyboardVisible, setKeyBoardVisible] = useState(false);
const [scrollPosition, setScrollPosition] = useState(0);
const [wrapperHeight, setWrapperHeight] = useState(0);
const [errorHeight, setErrorHeight] = useState(0);
const [displayNameFieldHeight, setDisplayNameFieldHeight] = useState(0);
const [makePrivateHeight, setMakePrivateHeight] = useState(0);
@@ -228,29 +223,17 @@ export default function ChannelInfoForm({
setWrapperHeight(e.nativeEvent.layout.height);
}, []);
const bottomSpace = (dimensions.height - wrapperHeight - modalPosition);
const otherElementsSize = LIST_PADDING + errorHeight +
(showSelector ? makePrivateHeight + MAKE_PRIVATE_MARGIN_BOTTOM : 0) +
(displayHeaderOnly ? 0 : purposeFieldHeight + FIELD_MARGIN_BOTTOM + displayNameFieldHeight + FIELD_MARGIN_BOTTOM);
const keyboardOverlap = Platform.select({
ios: isTablet ?
Math.max(0, keyboardHeight - bottomSpace) :
keyboardHeight || insets.bottom,
default: 0});
const workingSpace = wrapperHeight - keyboardOverlap;
const spaceOnTop = otherElementsSize - scrollPosition - AUTOCOMPLETE_ADJUST;
const spaceOnBottom = (workingSpace + scrollPosition) - (otherElementsSize + headerFieldHeight + BOTTOM_AUTOCOMPLETE_SEPARATION);
const insetsAdjust = keyboardHeight || insets.bottom;
const keyboardAdjust = Platform.select({
ios: isTablet ?
keyboardOverlap :
insetsAdjust,
default: 0,
});
const autocompletePosition = spaceOnBottom > spaceOnTop ?
(otherElementsSize + headerFieldHeight) - scrollPosition :
(workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardAdjust) - otherElementsSize;
(workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardOverlap) - otherElementsSize;
const autocompleteAvailableSpace = spaceOnBottom > spaceOnTop ? spaceOnBottom : spaceOnTop;
const growDown = spaceOnBottom > spaceOnTop;

View File

@@ -3,8 +3,7 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Alert, Keyboard, type LayoutChangeEvent, Platform, SafeAreaView, useWindowDimensions, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Alert, Keyboard, type LayoutChangeEvent, Platform, SafeAreaView, View} from 'react-native';
import {deletePost, editPost} from '@actions/remote/post';
import Autocomplete from '@components/autocomplete';
@@ -13,7 +12,7 @@ import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device';
import {useKeyboardOverlap} from '@hooks/device';
import useDidUpdate from '@hooks/did_update';
import {useInputPropagation} from '@hooks/input';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
@@ -66,27 +65,15 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
const [propagateValue, shouldProcessEvent] = useInputPropagation();
const mainView = useRef<View>(null);
const modalPosition = useModalPosition(mainView);
const postInputRef = useRef<EditPostInputRef>(null);
const theme = useTheme();
const intl = useIntl();
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
const keyboardHeight = useKeyboardHeight();
const insets = useSafeAreaInsets();
const dimensions = useWindowDimensions();
const isTablet = useIsTablet();
useEffect(() => {
setButtons(componentId, {
rightButtons: [{
color: theme.sidebarHeaderTextColor,
text: intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}),
...RIGHT_BUTTON,
enabled: false,
}],
});
toggleSaveButton(false);
}, []);
useEffect(() => {
@@ -211,11 +198,8 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
useNavButtonPressed(closeButtonId, componentId, onClose, []);
useAndroidHardwareBackHandler(componentId, onClose);
const bottomSpace = (dimensions.height - containerHeight - modalPosition);
const autocompletePosition = Platform.select({
ios: isTablet ? Math.max(0, keyboardHeight - bottomSpace) : keyboardHeight || insets.bottom,
default: 0,
}) + AUTOCOMPLETE_SEPARATION;
const overlap = useKeyboardOverlap(mainView, containerHeight);
const autocompletePosition = overlap + AUTOCOMPLETE_SEPARATION;
const autocompleteAvailableSpace = containerHeight - autocompletePosition;
const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace);

View File

@@ -38,7 +38,7 @@ type Props = {
channelsMatchStart: ChannelModel[];
currentTeamId: string;
isCRTEnabled: boolean;
keyboardHeight: number;
keyboardOverlap: number;
loading: boolean;
onLoading: (loading: boolean) => void;
restrictDirectMessage: boolean;
@@ -75,7 +75,7 @@ const sortByUserOrChannel = <T extends Channel |UserModel>(locale: string, teamm
const FilteredList = ({
archivedChannels, close, channelsMatch, channelsMatchStart, currentTeamId,
isCRTEnabled, keyboardHeight, loading, onLoading, restrictDirectMessage, showTeamName,
isCRTEnabled, keyboardOverlap, loading, onLoading, restrictDirectMessage, showTeamName,
teamIds, teammateDisplayNameSetting, term, usersMatch, usersMatchStart, testID,
}: Props) => {
const bounce = useRef<DebouncedFunc<() => void>>();
@@ -83,7 +83,7 @@ const FilteredList = ({
const serverUrl = useServerUrl();
const theme = useTheme();
const {locale, formatMessage} = useIntl();
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardHeight}), [keyboardHeight]);
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardOverlap}), [keyboardOverlap]);
const [remoteChannels, setRemoteChannels] = useState<RemoteChannels>({archived: [], startWith: [], matches: []});
const totalLocalResults = channelsMatchStart.length + channelsMatch.length + usersMatchStart.length;

View File

@@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useState} from 'react';
import {Keyboard, View} from 'react-native';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Keyboard, type LayoutChangeEvent, View} from 'react-native';
import SearchBar from '@components/search';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useKeyboardHeight} from '@hooks/device';
import {useKeyboardOverlap} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {dismissModal} from '@screens/navigation';
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
@@ -48,7 +48,10 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
const [loading, setLoading] = useState(false);
const styles = getStyleSheet(theme);
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
const keyboardHeight = useKeyboardHeight();
const listView = useRef<View>(null);
const [containerHeight, setContainerHeight] = useState(0);
const overlap = useKeyboardOverlap(listView, containerHeight);
const cancelButtonProps = useMemo(() => ({
color,
@@ -57,6 +60,10 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
},
}), [color]);
const onLayout = useCallback((e: LayoutChangeEvent) => {
setContainerHeight(e.nativeEvent.layout.height);
}, []);
const close = useCallback(() => {
Keyboard.dismiss();
return dismissModal({componentId});
@@ -99,18 +106,22 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
testID='find_channels.search_bar'
/>
{term === '' && <QuickOptions close={close}/>}
<View style={styles.listContainer}>
<View
style={styles.listContainer}
onLayout={onLayout}
ref={listView}
>
{term === '' &&
<UnfilteredList
close={close}
keyboardHeight={keyboardHeight}
keyboardOverlap={overlap}
testID='find_channels.unfiltered_list'
/>
}
{Boolean(term) &&
<FilteredList
close={close}
keyboardHeight={keyboardHeight}
keyboardOverlap={overlap}
loading={loading}
onLoading={setLoading}
term={term}

View File

@@ -17,7 +17,7 @@ import type ChannelModel from '@typings/database/models/servers/channel';
type Props = {
close: () => Promise<void>;
keyboardHeight: number;
keyboardOverlap: number;
recentChannels: ChannelModel[];
showTeamName: boolean;
testID?: string;
@@ -46,11 +46,11 @@ const buildSections = (recentChannels: ChannelModel[]) => {
return sections;
};
const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, testID}: Props) => {
const UnfilteredList = ({close, keyboardOverlap, recentChannels, showTeamName, testID}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const [sections, setSections] = useState(buildSections(recentChannels));
const sectionListStyle = useMemo(() => ({paddingBottom: keyboardHeight}), [keyboardHeight]);
const sectionListStyle = useMemo(() => ({paddingBottom: keyboardOverlap}), [keyboardOverlap]);
const onPress = useCallback(async (c: Channel | ChannelModel) => {
await close();

View File

@@ -54,7 +54,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
marginTop: 20,
},
header: {
color: theme.mentionColor,
color: theme.centerChannelColor,
marginBottom: 12,
...typography('Heading', 1000, 'SemiBold'),
},
@@ -84,7 +84,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
textAlign: 'center',
},
successTitle: {
color: theme.mentionColor,
color: theme.centerChannelColor,
marginBottom: 12,
...typography('Heading', 1000),
},

View File

@@ -13,7 +13,7 @@ import Loading from '@components/loading';
import {ServerErrors} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useModalPosition} from '@hooks/device';
import {useKeyboardOverlap} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {dismissModal, setButtons} from '@screens/navigation';
import {isEmail} from '@utils/helpers';
@@ -112,8 +112,10 @@ export default function Invite({
const theme = useTheme();
const styles = getStyleSheet(theme);
const serverUrl = useServerUrl();
const mainView = useRef<View>(null);
const modalPosition = useModalPosition(mainView);
const [wrapperHeight, setWrapperHeight] = useState(0);
const keyboardOverlap = useKeyboardOverlap(mainView, wrapperHeight);
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
const retryTimeoutId = useRef<NodeJS.Timeout | null>(null);
@@ -123,7 +125,6 @@ export default function Invite({
const [selectedIds, setSelectedIds] = useState<{[id: string]: SearchResult}>({});
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<Result>(DEFAULT_RESULT);
const [wrapperHeight, setWrapperHeight] = useState(0);
const [stage, setStage] = useState(Stage.SELECTION);
const [sendError, setSendError] = useState('');
@@ -394,7 +395,7 @@ export default function Invite({
term={term}
searchResults={searchResults}
selectedIds={selectedIds}
modalPosition={modalPosition}
keyboardOverlap={keyboardOverlap}
wrapperHeight={wrapperHeight}
loading={loading}
onSearchChange={handleSearchChange}

View File

@@ -13,7 +13,6 @@ import {
ScrollView,
} from 'react-native';
import Animated, {useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import SelectedChip from '@components/selected_chip';
import SelectedUser from '@components/selected_users/selected_user';
@@ -22,7 +21,7 @@ import UserItem from '@components/user_item';
import {MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete';
import {useTheme} from '@context/theme';
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
import {useIsTablet, useKeyboardHeight} from '@hooks/device';
import {useIsTablet} from '@hooks/device';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import SelectionSearchBar from './selection_search_bar';
@@ -32,7 +31,6 @@ import TextItem, {TextItemType} from './text_item';
import type {SearchResult} from './invite';
const AUTOCOMPLETE_ADJUST = 5;
const KEYBOARD_HEIGHT_ADJUST = 3;
const INITIAL_BATCH_TO_RENDER = 15;
const SCROLL_EVENT_THROTTLE = 60;
@@ -112,7 +110,7 @@ type SelectionProps = {
term: string;
searchResults: SearchResult[];
selectedIds: {[id: string]: SearchResult};
modalPosition: number;
keyboardOverlap: number;
wrapperHeight: number;
loading: boolean;
testID: string;
@@ -132,7 +130,7 @@ export default function Selection({
term,
searchResults,
selectedIds,
modalPosition,
keyboardOverlap,
wrapperHeight,
loading,
testID,
@@ -144,9 +142,7 @@ export default function Selection({
const theme = useTheme();
const styles = getStyleSheet(theme);
const dimensions = useWindowDimensions();
const insets = useSafeAreaInsets();
const isTablet = useIsTablet();
const keyboardHeight = useKeyboardHeight();
const [teamBarHeight, setTeamBarHeight] = useState(0);
const [searchBarHeight, setSearchBarHeight] = useState(0);
@@ -163,26 +159,10 @@ export default function Selection({
onRemoveItem(id);
};
const bottomSpace = dimensions.height - wrapperHeight - modalPosition;
const otherElementsSize = teamBarHeight + searchBarHeight;
const insetsAdjust = (keyboardHeight + KEYBOARD_HEIGHT_ADJUST) || insets.bottom;
const keyboardOverlap = Platform.select({
ios: isTablet ? (
Math.max(0, keyboardHeight - bottomSpace)
) : (
insetsAdjust
),
default: 0,
});
const keyboardAdjust = Platform.select({
ios: isTablet ? keyboardOverlap : insetsAdjust,
default: 0,
});
const workingSpace = wrapperHeight - keyboardOverlap;
const spaceOnTop = otherElementsSize - AUTOCOMPLETE_ADJUST;
const spaceOnBottom = workingSpace - (otherElementsSize + keyboardAdjust);
const spaceOnBottom = workingSpace - otherElementsSize;
const autocompletePosition = spaceOnBottom > spaceOnTop ? (
otherElementsSize
) : (

View File

@@ -57,7 +57,7 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
flex: 1,
},
header: {
color: theme.mentionColor,
color: theme.centerChannelColor,
marginBottom: 12,
...typography('Heading', 1000, 'SemiBold'),
},

View File

@@ -10,7 +10,7 @@ import {observeTutorialWatched} from '@queries/app/global';
import {observeCurrentChannel} from '@queries/servers/channel';
import {observeCanManageChannelMembers, observePermissionForChannel} from '@queries/servers/role';
import {observeCurrentChannelId, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeCurrentUser} from '@queries/servers/user';
import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user';
import ManageChannelMembers from './manage_channel_members';
@@ -22,10 +22,14 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const currentChannel = observeCurrentChannel(database);
const canManageAndRemoveMembers = combineLatest([currentChannelId, currentUser]).pipe(
switchMap(([cId, u]) => (cId && u ? observeCanManageChannelMembers(database, cId, u) : of$(false))));
switchMap(([cId, u]) => (cId && u ? observeCanManageChannelMembers(database, cId, u) : of$(false))),
);
const canChangeMemberRoles = combineLatest([currentChannel, currentUser, canManageAndRemoveMembers]).pipe(
switchMap(([c, u, m]) => (of$(c) && of$(u) && of$(m) && observePermissionForChannel(database, c, u, Permissions.MANAGE_CHANNEL_ROLES, true))));
switchMap(([c, u, m]) => (of$(c) && of$(u) && of$(m) && observePermissionForChannel(database, c, u, Permissions.MANAGE_CHANNEL_ROLES, true))),
);
const teammateDisplayNameSetting = observeTeammateNameDisplay(database);
return {
currentUserId: observeCurrentUserId(database),
@@ -33,6 +37,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
canManageAndRemoveMembers,
tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS),
canChangeMemberRoles,
teammateDisplayNameSetting,
};
});

View File

@@ -8,18 +8,18 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import {fetchChannelMemberships} from '@actions/remote/channel';
import {fetchUsersByIds, searchProfiles} from '@actions/remote/user';
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
import Search from '@components/search';
import UserList from '@components/user_list';
import {Events, General, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {openAsBottomSheet, setButtons} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {showRemoveChannelUserSnackbar} from '@utils/snack_bar';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import {filterProfilesMatchingTerm} from '@utils/user';
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
import type {AvailableScreens} from '@typings/screens/navigation';
@@ -30,6 +30,7 @@ type Props = {
currentTeamId: string;
currentUserId: string;
tutorialWatched: boolean;
teammateDisplayNameSetting: string;
}
const styles = StyleSheet.create({
@@ -54,6 +55,12 @@ const messages = defineMessages({
},
});
const sortUsers = (a: UserProfile, b: UserProfile, locale: string, teammateDisplayNameSetting: string) => {
const aName = displayUsername(a, locale, teammateDisplayNameSetting);
const bName = displayUsername(b, locale, teammateDisplayNameSetting);
return aName.localeCompare(bName, locale);
};
const MANAGE_BUTTON = 'manage-button';
const EMPTY: UserProfile[] = [];
const EMPTY_MEMBERS: ChannelMembership[] = [];
@@ -68,47 +75,29 @@ export default function ManageChannelMembers({
currentTeamId,
currentUserId,
tutorialWatched,
teammateDisplayNameSetting,
}: Props) {
const serverUrl = useServerUrl();
const theme = useTheme();
const {formatMessage} = useIntl();
const {formatMessage, locale} = useIntl();
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
const mounted = useRef(false);
const [isManageMode, setIsManageMode] = useState(false);
const [profiles, setProfiles] = useState<UserProfile[]>(EMPTY);
const hasMoreProfiles = useRef(false);
const [channelMembers, setChannelMembers] = useState<ChannelMembership[]>(EMPTY_MEMBERS);
const [searchResults, setSearchResults] = useState<UserProfile[]>(EMPTY);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [term, setTerm] = useState('');
const loadedProfiles = (users: UserProfile[], members: ChannelMembership[]) => {
if (mounted.current) {
setLoading(false);
setProfiles(users);
setChannelMembers(members);
}
};
const [searchedTerm, setSearchedTerm] = useState('');
const clearSearch = useCallback(() => {
setTerm('');
setSearchResults(EMPTY);
}, []);
const getProfiles = useCallback(debounce(async () => {
const hasTerm = Boolean(term);
if (!loading && !hasTerm && mounted.current) {
setLoading(true);
const options = {sort: 'admin', active: true};
const {users, members} = await fetchChannelMemberships(serverUrl, channelId, options, true);
if (users.length) {
loadedProfiles(users, members);
}
setLoading(false);
}
}, 100), [channelId, loading, serverUrl, term]);
const handleSelectProfile = useCallback(async (profile: UserProfile) => {
if (profile.id === currentUserId && isManageMode) {
return;
@@ -133,15 +122,19 @@ export default function ManageChannelMembers({
}, [canManageAndRemoveMembers, channelId, isManageMode, currentUserId]);
const searchUsers = useCallback(async (searchTerm: string) => {
setSearchedTerm(searchTerm);
if (!hasMoreProfiles.current) {
return;
}
const lowerCasedTerm = searchTerm.toLowerCase();
setLoading(true);
const options: SearchUserOptions = {team_id: currentTeamId, in_channel_id: channelId, allow_inactive: false};
const {data = EMPTY} = await searchProfiles(serverUrl, lowerCasedTerm, options);
setSearchResults(data);
setSearchResults(data.sort((a, b) => sortUsers(a, b, locale, teammateDisplayNameSetting)));
setLoading(false);
}, [serverUrl, channelId, currentTeamId]);
}, [serverUrl, channelId, currentTeamId, locale, teammateDisplayNameSetting]);
const search = useCallback(() => {
searchUsers(term);
@@ -210,19 +203,44 @@ export default function ManageChannelMembers({
setChannelMembers(clone);
}, [channelMembers]);
const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => {
return sortUsers(a, b, locale, teammateDisplayNameSetting);
}), [profiles, locale, teammateDisplayNameSetting]);
const data = useMemo(() => {
const isSearch = Boolean(term);
const isSearch = Boolean(searchedTerm);
if (isSearch) {
return filterProfilesMatchingTerm(searchResults, term);
return filterProfilesMatchingTerm(searchResults.length ? searchResults : sortedProfiles, searchedTerm);
}
return profiles;
}, [term, searchResults, profiles]);
}, [searchResults, profiles, searchedTerm, sortedProfiles]);
useEffect(() => {
if (!term) {
setSearchResults(EMPTY);
setSearchedTerm('');
}
}, [Boolean(term)]);
useNavButtonPressed(MANAGE_BUTTON, componentId, toggleManageEnabled, [toggleManageEnabled]);
useEffect(() => {
mounted.current = true;
getProfiles();
const options: GetUsersOptions = {sort: 'admin', active: true, per_page: PER_PAGE_DEFAULT};
fetchChannelMemberships(serverUrl, channelId, options, true).then(({users, members}) => {
if (!mounted.current) {
return;
}
if (users.length >= PER_PAGE_DEFAULT) {
hasMoreProfiles.current = true;
}
if (users.length) {
setProfiles(users);
setChannelMembers(members);
}
setLoading(false);
});
return () => {
mounted.current = false;
};
@@ -272,7 +290,7 @@ export default function ManageChannelMembers({
selectedIds={EMPTY_IDS}
showManageMode={canManageAndRemoveMembers && isManageMode}
showNoResults={!loading}
term={term}
term={searchedTerm}
testID='manage_members.user_list'
tutorialWatched={tutorialWatched}
includeUserMargin={true}

View File

@@ -61,7 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
marginTop: 20,
},
header: {
color: theme.mentionColor,
color: theme.centerChannelColor,
marginBottom: 12,
...typography('Heading', 1000, 'SemiBold'),
},

View File

@@ -51,7 +51,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
...typography('Body', 100, 'Regular'),
},
infoTitle: {
color: theme.mentionColor,
color: theme.centerChannelColor,
marginBottom: 4,
...typography('Heading', 700),
},

View File

@@ -72,7 +72,7 @@ export const notificationError = (intl: IntlShape, type: 'Team' | 'Channel' | 'C
case 'Connection':
message = intl.formatMessage({
id: 'notification.no_connection',
defaultMessage: 'The server is unreachable and we were not able to retrieve the notification channel / team.',
defaultMessage: 'The server is unreachable and it was not possible to retrieve the specific message information for the notification.',
});
break;
}

View File

@@ -312,7 +312,7 @@ export function filterProfilesMatchingTerm(users: UserProfile[], term: string):
return profileSuggestions.
filter((suggestion) => suggestion !== '').
some((suggestion) => suggestion.startsWith(trimmedTerm));
some((suggestion) => suggestion.includes(trimmedTerm));
});
}

View File

@@ -765,7 +765,7 @@
"notification_settings.threads_start": "Threads that I start",
"notification_settings.threads_start_participate": "Threads that I start or participate in",
"notification.message_not_found": "Message not found",
"notification.no_connection": "The server is unreachable and we were not able to retrieve the notification channel / team.",
"notification.no_connection": "The server is unreachable and it was not possible to retrieve the specific message information for the notification.",
"notification.no_post": "The message has not been found.",
"notification.not_channel_member": "This message belongs to a channel where you are not a member.",
"notification.not_team_member": "This message belongs to a team where you are not a member.",

BIN
fastlane/metadata/android/en-US/images/icon.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1655,7 +1655,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 471;
CURRENT_PROJECT_VERSION = 472;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@@ -1699,7 +1699,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 471;
CURRENT_PROJECT_VERSION = 472;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@@ -1842,7 +1842,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 471;
CURRENT_PROJECT_VERSION = 472;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -1893,7 +1893,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 471;
CURRENT_PROJECT_VERSION = 472;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;

View File

@@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>471</string>
<string>472</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>2.4.0</string>
<key>CFBundleVersion</key>
<string>471</string>
<string>472</string>
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>

View File

@@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>2.4.0</string>
<key>CFBundleVersion</key>
<string>471</string>
<string>472</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -85,7 +85,7 @@ PODS:
- HMSegmentedControl (1.5.6)
- jail-monkey (2.8.0):
- React-Core
- JitsiWebRTC (111.0.1)
- JitsiWebRTC (111.0.2)
- libevent (2.1.12)
- libwebp (1.2.4):
- libwebp/demux (= 1.2.4)
@@ -570,7 +570,7 @@ PODS:
- React-Core
- RNVectorIcons (9.2.0):
- React-Core
- Rudder (1.14.0)
- Rudder (1.15.1)
- SDWebImage (5.12.6):
- SDWebImage/Core (= 5.12.6)
- SDWebImage/Core (5.12.6)
@@ -920,7 +920,7 @@ SPEC CHECKSUMS:
hermes-engine: 4438d2b8bf8bebaba1b1ac0451160bab59e491f8
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
jail-monkey: a71b35d482a70ecba844a90f002994012cf12a5d
JitsiWebRTC: 9619c1f71cc16eeca76df68aa2d213c6d63274a8
JitsiWebRTC: 80f62908fcf2a1160e0d14b584323fb6e6be630b
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
mattermost-react-native-turbo-log: a00b39dafdef7905164110466e7d725f6f079751
@@ -992,7 +992,7 @@ SPEC CHECKSUMS:
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
Rudder: 41523d90e7f8040605ca6a803f662a538144c90f
Rudder: 41040d4537a178e4e32477b68400f98ca0c354eb
SDWebImage: a47aea9e3d8816015db4e523daff50cfd294499d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
Sentry: 16d46dd5ca10e7f4469a2611805a3de123188add

2
package-lock.json generated
View File

@@ -26183,7 +26183,7 @@
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "*",
"@types/react": "^18.0.35",
"hoist-non-react-statics": "^3.3.0"
}
},

View File

@@ -0,0 +1,70 @@
diff --git a/node_modules/@sentry/utils/cjs/object.js b/node_modules/@sentry/utils/cjs/object.js
index eb89fb8..0716abb 100644
--- a/node_modules/@sentry/utils/cjs/object.js
+++ b/node_modules/@sentry/utils/cjs/object.js
@@ -198,7 +198,11 @@ function dropUndefinedKeys(inputValue) {
return _dropUndefinedKeys(inputValue, memoizationMap);
}
-function _dropUndefinedKeys(inputValue, memoizationMap) {
+function _dropUndefinedKeys(inputValue, memoizationMap, depth = 0) {
+ if (depth >= 5) {
+ return inputValue;
+ }
+
if (is.isPlainObject(inputValue)) {
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
const memoVal = memoizationMap.get(inputValue);
@@ -212,7 +216,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
for (const key of Object.keys(inputValue)) {
if (typeof inputValue[key] !== 'undefined') {
- returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap);
+ returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap, depth + 1);
}
}
@@ -231,7 +235,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
memoizationMap.set(inputValue, returnValue);
inputValue.forEach((item) => {
- returnValue.push(_dropUndefinedKeys(item, memoizationMap));
+ returnValue.push(_dropUndefinedKeys(item, memoizationMap, depth + 1));
});
return returnValue ;
diff --git a/node_modules/@sentry/utils/esm/object.js b/node_modules/@sentry/utils/esm/object.js
index 0f5c411..1a8b5c9 100644
--- a/node_modules/@sentry/utils/esm/object.js
+++ b/node_modules/@sentry/utils/esm/object.js
@@ -196,7 +196,11 @@ function dropUndefinedKeys(inputValue) {
return _dropUndefinedKeys(inputValue, memoizationMap);
}
-function _dropUndefinedKeys(inputValue, memoizationMap) {
+function _dropUndefinedKeys(inputValue, memoizationMap, depth = 0) {
+ if (depth >= 5) {
+ return inputValue;
+ }
+
if (isPlainObject(inputValue)) {
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
const memoVal = memoizationMap.get(inputValue);
@@ -210,7 +214,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
for (const key of Object.keys(inputValue)) {
if (typeof inputValue[key] !== 'undefined') {
- returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap);
+ returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap, depth + 1);
}
}
@@ -229,7 +233,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
memoizationMap.set(inputValue, returnValue);
inputValue.forEach((item) => {
- returnValue.push(_dropUndefinedKeys(item, memoizationMap));
+ returnValue.push(_dropUndefinedKeys(item, memoizationMap, depth + 1));
});
return returnValue ;