From 19315c63c4b2de54490c6ac4d1256e211a6e69e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 23 May 2023 17:33:17 +0200 Subject: [PATCH 01/12] Improve manage members search (#7357) --- app/screens/manage_channel_members/index.tsx | 11 ++- .../manage_channel_members.tsx | 82 +++++++++++-------- app/utils/user/index.ts | 2 +- 3 files changed, 59 insertions(+), 36 deletions(-) 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/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)); }); } -- 2.49.1 From 68818d0048d54b697505f4cfd942f36533dc4077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 23 May 2023 17:34:31 +0200 Subject: [PATCH 02/12] Update string on notification error (#7370) --- app/utils/notification/index.ts | 2 +- assets/base/i18n/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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.", -- 2.49.1 From 3cad740d677c3e7b363fe50136bff144c3fe8737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 23 May 2023 17:36:48 +0200 Subject: [PATCH 03/12] Update .lock files (#7367) --- ios/Podfile.lock | 8 ++++---- package-lock.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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" } }, -- 2.49.1 From 83e5ca835de2b7d1000956dd3f6af789aaa8410b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 23 May 2023 17:39:27 +0200 Subject: [PATCH 04/12] Fix Save button not appearing on edit post screen (#7360) --- app/screens/edit_post/edit_post.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/screens/edit_post/edit_post.tsx b/app/screens/edit_post/edit_post.tsx index e369d11eb2..18880fd44c 100644 --- a/app/screens/edit_post/edit_post.tsx +++ b/app/screens/edit_post/edit_post.tsx @@ -79,14 +79,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach 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(() => { -- 2.49.1 From 0a89ac7cf386443e5be31ef75527adb231d607ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 23 May 2023 17:40:07 +0200 Subject: [PATCH 05/12] Fix minor error on marking threads as read (#7355) --- app/init/push_notifications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); -- 2.49.1 From 66d84ee97be3ffb6adff11629d4716a4797722db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 23 May 2023 23:29:39 +0200 Subject: [PATCH 06/12] Patch _dropUndefinedKeys to limit the depth (#7363) --- patches/@sentry+utils+7.47.0.patch | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 patches/@sentry+utils+7.47.0.patch 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 ; -- 2.49.1 From b6f62e35fb5234e91f48af71a656cc88a76fb53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 23 May 2023 23:33:37 +0200 Subject: [PATCH 07/12] Remove unneeded padding on the FindChannels screen on Android (#7368) * Remove unneeded padding on the FindChannels screen on Android * Refactor keyboard overlap --- app/components/selected_users/index.tsx | 52 +++---------------- app/hooks/device.ts | 22 +++++++- .../channel_add_members.tsx | 9 ++-- .../create_direct_message.tsx | 9 ++-- .../channel_info_form.tsx | 29 +++-------- app/screens/edit_post/edit_post.tsx | 17 ++---- .../filtered_list/filtered_list.tsx | 6 +-- app/screens/find_channels/index.tsx | 25 ++++++--- .../unfiltered_list/unfiltered_list.tsx | 6 +-- app/screens/invite/invite.tsx | 9 ++-- app/screens/invite/selection.tsx | 28 ++-------- 11 files changed, 78 insertions(+), 134 deletions(-) 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/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/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 18880fd44c..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,17 +65,12 @@ 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(() => { toggleSaveButton(false); @@ -204,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/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 ) : ( -- 2.49.1 From 5d20b26b7397e8195f1281bf6c779624093f2a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 24 May 2023 12:29:48 +0200 Subject: [PATCH 08/12] Add license checks to several groups calls (#7369) --- app/actions/remote/groups.ts | 49 ++++++++++++++++++++---------------- app/client/rest/groups.ts | 32 ----------------------- 2 files changed, 28 insertions(+), 53 deletions(-) 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; -- 2.49.1 From 9b2c68985acee996cb800beb589124745f5a20fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Thu, 25 May 2023 10:51:07 +0200 Subject: [PATCH 09/12] Fix all references to mention color on "login" screens (#7364) * Fix all references to mention color on "login" screens * Add missing change --- app/screens/forgot_password/index.tsx | 4 ++-- app/screens/login/index.tsx | 2 +- app/screens/mfa/index.tsx | 2 +- app/screens/sso/sso_with_redirect_url.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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/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/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), }, -- 2.49.1 From aa54d72a01e1ff905bba1d30bfde5c35c4c2f743 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 25 May 2023 13:47:09 -0400 Subject: [PATCH 10/12] MM-52888 Always load users mentioned in message attachments (#7361) --- app/helpers/api/user.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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; }; -- 2.49.1 From 825e70d94571b84bdeadaff3a05b3546211f8d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 26 May 2023 14:54:46 +0200 Subject: [PATCH 11/12] Substitute Google Play icon on Fastlane (#7374) --- .../metadata/android/en-US/images/icon.png | Bin 16427 -> 11788 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 fastlane/metadata/android/en-US/images/icon.png 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 16d632182fd5a2aa4feddcc1c14d0f44f3b10489..9f246e428b274480e79662b3d3bae168e4699c77 GIT binary patch literal 11788 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 AQUCw| literal 16427 zcmcKhc|6qL`v(rcW(?WMlC474k}Yf5#!l8s_BE2SC5a+4*%euevTG4V))bMMEM*&e z_K+AN>)1wP=03yw^Syt6-2dG7<8gcRc%;`k=Q`K=ysmSdQ@o|QAqyivBLqP##zuM< zAqWnBg+ugo;IEC4fn5mFS~u3ywhDp${rUiWgV6bVZ+YHQXZ`>22R%}IXxP3_8U1^x zTBE=F@;{NKUpbyFsLG`_vUq`ntk5#WV72_CF%I@oThN|Nj<5kM$xB*{NNypK(06N?K@+F!B}-&>_G2~GpU1o=R%-lqe5Tb zcp+H`N>2x)cd%~*csivJ{C=k?X&26I?P?hoUK3n1gvtkSnyCqx-NN;7uWWj`+9Rl^ zrhclem$jg)e>TP?+IAL0za2|aL-1cYO}QLE{N6-|#_3_-=^ufmQ(AXMk4?k;mserz zus<`SIr_**8Oq=3Bs#WxTQSlY3Vldmod=sn<&% zVuP+<$A7I+(?i}j%bmrXJ1;Sm^L%a!CX{0v>~S|~<{@-RumiP&HVjv`&kWV-enEq@ z&%7V#wf(O_3cjEMSwFp+j`|>;n zJ&3wXJS6>Rpp%B_>}|-%L|`RRO-ul`3?Gn$K30;l94Mh(Mli}U4>aMh^=R+jv|rmX z7?Z#g!XPTJ=gciE=NNauDkN7wDMCrA6Z7gKO6EWOy?_z(GGo1e{au{yA#_TUv#RKF zdVY%&dh`%-YXNi8i`72zqX((F>Y?gwC{9fb3|5mQk&spRy7S&A+GD@UWlm4`z=W2c z);L6bBn87v3;Ze1%E)7pUAM0+Dd`_Q2LS{G(XbpjuwYmKFC;EqU75^T+B#l!*5R>4HeylJSNE9<7Bqo)U_c$Nh*VUp_{va z#pV3KMHd>hv{~;_smvV@v zZAVJwNXQOEkA8r|91?;~9fut5!pa>WE;b}of2+v`o?qwejM$(>;%Tw2miw$QdpaXJ zx7i+;!eGgJa3KObAA<53VLiEf96)_S(5*07k1PZgSwb6gJK)oR5EO$zeuWU&WT>bc znzwg^L1>5_l0raG5EtZBOW{jC47I?KfD30Bgp8&EG7l(2nbdI($PAuO2R2f{!Xfl2 z1oC?yJw#p4>y-~^Ktd3hMg0PS_2BdWJ|*gaui#$*&k$$`f?z@r1U5MMgr*bw|GZ2w z+uyW1tZTw9Xc6ca#NgKF|2&U4zoqNu7*!Kgx-3&asH!$#=_guhVHZ5123zK?k$(q72oU#QQ5T!aKkdIT77{q#Q0d z3TUTEFa}FLPWOW6i1AxYr=8_)<#)_@i*jMsg$Z#%F||P#A&et;b8Awh?XA;s_a|cm zD&BEFz#~3KZV#SY(^}ysQ5@xU;Jh(#q$I@L@0}o+)V=B6s;s4qI>sTbnGxl7dS^gP znWAj`2_eT!i?)P#@puMUeN|&p#vvM2`yK@9($S176e1L^Y^g{Q6o;NdfbrKfj8K!K z%3y4;%})7@Ei|cSf7k-aD5IL$t5#?^Mn22n%FBqfc>&3F>MDHU#dO5JvT5{sEGi^W z-5}{FXjgKbd_V_m>6ZI^>D%8(O)jfXNYBg@4CDmzmPRDLqTw2mCzdQ~qB zS4cL3V;pB{tPh5fOw`pbyv>cTw5@7h$_f0wGZ1vAX@4mij+G0>)5`53dDBXHJKH~Z zYvla5DV641l8|yZw~n_WgF;}XWYp=kT9)^&6A1B*QoF{!;mfme)nXX)={ z_o(TKd|p`b@G3^iyRn`oF~mJQ8Xro2Z2c;g{2f+oPS?lC<6zHL^rLn)%roRI)VdvdDkC*4M6tC$ zEkfikZ}^91kKF0`%YB2KxjB+CJ1tFCWI}VTAjY>F*ve#o`9|NWsYMr(nF+wGe>^iy3EuUWi9hML}IE`T`+vg)=gp8-1JXgwNWwQw7Y;>LMQzUg-!2Nhv3>}UxOXj-t zyhX|EgP~{N1Y~+IwT#z4Y1BGavzGQ;X2ZQoxwYT-UT0;+94QVC(bH!q5vBc^<^GOZ%^Wq_t>@QB5Vy z*ypPf`=t~fo^=X^YK3yI@Y+T(^F{@xxl@^II|G%){u4gr69hKpmvwhk3nh-

~&+&gL@c7kToGll@<9XSuMVFC5HKc+e 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

    (|G)jK5I^!xgW{2xB5dcUfqmB z%ZF?N3^tW^SEd5$@ChBX=+Ud8ngAt^{?9#r5(`#W)9w$4scd%xhR#vXpbpIzT@k|I%E$Fpm;qv-i`L5Ue^@%Q_mhiL(I7PcY z#VlwgnhWJ-JD08lRTf+I-eK>+?W60^w`A*!> zLtGv}X*vJ!s`f)t>^F?3Qqd)p8lKzq7 zTUgV$e#+?~5|wA#^@S^fCC%4tbmryba4zQ8^A$9 z$-l-9yzCe?2sB?YeG)g_e)~RNC4d@}rers*l5YbVi9X~$BVet;3wNwC@C919Us?+) zs34H#vFhp_Y{e<254&kbND5zoZXPv-ccp)NzE08I`)>36J|{I}VzbM99<;E3FK#&V zDHW&nstg@5=v}Ir8o9SP9Rz+NN0f{J?@87tbRHTYhIk8v> z2!!7a&_rZ%mY?{V9@S+GWuX?N3WWWWm^=&4rZ$^3bqw!|oy*`FZvWD_K4ry~pyI)K zjiB-iU$(aeE@Z{1OHXA<`75?nCrUc zS|mLK_H_=65827-B+GPiUL=o>WpsdG-7o-GOFQU!1Lyr|*83;0xq+~aHfcPtRo+Fv zLUQy@J5BP7R^w6iAiI{!26-uF>ZS5;r+Uq0$?7~qRIB|nFb zrFyIsJp?L~N1zfB6kfJGT81m00TfX`##P?J%`Lc29roj1%A-y!riZG?x7lYfTAbOR z>r!x-pJ{!o#RWgk=N4s9-X73mfC4H1G|ge>NJq{^WyLGy;9reiIaR&2_N4vzfGIkt}^UYN2M%hreCCy+=sgPhvoPmhVQf1w5*`x%`F>bOpbcmVzLCJ|_OJ`P<;#R&X;o zuJI-ztEM&!quPOL{z$izHC@gq7Ic{E0^#6g0Q1BGjq%>jfN<7AavBcrZ*ZMj;j8v` z->9!H&%|jdUAJ`k&*@&Xo_y!qB4}eB$f;sX5R@x`ACif3)qm{BS_SI7)k(1LchAru zbs90AqtYQ7lV9<&g^F==q>iM!r$ZrY(e&oXOQ*n{_A@$0v=Rbscjq+pG6}Stc7AY3 zsH`nJUs7%J?~{2J;^BT3FSuR~X!d}cTKu=Qa7Dxwq1}^3el6I|>iLW&d>+H~*7lR! zE7A~1d!*UfjY`7&Z_bOVmovvcHeHzQ4rq?Id$ArDqsXOnp^7xEn63H&XmrV_B{Jw`onb!M4-NyUWery5r< z(4QRMC=k4~tvFp)?7iCf_dF@A-bww^O4r9pg+j#M_mLJ}%C^)EIUf@ZP{BWXtN$rH z&!V~RoNQM>C9%+nUlM{dLQhb`Y*+lWS7Uxlmio55ofiK~k`a<=Z5vx^%-;;U@^E!a zXD-RkN_x;m`u&j;DODB6-9M_hjykIxIkx3GrUE-EOaI6{Ng%ReJ2=K1k zgP{-G?>HOh$W%<32g*v=9Zu@hrq8Gcq#fr@H8LQ$Gn9D^C8S2AQiU zuzSo}xZ|0Pi6EU;o))wDj1DtL6ew+w5?qk;v7_v=CxW!1k{dIv1BThMFv5LESj`cz zXstFJ;lMGkNiQ5k=3PGS;D-fp+RcDu1iO4n|3|zE3I7afntas{Drf%dQwSsnFakz} zHrbk9Sf|7#XGPH!fF{@mJZJFSDs+2&NXO^|1Ig{GbQY!pY%+fed|&z_&aShe5OOJU z4P+%P+WcZb-PxTqOsYQ4AQaW9Ips#^&R4XAZX;p&h_Krq`QGZ%+o9U?et~i>51tR+ ze)NNGYAD7+A5JgqfMEqMCU7ov*TL;LP|*h8;`bLF(lyFa$(JRmI_0dvijZ=8D4MCaac`ig}Sa#?3^CDBBZ zg^nHqSLqnXUj{WwFrAVpHLmZRD{3|Ii}r~flzN+HyeBdDM1in^{iyMM&Emops~yL8)|%a5^1|$Rv)SPO=}3-4)H{yTZlvXT z_2kt|0-XMOqPRTS{n_yI5t3Ls9ktX-AaSf#X}5%UYjcA;s8(j-f}q3d%0FKmxDse8 zrx?MPd3~GOUdqv@tewdNr>A+1ngpr^r>GT{qy+2)r=}mfzjX&By!n84!pZSf55uP z^Ag37Ge7k&7p?2{{$%H{fI!L#*t4tt@(Vf}X~0eikkO_g-SlHY=?3 z)Re*$n0>?de{^WYVJB$B$JW4^JnA)JVdC%@e@dE&@_l6#)A{|gw86SDa%}TUP|)Wt z79r7eqv^;en&ax@rR4vKalwN_=vKennk^*VD|+!ka+wb<_MSG5CPSD9~x22KYriaZ=kQa)GK0J?&UffK2PT@!HeBy{W%8v3qYRFdQ6N z_nG(>#$nmldrDJ)R4>V_1DYHO6BBX3O~yfqO>Lma9Z7~NG!?2i)G$m!xljdf;#XC0 zQ*a$KcMbF<_H)4L6R<0LEm5~h&MoT)>IHEMu?BO29@)$8ug)2UD=$@cul)`DU0N}FZcM%40hP`bDxHhn;5go@H4}5G z-6YlsR0~}S+VxdMnOUmTLcb5B>tCYzzRo!=)K&vHfuz{A(i%S6Nhn)s6?9&Z`XLN~ z(<{)=Lng4Jq_JUh9USFo7kPg!lKzu8GNCT}+7AZ7<9W*!#9f;jzf!HiCyu;v}yNDg9@dMXjcploNWTS_V zLdt)A5UkDOzq!)9&dRTug}SG6uz@AK?*~ck%Yj}{^Gh5@A^qorpB*`rMs&{uj|{r)Bid}{7zf_QEUXK1e43} zZU05PWPei;Dq9~-%*9RSTOQOMPtkt zMYDs;lqj(ui299Tcc#BLIS{qWSGL;Ev;q|3%vHFCX|pkph@g8O&S%-CB_B zHIn$1H?__0u7PZ6*WN^L1n36Y5^CA{wE{#!b-yG(&9jg+w0=xB=*IgasX;+ScT0Gs zNut9gQ)}~+e!QMdORtu$xQ9!Y!7aLPPR-uEe5EQp^g>YRVbo*?A)B(D#O=NtD*p8M z#|yJNsJ~+sEIJpMTY5CYzekiGt?Id0PJh}>zdpNBVYv39ABKti`H5=0Yz;{of`kA5 zgs$+K4D$siQKA+c7mzY3C#_}_qzE5q|=|3~is zF>ue7VO|(431C7)5UVX5Z0^O*0!KnKNW|@Aup$JcAt(^b(L?3S3;cHxp+hI~Bb$x0 zn`sbe@L)0>O}Ec7n01yNMgu5;{XJnJ8nkH0>w#Fl9$>oH6-ajoGJ)cOCJG&sJ^}e4 zCTD~`b+0237(9%e1~O4@5gi}|RKBfm`M*^f<%mv`|I-kfNBe*J*+BmPFpvP){!iKe z4^pnsVL5544xVACjQzj)Ld&0q0k$+`P*m;I(?6t&dK!XHak*0U9K6r%)B&@rS67a@ z`WVBZnvY6^iBZ+DkG_FRRlZ}^H$Sd4_>O&SqSQT^@aW6fRJTbz|9$c84cw?9jTH1i z04n|1k|)IKDh9=i1c)v-dkvToBOv;xkXH`nNFQ_pouSn`h0u$&6aOuGe!poo^fKgO2N!Bnfgz#h4K66dhIC7_^8X0vjq^!BLx7vGLIgI;e4LyZj3$|2-qtXQi;)?EDqA)Q12Ni*|2aB=5T zfyvWLx9A~3I&>pHv}O-^*wYBj7KO4IvhLAQ8DeMA(;a*Hv-h9M(U-KyH%F**^f;hg z7PRCh?(yYGlHO*MQ9ljRa_YwOEsk6C!f}x3TE9WPy6%1&_*=)VpeB0UA zl5ngpOz4j=lqwBNmX@QfU5K^G9;>|pmf54FA|C62-8`E%82XO23)i-v97%_amW81c z^iVe)@&k~D@yrLPm4{1A@cvwHZE%TGUW-91*0{mBhQq&~frtEI5r#Uc=a7iPn9YhM%QkBdns3UHc-Oq>2;Hi2r6 zg|L)20lRMe5l81+TAr5dP+j@macjj9bL=c61q1q5054iGBAW?AvQJ7ayCje9ut2i3 zkStB57JX434f|8ZQ+?fg^Y7}_2K`gn3=g~lsrVd6P%g+`!(Z+@9C5L34^>BlCa-e{ z`N8MsAfJyg#B4kuMTb4RrD3Ydl|D%jU ztNnqH@)$IF3Y~xIbno!@8nbIRZ@kD_axSqhYi_B%;d-;-`iSf? z*A!Gn_>P4O`5I-C0!L2L^-$!Kf1X0tK1AQxWGWB7J@G5{s`JL>Cl|D43FAOyy(8?+ zhq0Ynw_iT+vJ54SsDRfwOQa-+*(zsE!jCGw9njh>m(kDAkz5a|YMy&vfFPbg zvUvo>^_Pp%9$Pqm!)21x&A2M}S_&P0SV+vZL=0I<^YOAiGTW&Jf^G!d`6a5p^Xb<5 z(5j%x&LSIyw7Stva#>MAeuOLjt-`)YLx*q*d84@=+~g`e0fQb-=YK6L4A0n8BE@cv zxQM9j4V|R~vLu&4#tic}7|K;(CQ@(*X_{Y_iV$`}2j%@0^Fa;8Lh<7u6aVB9F$vt* zGWcH&e_xTC5E^IUTyoJ8uK`bwboLM^$8S z!5}&1S5q%0)22SPX&ooc{>n3@yeZN;k7YUuyC=5#tLhEUgPwjJ*BE^WVm6)BnT*hC zSbNsy(nx7ZjL{OqmLW>$6+bXXna@9T21a!1W$H7bC|dgbu; z?Vh123-dkVUw=l@E5r`}ls>it%>$RE&9H&csD-QF8YgHbbtfYyE%u@)iD`hpA6JFc zt1en1@!HTV8DVaZ(h;HyvHiY%45h(J66fu@Mm= zN&ix4a@~ShuI>Lb_MWBuZJJe9cO4gn-EWhJ>F>XSVWoEsq Date: Fri, 26 May 2023 18:35:07 +0200 Subject: [PATCH 12/12] Bump app build number to 472 (#7375) --- android/app/build.gradle | 2 +- ios/Mattermost.xcodeproj/project.pbxproj | 8 ++++---- ios/Mattermost/Info.plist | 2 +- ios/MattermostShare/Info.plist | 2 +- ios/NotificationService/Info.plist | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) 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/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 -- 2.49.1