forked from Ivasoft/mattermost-mobile
main #2
@@ -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'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getLicense} from '@queries/servers/system';
|
||||
import {getTeamById} from '@queries/servers/team';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
@@ -12,25 +13,14 @@ import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
export const fetchGroup = async (serverUrl: string, id: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
const group = await client.getGroup(id);
|
||||
|
||||
// Save locally
|
||||
return operator.handleGroups({groups: [group], prepareRecordsOnly: fetchOnly});
|
||||
} catch (error) {
|
||||
logDebug('error on fetchGroup', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchGroupsForAutocomplete = async (serverUrl: string, query: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const license = await getLicense(database);
|
||||
if (!license || !license.IsLicensed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getGroups({query, includeMemberCount: true});
|
||||
|
||||
@@ -48,7 +38,11 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin
|
||||
|
||||
export const fetchGroupsByNames = async (serverUrl: string, names: string[], fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const license = await getLicense(database);
|
||||
if (!license || !license.IsLicensed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const promises: Array <Promise<Group[]>> = [];
|
||||
@@ -74,7 +68,12 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
|
||||
|
||||
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const license = await getLicense(database);
|
||||
if (!license || !license.IsLicensed) {
|
||||
return {groups: [], groupChannels: []};
|
||||
}
|
||||
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToChannel(channelId);
|
||||
|
||||
@@ -101,7 +100,11 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
|
||||
|
||||
export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const license = await getLicense(database);
|
||||
if (!license || !license.IsLicensed) {
|
||||
return {groups: [], groupTeams: []};
|
||||
}
|
||||
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToTeam(teamId);
|
||||
@@ -128,7 +131,11 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
|
||||
|
||||
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const license = await getLicense(database);
|
||||
if (!license || !license.IsLicensed) {
|
||||
return {groups: [], groupMemberships: []};
|
||||
}
|
||||
|
||||
const client: Client = NetworkManager.getClient(serverUrl);
|
||||
const response = await client.getAllGroupsAssociatedToMembership(userId);
|
||||
|
||||
@@ -8,24 +8,13 @@ import {PER_PAGE_DEFAULT} from './constants';
|
||||
import type ClientBase from './base';
|
||||
|
||||
export interface ClientGroupsMix {
|
||||
getGroup: (id: string) => Promise<Group>;
|
||||
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToTeam: (teamId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
getAllChannelsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupChannels: GroupChannel[]}>;
|
||||
getAllMembershipsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupMemberships: UserProfile[]; total_member_count: number}>;
|
||||
getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>;
|
||||
}
|
||||
|
||||
const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getGroup = async (id: string) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/groups/${id}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getGroups = async ({query = '', filterAllowReference = true, page = 0, perPage = PER_PAGE_DEFAULT, since = 0, includeMemberCount = false}) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/groups${buildQueryString({
|
||||
@@ -64,27 +53,6 @@ const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase)
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAllTeamsAssociatedToGroup = async (groupId: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/groups/${groupId}/teams${buildQueryString({filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAllChannelsAssociatedToGroup = async (groupId: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/groups/${groupId}/channels${buildQueryString({filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAllMembershipsAssociatedToGroup = async (groupId: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/groups/${groupId}/members${buildQueryString({filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientGroups;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -7,9 +7,13 @@ import {MENTIONS_REGEX} from '@constants/autocomplete';
|
||||
export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Post[], excludeUsername?: string) => {
|
||||
const usernamesToLoad = new Set<string>();
|
||||
|
||||
posts.forEach((p) => {
|
||||
const findNeededUsernames = (text?: string) => {
|
||||
if (!text || !text.includes('@')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let match;
|
||||
while ((match = MENTIONS_REGEX.exec(p.message)) !== null) {
|
||||
while ((match = MENTIONS_REGEX.exec(text)) !== null) {
|
||||
const lowercaseMatch = match[1].toLowerCase();
|
||||
|
||||
if (General.SPECIAL_MENTIONS.has(lowercaseMatch)) {
|
||||
@@ -26,7 +30,19 @@ export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Pos
|
||||
|
||||
usernamesToLoad.add(lowercaseMatch);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
for (const post of posts) {
|
||||
// These correspond to the fields searched by getMentionsEnabledFields on the server
|
||||
findNeededUsernames(post.message);
|
||||
|
||||
if (post.props?.attachments) {
|
||||
for (const attachment of post.props.attachments) {
|
||||
findNeededUsernames(attachment.pretext);
|
||||
findNeededUsernames(attachment.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usernamesToLoad;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type RefObject, useEffect, useRef, useState} from 'react';
|
||||
import {AppState, Keyboard, NativeEventEmitter, NativeModules, Platform, View} from 'react-native';
|
||||
import {AppState, Keyboard, NativeEventEmitter, NativeModules, Platform, useWindowDimensions, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {Device} from '@constants';
|
||||
@@ -110,7 +110,7 @@ export function useKeyboardHeight(keyboardTracker?: React.RefObject<KeyboardTrac
|
||||
return height;
|
||||
}
|
||||
|
||||
export function useModalPosition(viewRef: RefObject<View>, deps: React.DependencyList = []) {
|
||||
export function useViewPosition(viewRef: RefObject<View>, deps: React.DependencyList = []) {
|
||||
const [modalPosition, setModalPosition] = useState(0);
|
||||
const isTablet = useIsTablet();
|
||||
const height = useKeyboardHeight();
|
||||
@@ -127,3 +127,21 @@ export function useModalPosition(viewRef: RefObject<View>, deps: React.Dependenc
|
||||
|
||||
return modalPosition;
|
||||
}
|
||||
|
||||
export function useKeyboardOverlap(viewRef: RefObject<View>, containerHeight: number) {
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const isTablet = useIsTablet();
|
||||
const viewPosition = useViewPosition(viewRef, [containerHeight]);
|
||||
const dimensions = useWindowDimensions();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const bottomSpace = (dimensions.height - containerHeight - viewPosition);
|
||||
const tabletOverlap = Math.max(0, keyboardHeight - bottomSpace);
|
||||
const phoneOverlap = keyboardHeight || insets.bottom;
|
||||
const overlap = Platform.select({
|
||||
ios: isTablet ? tabletOverlap : phoneOverlap,
|
||||
default: 0,
|
||||
});
|
||||
|
||||
return overlap;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useModalPosition} from '@hooks/device';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {t} from '@i18n';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
@@ -126,12 +126,12 @@ export default function ChannelAddMembers({
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const keyboardOverlap = useKeyboardOverlap(mainView, containerHeight);
|
||||
|
||||
const [term, setTerm] = useState('');
|
||||
const [addingMembers, setAddingMembers] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
@@ -281,8 +281,7 @@ export default function ChannelAddMembers({
|
||||
createFilter={createUserFilter}
|
||||
/>
|
||||
<SelectedUsers
|
||||
containerHeight={containerHeight}
|
||||
modalPosition={modalPosition}
|
||||
keyboardOverlap={keyboardOverlap}
|
||||
selectedIds={selectedIds}
|
||||
onRemove={handleRemoveProfile}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useModalPosition} from '@hooks/device';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {dismissModal, setButtons} from '@screens/navigation';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
@@ -116,13 +116,13 @@ export default function CreateDirectMessage({
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const keyboardOverlap = useKeyboardOverlap(mainView, containerHeight);
|
||||
|
||||
const [term, setTerm] = useState('');
|
||||
const [startingConversation, setStartingConversation] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const selectedCount = Object.keys(selectedIds).length;
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
@@ -327,8 +327,7 @@ export default function CreateDirectMessage({
|
||||
createFilter={createUserFilter}
|
||||
/>
|
||||
<SelectedUsers
|
||||
containerHeight={containerHeight}
|
||||
modalPosition={modalPosition}
|
||||
keyboardOverlap={keyboardOverlap}
|
||||
showToast={showToast}
|
||||
setShowToast={setShowToast}
|
||||
toastIcon={'check'}
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
type NativeSyntheticEvent,
|
||||
type NativeScrollEvent,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import ErrorText from '@components/error_text';
|
||||
@@ -26,7 +25,7 @@ import OptionItem from '@components/option_item';
|
||||
import {General, Channel} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
|
||||
import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device';
|
||||
import {useKeyboardHeight, useKeyboardOverlap} from '@hooks/device';
|
||||
import {useInputPropagation} from '@hooks/input';
|
||||
import {t} from '@i18n';
|
||||
import {
|
||||
@@ -108,7 +107,6 @@ export default function ChannelInfoForm({
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
@@ -122,10 +120,8 @@ export default function ChannelInfoForm({
|
||||
const updateScrollTimeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
|
||||
const dimensions = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
const [wrapperHeight, setWrapperHeight] = useState(0);
|
||||
const keyboardOverlap = useKeyboardOverlap(mainView, wrapperHeight);
|
||||
|
||||
const [propagateValue, shouldProcessEvent] = useInputPropagation();
|
||||
|
||||
@@ -133,7 +129,6 @@ export default function ChannelInfoForm({
|
||||
const [keyboardVisible, setKeyBoardVisible] = useState(false);
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
|
||||
const [wrapperHeight, setWrapperHeight] = useState(0);
|
||||
const [errorHeight, setErrorHeight] = useState(0);
|
||||
const [displayNameFieldHeight, setDisplayNameFieldHeight] = useState(0);
|
||||
const [makePrivateHeight, setMakePrivateHeight] = useState(0);
|
||||
@@ -228,29 +223,17 @@ export default function ChannelInfoForm({
|
||||
setWrapperHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const bottomSpace = (dimensions.height - wrapperHeight - modalPosition);
|
||||
const otherElementsSize = LIST_PADDING + errorHeight +
|
||||
(showSelector ? makePrivateHeight + MAKE_PRIVATE_MARGIN_BOTTOM : 0) +
|
||||
(displayHeaderOnly ? 0 : purposeFieldHeight + FIELD_MARGIN_BOTTOM + displayNameFieldHeight + FIELD_MARGIN_BOTTOM);
|
||||
|
||||
const keyboardOverlap = Platform.select({
|
||||
ios: isTablet ?
|
||||
Math.max(0, keyboardHeight - bottomSpace) :
|
||||
keyboardHeight || insets.bottom,
|
||||
default: 0});
|
||||
const workingSpace = wrapperHeight - keyboardOverlap;
|
||||
const spaceOnTop = otherElementsSize - scrollPosition - AUTOCOMPLETE_ADJUST;
|
||||
const spaceOnBottom = (workingSpace + scrollPosition) - (otherElementsSize + headerFieldHeight + BOTTOM_AUTOCOMPLETE_SEPARATION);
|
||||
const insetsAdjust = keyboardHeight || insets.bottom;
|
||||
const keyboardAdjust = Platform.select({
|
||||
ios: isTablet ?
|
||||
keyboardOverlap :
|
||||
insetsAdjust,
|
||||
default: 0,
|
||||
});
|
||||
|
||||
const autocompletePosition = spaceOnBottom > spaceOnTop ?
|
||||
(otherElementsSize + headerFieldHeight) - scrollPosition :
|
||||
(workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardAdjust) - otherElementsSize;
|
||||
(workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardOverlap) - otherElementsSize;
|
||||
const autocompleteAvailableSpace = spaceOnBottom > spaceOnTop ? spaceOnBottom : spaceOnTop;
|
||||
const growDown = spaceOnBottom > spaceOnTop;
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, Keyboard, type LayoutChangeEvent, Platform, SafeAreaView, useWindowDimensions, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {Alert, Keyboard, type LayoutChangeEvent, Platform, SafeAreaView, View} from 'react-native';
|
||||
|
||||
import {deletePost, editPost} from '@actions/remote/post';
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
@@ -13,7 +12,7 @@ import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
|
||||
import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {useInputPropagation} from '@hooks/input';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
@@ -66,27 +65,15 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
||||
const [propagateValue, shouldProcessEvent] = useInputPropagation();
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
|
||||
const postInputRef = useRef<EditPostInputRef>(null);
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const insets = useSafeAreaInsets();
|
||||
const dimensions = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [{
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}),
|
||||
...RIGHT_BUTTON,
|
||||
enabled: false,
|
||||
}],
|
||||
});
|
||||
toggleSaveButton(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -211,11 +198,8 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
||||
useNavButtonPressed(closeButtonId, componentId, onClose, []);
|
||||
useAndroidHardwareBackHandler(componentId, onClose);
|
||||
|
||||
const bottomSpace = (dimensions.height - containerHeight - modalPosition);
|
||||
const autocompletePosition = Platform.select({
|
||||
ios: isTablet ? Math.max(0, keyboardHeight - bottomSpace) : keyboardHeight || insets.bottom,
|
||||
default: 0,
|
||||
}) + AUTOCOMPLETE_SEPARATION;
|
||||
const overlap = useKeyboardOverlap(mainView, containerHeight);
|
||||
const autocompletePosition = overlap + AUTOCOMPLETE_SEPARATION;
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition;
|
||||
|
||||
const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace);
|
||||
|
||||
@@ -38,7 +38,7 @@ type Props = {
|
||||
channelsMatchStart: ChannelModel[];
|
||||
currentTeamId: string;
|
||||
isCRTEnabled: boolean;
|
||||
keyboardHeight: number;
|
||||
keyboardOverlap: number;
|
||||
loading: boolean;
|
||||
onLoading: (loading: boolean) => void;
|
||||
restrictDirectMessage: boolean;
|
||||
@@ -75,7 +75,7 @@ const sortByUserOrChannel = <T extends Channel |UserModel>(locale: string, teamm
|
||||
|
||||
const FilteredList = ({
|
||||
archivedChannels, close, channelsMatch, channelsMatchStart, currentTeamId,
|
||||
isCRTEnabled, keyboardHeight, loading, onLoading, restrictDirectMessage, showTeamName,
|
||||
isCRTEnabled, keyboardOverlap, loading, onLoading, restrictDirectMessage, showTeamName,
|
||||
teamIds, teammateDisplayNameSetting, term, usersMatch, usersMatchStart, testID,
|
||||
}: Props) => {
|
||||
const bounce = useRef<DebouncedFunc<() => void>>();
|
||||
@@ -83,7 +83,7 @@ const FilteredList = ({
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const {locale, formatMessage} = useIntl();
|
||||
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardHeight}), [keyboardHeight]);
|
||||
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardOverlap}), [keyboardOverlap]);
|
||||
const [remoteChannels, setRemoteChannels] = useState<RemoteChannels>({archived: [], startWith: [], matches: []});
|
||||
|
||||
const totalLocalResults = channelsMatchStart.length + channelsMatch.length + usersMatchStart.length;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {Keyboard, View} from 'react-native';
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import {Keyboard, type LayoutChangeEvent, View} from 'react-native';
|
||||
|
||||
import SearchBar from '@components/search';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -48,7 +48,10 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const styles = getStyleSheet(theme);
|
||||
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const listView = useRef<View>(null);
|
||||
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const overlap = useKeyboardOverlap(listView, containerHeight);
|
||||
|
||||
const cancelButtonProps = useMemo(() => ({
|
||||
color,
|
||||
@@ -57,6 +60,10 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
|
||||
},
|
||||
}), [color]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
return dismissModal({componentId});
|
||||
@@ -99,18 +106,22 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
|
||||
testID='find_channels.search_bar'
|
||||
/>
|
||||
{term === '' && <QuickOptions close={close}/>}
|
||||
<View style={styles.listContainer}>
|
||||
<View
|
||||
style={styles.listContainer}
|
||||
onLayout={onLayout}
|
||||
ref={listView}
|
||||
>
|
||||
{term === '' &&
|
||||
<UnfilteredList
|
||||
close={close}
|
||||
keyboardHeight={keyboardHeight}
|
||||
keyboardOverlap={overlap}
|
||||
testID='find_channels.unfiltered_list'
|
||||
/>
|
||||
}
|
||||
{Boolean(term) &&
|
||||
<FilteredList
|
||||
close={close}
|
||||
keyboardHeight={keyboardHeight}
|
||||
keyboardOverlap={overlap}
|
||||
loading={loading}
|
||||
onLoading={setLoading}
|
||||
term={term}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
type Props = {
|
||||
close: () => Promise<void>;
|
||||
keyboardHeight: number;
|
||||
keyboardOverlap: number;
|
||||
recentChannels: ChannelModel[];
|
||||
showTeamName: boolean;
|
||||
testID?: string;
|
||||
@@ -46,11 +46,11 @@ const buildSections = (recentChannels: ChannelModel[]) => {
|
||||
return sections;
|
||||
};
|
||||
|
||||
const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, testID}: Props) => {
|
||||
const UnfilteredList = ({close, keyboardOverlap, recentChannels, showTeamName, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [sections, setSections] = useState(buildSections(recentChannels));
|
||||
const sectionListStyle = useMemo(() => ({paddingBottom: keyboardHeight}), [keyboardHeight]);
|
||||
const sectionListStyle = useMemo(() => ({paddingBottom: keyboardOverlap}), [keyboardOverlap]);
|
||||
|
||||
const onPress = useCallback(async (c: Channel | ChannelModel) => {
|
||||
await close();
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import Loading from '@components/loading';
|
||||
import {ServerErrors} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useModalPosition} from '@hooks/device';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {dismissModal, setButtons} from '@screens/navigation';
|
||||
import {isEmail} from '@utils/helpers';
|
||||
@@ -112,8 +112,10 @@ export default function Invite({
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
const [wrapperHeight, setWrapperHeight] = useState(0);
|
||||
const keyboardOverlap = useKeyboardOverlap(mainView, wrapperHeight);
|
||||
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const retryTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -123,7 +125,6 @@ export default function Invite({
|
||||
const [selectedIds, setSelectedIds] = useState<{[id: string]: SearchResult}>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<Result>(DEFAULT_RESULT);
|
||||
const [wrapperHeight, setWrapperHeight] = useState(0);
|
||||
const [stage, setStage] = useState(Stage.SELECTION);
|
||||
const [sendError, setSendError] = useState('');
|
||||
|
||||
@@ -394,7 +395,7 @@ export default function Invite({
|
||||
term={term}
|
||||
searchResults={searchResults}
|
||||
selectedIds={selectedIds}
|
||||
modalPosition={modalPosition}
|
||||
keyboardOverlap={keyboardOverlap}
|
||||
wrapperHeight={wrapperHeight}
|
||||
loading={loading}
|
||||
onSearchChange={handleSearchChange}
|
||||
|
||||
@@ -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
|
||||
) : (
|
||||
|
||||
@@ -57,7 +57,7 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
color: theme.mentionColor,
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 12,
|
||||
...typography('Heading', 1000, 'SemiBold'),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -8,18 +8,18 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchChannelMemberships} from '@actions/remote/channel';
|
||||
import {fetchUsersByIds, searchProfiles} from '@actions/remote/user';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import Search from '@components/search';
|
||||
import UserList from '@components/user_list';
|
||||
import {Events, General, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {openAsBottomSheet, setButtons} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {showRemoveChannelUserSnackbar} from '@utils/snack_bar';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
import {filterProfilesMatchingTerm} from '@utils/user';
|
||||
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
@@ -30,6 +30,7 @@ type Props = {
|
||||
currentTeamId: string;
|
||||
currentUserId: string;
|
||||
tutorialWatched: boolean;
|
||||
teammateDisplayNameSetting: string;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -54,6 +55,12 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const sortUsers = (a: UserProfile, b: UserProfile, locale: string, teammateDisplayNameSetting: string) => {
|
||||
const aName = displayUsername(a, locale, teammateDisplayNameSetting);
|
||||
const bName = displayUsername(b, locale, teammateDisplayNameSetting);
|
||||
return aName.localeCompare(bName, locale);
|
||||
};
|
||||
|
||||
const MANAGE_BUTTON = 'manage-button';
|
||||
const EMPTY: UserProfile[] = [];
|
||||
const EMPTY_MEMBERS: ChannelMembership[] = [];
|
||||
@@ -68,47 +75,29 @@ export default function ManageChannelMembers({
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
tutorialWatched,
|
||||
teammateDisplayNameSetting,
|
||||
}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const {formatMessage} = useIntl();
|
||||
const {formatMessage, locale} = useIntl();
|
||||
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const mounted = useRef(false);
|
||||
|
||||
const [isManageMode, setIsManageMode] = useState(false);
|
||||
const [profiles, setProfiles] = useState<UserProfile[]>(EMPTY);
|
||||
const hasMoreProfiles = useRef(false);
|
||||
const [channelMembers, setChannelMembers] = useState<ChannelMembership[]>(EMPTY_MEMBERS);
|
||||
const [searchResults, setSearchResults] = useState<UserProfile[]>(EMPTY);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [term, setTerm] = useState('');
|
||||
|
||||
const loadedProfiles = (users: UserProfile[], members: ChannelMembership[]) => {
|
||||
if (mounted.current) {
|
||||
setLoading(false);
|
||||
setProfiles(users);
|
||||
setChannelMembers(members);
|
||||
}
|
||||
};
|
||||
const [searchedTerm, setSearchedTerm] = useState('');
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
setSearchResults(EMPTY);
|
||||
}, []);
|
||||
|
||||
const getProfiles = useCallback(debounce(async () => {
|
||||
const hasTerm = Boolean(term);
|
||||
if (!loading && !hasTerm && mounted.current) {
|
||||
setLoading(true);
|
||||
const options = {sort: 'admin', active: true};
|
||||
const {users, members} = await fetchChannelMemberships(serverUrl, channelId, options, true);
|
||||
if (users.length) {
|
||||
loadedProfiles(users, members);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}, 100), [channelId, loading, serverUrl, term]);
|
||||
|
||||
const handleSelectProfile = useCallback(async (profile: UserProfile) => {
|
||||
if (profile.id === currentUserId && isManageMode) {
|
||||
return;
|
||||
@@ -133,15 +122,19 @@ export default function ManageChannelMembers({
|
||||
}, [canManageAndRemoveMembers, channelId, isManageMode, currentUserId]);
|
||||
|
||||
const searchUsers = useCallback(async (searchTerm: string) => {
|
||||
setSearchedTerm(searchTerm);
|
||||
if (!hasMoreProfiles.current) {
|
||||
return;
|
||||
}
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
setLoading(true);
|
||||
|
||||
const options: SearchUserOptions = {team_id: currentTeamId, in_channel_id: channelId, allow_inactive: false};
|
||||
const {data = EMPTY} = await searchProfiles(serverUrl, lowerCasedTerm, options);
|
||||
|
||||
setSearchResults(data);
|
||||
setSearchResults(data.sort((a, b) => sortUsers(a, b, locale, teammateDisplayNameSetting)));
|
||||
setLoading(false);
|
||||
}, [serverUrl, channelId, currentTeamId]);
|
||||
}, [serverUrl, channelId, currentTeamId, locale, teammateDisplayNameSetting]);
|
||||
|
||||
const search = useCallback(() => {
|
||||
searchUsers(term);
|
||||
@@ -210,19 +203,44 @@ export default function ManageChannelMembers({
|
||||
setChannelMembers(clone);
|
||||
}, [channelMembers]);
|
||||
|
||||
const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => {
|
||||
return sortUsers(a, b, locale, teammateDisplayNameSetting);
|
||||
}), [profiles, locale, teammateDisplayNameSetting]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
const isSearch = Boolean(term);
|
||||
const isSearch = Boolean(searchedTerm);
|
||||
if (isSearch) {
|
||||
return filterProfilesMatchingTerm(searchResults, term);
|
||||
return filterProfilesMatchingTerm(searchResults.length ? searchResults : sortedProfiles, searchedTerm);
|
||||
}
|
||||
return profiles;
|
||||
}, [term, searchResults, profiles]);
|
||||
}, [searchResults, profiles, searchedTerm, sortedProfiles]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!term) {
|
||||
setSearchResults(EMPTY);
|
||||
setSearchedTerm('');
|
||||
}
|
||||
}, [Boolean(term)]);
|
||||
|
||||
useNavButtonPressed(MANAGE_BUTTON, componentId, toggleManageEnabled, [toggleManageEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
getProfiles();
|
||||
const options: GetUsersOptions = {sort: 'admin', active: true, per_page: PER_PAGE_DEFAULT};
|
||||
fetchChannelMemberships(serverUrl, channelId, options, true).then(({users, members}) => {
|
||||
if (!mounted.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (users.length >= PER_PAGE_DEFAULT) {
|
||||
hasMoreProfiles.current = true;
|
||||
}
|
||||
if (users.length) {
|
||||
setProfiles(users);
|
||||
setChannelMembers(members);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
@@ -272,7 +290,7 @@ export default function ManageChannelMembers({
|
||||
selectedIds={EMPTY_IDS}
|
||||
showManageMode={canManageAndRemoveMembers && isManageMode}
|
||||
showNoResults={!loading}
|
||||
term={term}
|
||||
term={searchedTerm}
|
||||
testID='manage_members.user_list'
|
||||
tutorialWatched={tutorialWatched}
|
||||
includeUserMargin={true}
|
||||
|
||||
@@ -61,7 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
marginTop: 20,
|
||||
},
|
||||
header: {
|
||||
color: theme.mentionColor,
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 12,
|
||||
...typography('Heading', 1000, 'SemiBold'),
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -765,7 +765,7 @@
|
||||
"notification_settings.threads_start": "Threads that I start",
|
||||
"notification_settings.threads_start_participate": "Threads that I start or participate in",
|
||||
"notification.message_not_found": "Message not found",
|
||||
"notification.no_connection": "The server is unreachable and we were not able to retrieve the notification channel / team.",
|
||||
"notification.no_connection": "The server is unreachable and it was not possible to retrieve the specific message information for the notification.",
|
||||
"notification.no_post": "The message has not been found.",
|
||||
"notification.not_channel_member": "This message belongs to a channel where you are not a member.",
|
||||
"notification.not_team_member": "This message belongs to a team where you are not a member.",
|
||||
|
||||
BIN
fastlane/metadata/android/en-US/images/icon.png
Executable file → Normal file
BIN
fastlane/metadata/android/en-US/images/icon.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
@@ -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;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>471</string>
|
||||
<string>472</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.4.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>471</string>
|
||||
<string>472</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>OpenSans-Bold.ttf</string>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.4.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>471</string>
|
||||
<string>472</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@@ -85,7 +85,7 @@ PODS:
|
||||
- HMSegmentedControl (1.5.6)
|
||||
- jail-monkey (2.8.0):
|
||||
- React-Core
|
||||
- JitsiWebRTC (111.0.1)
|
||||
- JitsiWebRTC (111.0.2)
|
||||
- libevent (2.1.12)
|
||||
- libwebp (1.2.4):
|
||||
- libwebp/demux (= 1.2.4)
|
||||
@@ -570,7 +570,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNVectorIcons (9.2.0):
|
||||
- React-Core
|
||||
- Rudder (1.14.0)
|
||||
- Rudder (1.15.1)
|
||||
- SDWebImage (5.12.6):
|
||||
- SDWebImage/Core (= 5.12.6)
|
||||
- SDWebImage/Core (5.12.6)
|
||||
@@ -920,7 +920,7 @@ SPEC CHECKSUMS:
|
||||
hermes-engine: 4438d2b8bf8bebaba1b1ac0451160bab59e491f8
|
||||
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
|
||||
jail-monkey: a71b35d482a70ecba844a90f002994012cf12a5d
|
||||
JitsiWebRTC: 9619c1f71cc16eeca76df68aa2d213c6d63274a8
|
||||
JitsiWebRTC: 80f62908fcf2a1160e0d14b584323fb6e6be630b
|
||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||
mattermost-react-native-turbo-log: a00b39dafdef7905164110466e7d725f6f079751
|
||||
@@ -992,7 +992,7 @@ SPEC CHECKSUMS:
|
||||
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
|
||||
RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
Rudder: 41523d90e7f8040605ca6a803f662a538144c90f
|
||||
Rudder: 41040d4537a178e4e32477b68400f98ca0c354eb
|
||||
SDWebImage: a47aea9e3d8816015db4e523daff50cfd294499d
|
||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||
Sentry: 16d46dd5ca10e7f4469a2611805a3de123188add
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
70
patches/@sentry+utils+7.47.0.patch
Normal file
70
patches/@sentry+utils+7.47.0.patch
Normal file
@@ -0,0 +1,70 @@
|
||||
diff --git a/node_modules/@sentry/utils/cjs/object.js b/node_modules/@sentry/utils/cjs/object.js
|
||||
index eb89fb8..0716abb 100644
|
||||
--- a/node_modules/@sentry/utils/cjs/object.js
|
||||
+++ b/node_modules/@sentry/utils/cjs/object.js
|
||||
@@ -198,7 +198,11 @@ function dropUndefinedKeys(inputValue) {
|
||||
return _dropUndefinedKeys(inputValue, memoizationMap);
|
||||
}
|
||||
|
||||
-function _dropUndefinedKeys(inputValue, memoizationMap) {
|
||||
+function _dropUndefinedKeys(inputValue, memoizationMap, depth = 0) {
|
||||
+ if (depth >= 5) {
|
||||
+ return inputValue;
|
||||
+ }
|
||||
+
|
||||
if (is.isPlainObject(inputValue)) {
|
||||
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
|
||||
const memoVal = memoizationMap.get(inputValue);
|
||||
@@ -212,7 +216,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
|
||||
|
||||
for (const key of Object.keys(inputValue)) {
|
||||
if (typeof inputValue[key] !== 'undefined') {
|
||||
- returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap);
|
||||
+ returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +235,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
|
||||
memoizationMap.set(inputValue, returnValue);
|
||||
|
||||
inputValue.forEach((item) => {
|
||||
- returnValue.push(_dropUndefinedKeys(item, memoizationMap));
|
||||
+ returnValue.push(_dropUndefinedKeys(item, memoizationMap, depth + 1));
|
||||
});
|
||||
|
||||
return returnValue ;
|
||||
diff --git a/node_modules/@sentry/utils/esm/object.js b/node_modules/@sentry/utils/esm/object.js
|
||||
index 0f5c411..1a8b5c9 100644
|
||||
--- a/node_modules/@sentry/utils/esm/object.js
|
||||
+++ b/node_modules/@sentry/utils/esm/object.js
|
||||
@@ -196,7 +196,11 @@ function dropUndefinedKeys(inputValue) {
|
||||
return _dropUndefinedKeys(inputValue, memoizationMap);
|
||||
}
|
||||
|
||||
-function _dropUndefinedKeys(inputValue, memoizationMap) {
|
||||
+function _dropUndefinedKeys(inputValue, memoizationMap, depth = 0) {
|
||||
+ if (depth >= 5) {
|
||||
+ return inputValue;
|
||||
+ }
|
||||
+
|
||||
if (isPlainObject(inputValue)) {
|
||||
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
|
||||
const memoVal = memoizationMap.get(inputValue);
|
||||
@@ -210,7 +214,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
|
||||
|
||||
for (const key of Object.keys(inputValue)) {
|
||||
if (typeof inputValue[key] !== 'undefined') {
|
||||
- returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap);
|
||||
+ returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +233,7 @@ function _dropUndefinedKeys(inputValue, memoizationMap) {
|
||||
memoizationMap.set(inputValue, returnValue);
|
||||
|
||||
inputValue.forEach((item) => {
|
||||
- returnValue.push(_dropUndefinedKeys(item, memoizationMap));
|
||||
+ returnValue.push(_dropUndefinedKeys(item, memoizationMap, depth + 1));
|
||||
});
|
||||
|
||||
return returnValue ;
|
||||
Reference in New Issue
Block a user