diff --git a/android/app/build.gradle b/android/app/build.gradle index f793ebd981..a9ef790225 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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' diff --git a/app/actions/remote/groups.ts b/app/actions/remote/groups.ts index 205c38b75b..e402226b3c 100644 --- a/app/actions/remote/groups.ts +++ b/app/actions/remote/groups.ts @@ -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 > = []; @@ -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); diff --git a/app/client/rest/groups.ts b/app/client/rest/groups.ts index 61b608f6bb..88ef1eaccd 100644 --- a/app/client/rest/groups.ts +++ b/app/client/rest/groups.ts @@ -8,24 +8,13 @@ import {PER_PAGE_DEFAULT} from './constants'; import type ClientBase from './base'; export interface ClientGroupsMix { - getGroup: (id: string) => Promise; getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise; getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>; getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean) => Promise; 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 = >(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 = >(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; diff --git a/app/components/selected_users/index.tsx b/app/components/selected_users/index.tsx index 98ff1ee620..983c63b694 100644 --- a/app/components/selected_users/index.tsx +++ b/app/components/selected_users/index.tsx @@ -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 { diff --git a/app/helpers/api/user.ts b/app/helpers/api/user.ts index 79bd792dc9..f0576f5841 100644 --- a/app/helpers/api/user.ts +++ b/app/helpers/api/user.ts @@ -7,9 +7,13 @@ import {MENTIONS_REGEX} from '@constants/autocomplete'; export const getNeededAtMentionedUsernames = (usernames: Set, posts: Post[], excludeUsername?: string) => { const usernamesToLoad = new Set(); - 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, 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; }; diff --git a/app/hooks/device.ts b/app/hooks/device.ts index 768f09b5bb..1f3e0a5e07 100644 --- a/app/hooks/device.ts +++ b/app/hooks/device.ts @@ -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, deps: React.DependencyList = []) { +export function useViewPosition(viewRef: RefObject, deps: React.DependencyList = []) { const [modalPosition, setModalPosition] = useState(0); const isTablet = useIsTablet(); const height = useKeyboardHeight(); @@ -127,3 +127,21 @@ export function useModalPosition(viewRef: RefObject, deps: React.Dependenc return modalPosition; } + +export function useKeyboardOverlap(viewRef: RefObject, 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; +} diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index 4dcb2e4ee4..c869be8914 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -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); diff --git a/app/screens/channel_add_members/channel_add_members.tsx b/app/screens/channel_add_members/channel_add_members.tsx index ce8b5add7b..c60efcf15e 100644 --- a/app/screens/channel_add_members/channel_add_members.tsx +++ b/app/screens/channel_add_members/channel_add_members.tsx @@ -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(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} /> (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} /> (); const mainView = useRef(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; diff --git a/app/screens/edit_post/edit_post.tsx b/app/screens/edit_post/edit_post.tsx index e369d11eb2..a0f07d9536 100644 --- a/app/screens/edit_post/edit_post.tsx +++ b/app/screens/edit_post/edit_post.tsx @@ -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(null); - const modalPosition = useModalPosition(mainView); const postInputRef = useRef(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); diff --git a/app/screens/find_channels/filtered_list/filtered_list.tsx b/app/screens/find_channels/filtered_list/filtered_list.tsx index c8c1f18273..d441a2a852 100644 --- a/app/screens/find_channels/filtered_list/filtered_list.tsx +++ b/app/screens/find_channels/filtered_list/filtered_list.tsx @@ -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 = (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 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({archived: [], startWith: [], matches: []}); const totalLocalResults = channelsMatchStart.length + channelsMatch.length + usersMatchStart.length; diff --git a/app/screens/find_channels/index.tsx b/app/screens/find_channels/index.tsx index 37e5176c24..aae3f173a0 100644 --- a/app/screens/find_channels/index.tsx +++ b/app/screens/find_channels/index.tsx @@ -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(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 === '' && } - + {term === '' && } {Boolean(term) && Promise; - 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(); diff --git a/app/screens/forgot_password/index.tsx b/app/screens/forgot_password/index.tsx index 2b6e95236a..b27dbb693b 100644 --- a/app/screens/forgot_password/index.tsx +++ b/app/screens/forgot_password/index.tsx @@ -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), }, diff --git a/app/screens/invite/invite.tsx b/app/screens/invite/invite.tsx index 3c784e7419..fd81ccea49 100644 --- a/app/screens/invite/invite.tsx +++ b/app/screens/invite/invite.tsx @@ -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(null); - const modalPosition = useModalPosition(mainView); + const [wrapperHeight, setWrapperHeight] = useState(0); + const keyboardOverlap = useKeyboardOverlap(mainView, wrapperHeight); const searchTimeoutId = useRef(null); const retryTimeoutId = useRef(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(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} diff --git a/app/screens/invite/selection.tsx b/app/screens/invite/selection.tsx index d6065fbb92..b243fb6e8e 100644 --- a/app/screens/invite/selection.tsx +++ b/app/screens/invite/selection.tsx @@ -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 ) : ( diff --git a/app/screens/login/index.tsx b/app/screens/login/index.tsx index 6e1e70777a..70ddde7237 100644 --- a/app/screens/login/index.tsx +++ b/app/screens/login/index.tsx @@ -57,7 +57,7 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({ flex: 1, }, header: { - color: theme.mentionColor, + color: theme.centerChannelColor, marginBottom: 12, ...typography('Heading', 1000, 'SemiBold'), }, diff --git a/app/screens/manage_channel_members/index.tsx b/app/screens/manage_channel_members/index.tsx index d1b7dca7a1..b8ed1a3082 100644 --- a/app/screens/manage_channel_members/index.tsx +++ b/app/screens/manage_channel_members/index.tsx @@ -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, }; }); diff --git a/app/screens/manage_channel_members/manage_channel_members.tsx b/app/screens/manage_channel_members/manage_channel_members.tsx index b78bf90fce..8ecb395c86 100644 --- a/app/screens/manage_channel_members/manage_channel_members.tsx +++ b/app/screens/manage_channel_members/manage_channel_members.tsx @@ -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(null); const mounted = useRef(false); const [isManageMode, setIsManageMode] = useState(false); const [profiles, setProfiles] = useState(EMPTY); + const hasMoreProfiles = useRef(false); const [channelMembers, setChannelMembers] = useState(EMPTY_MEMBERS); const [searchResults, setSearchResults] = useState(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} diff --git a/app/screens/mfa/index.tsx b/app/screens/mfa/index.tsx index 91781076db..f59aa3f6e5 100644 --- a/app/screens/mfa/index.tsx +++ b/app/screens/mfa/index.tsx @@ -61,7 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ marginTop: 20, }, header: { - color: theme.mentionColor, + color: theme.centerChannelColor, marginBottom: 12, ...typography('Heading', 1000, 'SemiBold'), }, diff --git a/app/screens/sso/sso_with_redirect_url.tsx b/app/screens/sso/sso_with_redirect_url.tsx index 7458363b9e..2238e48222 100644 --- a/app/screens/sso/sso_with_redirect_url.tsx +++ b/app/screens/sso/sso_with_redirect_url.tsx @@ -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), }, diff --git a/app/utils/notification/index.ts b/app/utils/notification/index.ts index 37b70d08f4..b53c096c89 100644 --- a/app/utils/notification/index.ts +++ b/app/utils/notification/index.ts @@ -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; } diff --git a/app/utils/user/index.ts b/app/utils/user/index.ts index 496bfa0e68..f5fbbc4798 100644 --- a/app/utils/user/index.ts +++ b/app/utils/user/index.ts @@ -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)); }); } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 5424b34eb8..d3f8a7ca9c 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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.", diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png old mode 100755 new mode 100644 index 16d632182f..9f246e428b Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index d806060aaf..3927c110cc 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -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; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 401610d726..6f5ad9f60e 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -37,7 +37,7 @@ CFBundleVersion - 471 + 472 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 734485a0b1..b0f156c7dd 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.4.0 CFBundleVersion - 471 + 472 UIAppFonts OpenSans-Bold.ttf diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 5268971a5b..fe2fd4c7b9 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.4.0 CFBundleVersion - 471 + 472 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d05f73ea8c..efdb7cc792 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/package-lock.json b/package-lock.json index a536992ed0..332a570228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } }, diff --git a/patches/@sentry+utils+7.47.0.patch b/patches/@sentry+utils+7.47.0.patch new file mode 100644 index 0000000000..0b031102d9 --- /dev/null +++ b/patches/@sentry+utils+7.47.0.patch @@ -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 ;