Fix MM-61710 (#8368) (#8382)

(cherry picked from commit f911a29e4c)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
Mattermost Build 2024-11-28 07:52:28 +01:00 committed by GitHub
parent 64581b8b2d
commit 68aa016921
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 84 additions and 49 deletions

View file

@ -19,6 +19,7 @@ import {getUserById, observeTeammateNameDisplay} from '@queries/servers/user';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {displayUsername} from '@utils/user';
import type {WithDatabaseArgs} from '@typings/database/database';
@ -93,7 +94,7 @@ async function getItemName(serverUrl: string, selected: string, teammateNameDisp
return '';
}
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database;
switch (dataSource) {
case ViewConstants.DATA_SOURCE_USERS: {

View file

@ -46,9 +46,7 @@ const FormattedText = (props: FormattedTextProps) => {
// when the `message` is formatted. This allows the formatted
// message to then be broken-up into parts with references to the
// React Elements inserted back in.
Object.keys(values).forEach((name) => {
const value = values[name];
Object.entries(values).forEach(([name, value]) => {
if (isValidElement(value)) {
const token = generateToken();
tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter;

View file

@ -10,6 +10,7 @@ import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {alertErrorWithFallback} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
import {secureGetFromRecord} from '@utils/types';
import type ChannelModel from '@typings/database/models/servers/channel';
import type TeamModel from '@typings/database/models/servers/team';
@ -40,7 +41,7 @@ function getChannelFromChannelName(name: string, channels: ChannelModel[], chann
});
while (channelName.length > 0) {
if (channelsByName[channelName]) {
if (secureGetFromRecord(channelsByName, channelName)) {
return channelsByName[channelName];
}

View file

@ -29,6 +29,7 @@ import {bottomSheetSnapPoint} from '@utils/helpers';
import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images';
import {getMarkdownImageSize} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {normalizeProtocol, tryOpenURL} from '@utils/url';
import type {GalleryItemType} from '@typings/screens/gallery';
@ -80,7 +81,7 @@ const MarkdownImage = ({
const style = getStyleSheet(theme);
const managedConfig = useManagedConfig<ManagedConfig>();
const genericFileId = useRef(generateId('uid')).current;
const metadata = imagesMetadata?.[source] || Object.values(imagesMetadata || {})[0];
const metadata = secureGetFromRecord(imagesMetadata, source) || Object.values(imagesMetadata || {})[0];
const [failed, setFailed] = useState(isGifTooLarge(metadata));
const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight);
const serverUrl = useServerUrl();

View file

@ -13,6 +13,7 @@ import {useGalleryItem} from '@hooks/gallery';
import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery';
import {generateId} from '@utils/general';
import {calculateDimensions, isGifTooLarge} from '@utils/images';
import {secureGetFromRecord} from '@utils/types';
import type {GalleryItemType} from '@typings/screens/gallery';
@ -33,7 +34,7 @@ const style = StyleSheet.create({
});
const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, source}: MarkdownTableImageProps) => {
const metadata = imagesMetadata[source];
const metadata = secureGetFromRecord(imagesMetadata, source);
const fileId = useRef(generateId('uid')).current;
const [failed, setFailed] = useState(isGifTooLarge(metadata));
const currentServerUrl = useServerUrl();

View file

@ -16,6 +16,7 @@ import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@
import {emptyFunction} from '@utils/general';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import LastUsers from './last_users';
import {postTypeMessages} from './messages';
@ -76,7 +77,7 @@ const CombinedUserActivity = ({
const usernamesValues = Object.values(usernamesById);
const usernames = userIds.reduce((acc: string[], id: string) => {
if (id !== currentUserId && id !== currentUsername) {
const name = usernamesById[id] ?? usernamesValues.find((n) => n === id);
const name = secureGetFromRecord(usernamesById, id) ?? usernamesValues.find((n) => n === id);
acc.push(name ? `@${name}` : someone);
}
return acc;
@ -112,7 +113,7 @@ const CombinedUserActivity = ({
return null;
}
let actor = '';
if (usernamesById[actorId]) {
if (secureGetFromRecord(usernamesById, actorId)) {
actor = `@${usernamesById[actorId]}`;
}
@ -141,11 +142,11 @@ const CombinedUserActivity = ({
const secondUser = usernames[1];
let localeHolder;
if (numOthers === 0) {
localeHolder = postTypeMessages[postType].one;
localeHolder = secureGetFromRecord(postTypeMessages, postType)?.one;
if (
(userIds[0] === currentUserId || userIds[0] === currentUsername) &&
postTypeMessages[postType].one_you
secureGetFromRecord(postTypeMessages, postType)?.one_you
) {
localeHolder = postTypeMessages[postType].one_you;
}
@ -153,7 +154,8 @@ const CombinedUserActivity = ({
localeHolder = postTypeMessages[postType].two;
}
const formattedMessage = intl.formatMessage(localeHolder, {firstUser, secondUser, actor});
// We default to empty string, but this should never happen
const formattedMessage = localeHolder ? intl.formatMessage(localeHolder, {firstUser, secondUser, actor}) : '';
return (
<Markdown
channelId={post.channel_id}

View file

@ -10,6 +10,7 @@ import FormattedText from '@components/formatted_text';
import Markdown from '@components/markdown';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {postTypeMessages, systemMessages} from './messages';
@ -50,12 +51,14 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las
if (expanded) {
const lastIndex = usernames.length - 1;
const lastUser = usernames[lastIndex];
const expandedMessage = postTypeMessages[postType].many_expanded;
const formattedMessage = intl.formatMessage(expandedMessage, {
const expandedMessage = secureGetFromRecord(postTypeMessages, postType)?.many_expanded;
// We default to empty string, but this should never happen
const formattedMessage = expandedMessage ? intl.formatMessage(expandedMessage, {
users: usernames.slice(0, lastIndex).join(', '),
lastUser,
actor,
});
}) : '';
return (
<Markdown
@ -72,6 +75,8 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las
const firstUser = usernames[0];
const numOthers = usernames.length - 1;
const message = secureGetFromRecord(systemMessages, postType);
return (
<Text>
<FormattedMarkdownText
@ -96,8 +101,8 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las
</Text>
<FormattedMarkdownText
channelId={channelId}
id={systemMessages[postType].id}
defaultMessage={systemMessages[postType].defaultMessage}
id={message?.id || ''}
defaultMessage={message?.defaultMessage || ''}
location={location}
values={{actor}}
baseTextStyle={style.baseText}

View file

@ -18,6 +18,7 @@ import {openGalleryAtIndex} from '@utils/gallery';
import {generateId} from '@utils/general';
import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images';
import {changeOpacity} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {extractFilenameFromUrl, isImageLink, isValidUrl} from '@utils/url';
import type {GalleryItemType} from '@typings/screens/gallery';
@ -55,7 +56,7 @@ const ImagePreview = ({expandedLink, isReplyPost, layoutWidth, link, location, m
const fileId = useRef(generateId('uid')).current;
const [imageUrl, setImageUrl] = useState(expandedLink || link);
const isTablet = useIsTablet();
const imageProps = metadata?.images?.[link];
const imageProps = secureGetFromRecord(metadata?.images, link);
const dimensions = calculateDimensions(imageProps?.height, imageProps?.width, layoutWidth || getViewPortWidth(isReplyPost, isTablet));
const onError = useCallback(() => {

View file

@ -9,6 +9,7 @@ import {useServerUrl} from '@context/server';
import {getStatusColors} from '@utils/message_attachment_colors';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import ActionButtonText from './action_button_text';
@ -65,7 +66,7 @@ const ActionButton = ({buttonColor, cookie, disabled, id, name, postId, theme}:
if (buttonColor) {
const STATUS_COLORS = getStatusColors(theme);
const hexColor = STATUS_COLORS[buttonColor] || theme[buttonColor] || buttonColor;
const hexColor = secureGetFromRecord(STATUS_COLORS, buttonColor) || secureGetFromRecord(theme, buttonColor) || buttonColor;
customButtonStyle = {borderColor: changeOpacity(hexColor, 0.25), backgroundColor: '#ffffff'};
customButtonTextStyle = {color: hexColor};
}

View file

@ -7,6 +7,7 @@ import {View} from 'react-native';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {getStatusColors} from '@utils/message_attachment_colors';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {isValidUrl} from '@utils/url';
import AttachmentActions from './attachment_actions';
@ -62,7 +63,7 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
if (attachment.color) {
if (attachment.color[0] === '#') {
borderStyle = {borderLeftColor: attachment.color};
} else if (STATUS_COLORS[attachment.color]) {
} else if (secureGetFromRecord(STATUS_COLORS, attachment.color)) {
borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]};
}
}

View file

@ -16,6 +16,7 @@ import {isTablet} from '@utils/helpers';
import {calculateDimensions} from '@utils/images';
import {type BestImage, getNearestPoint} from '@utils/opengraph';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {extractFilenameFromUrl, isValidUrl} from '@utils/url';
import type {GalleryItemType} from '@typings/screens/gallery';
@ -71,10 +72,7 @@ const OpengraphImage = ({isReplyPost, layoutWidth, location, metadata, openGraph
const imageUrl = (bestImage.secure_url || bestImage.url)!;
const imagesMetadata = metadata?.images;
let ogImage;
if (imagesMetadata && imagesMetadata[imageUrl]) {
ogImage = imagesMetadata[imageUrl];
}
let ogImage = secureGetFromRecord(imagesMetadata, imageUrl);
if (!ogImage) {
ogImage = openGraphImages.find((i: BestImage) => i.url === imageUrl || i.secure_url === imageUrl);

View file

@ -12,6 +12,7 @@ import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {typography} from '@utils/typography';
import type PostModel from '@typings/database/models/servers/post';
@ -276,7 +277,7 @@ export const SystemMessage = ({post, location, author, hideGuestTags}: SystemMes
return renderAddGuestToChannelMessage({post, author, location, styles, intl, theme}, hideGuestTags);
}
const renderer = systemMessageRenderers[post.type];
const renderer = secureGetFromRecord(systemMessageRenderers, post.type);
if (!renderer) {
return (
<Markdown

View file

@ -163,15 +163,15 @@ export default function SelectedUsers({
const users = useMemo(() => {
const u = [];
for (const id of Object.keys(selectedIds)) {
if (!selectedIds[id]) {
for (const [id, user] of Object.entries(selectedIds)) {
if (!user) {
continue;
}
u.push(
<SelectedUser
key={id}
user={selectedIds[id]}
user={user}
teammateNameDisplay={teammateNameDisplay}
onRemove={onRemove}
testID={`${testID}.selected_user`}

View file

@ -10,6 +10,7 @@ import ThemeProvider from '@context/theme';
import UserLocaleProvider from '@context/user_locale';
import DatabaseManager from '@database/manager';
import {subscribeActiveServers} from '@database/subscription/servers';
import {secureGetFromRecord} from '@utils/types';
import type {Database} from '@nozbe/watermelondb';
import type ServersModel from '@typings/database/models/app/servers';
@ -31,7 +32,7 @@ export function withServerDatabase<T extends JSX.IntrinsicAttributes>(Component:
if (server) {
const database =
DatabaseManager.serverDatabases[server.url]?.database;
secureGetFromRecord(DatabaseManager.serverDatabases, server.url)?.database;
if (database) {
setState({

View file

@ -20,6 +20,7 @@ import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import DialogIntroductionText from '../interactive_dialog/dialog_introduction_text';
import {buildNavigationButton, dismissModal, setButtons} from '../navigation';
@ -263,7 +264,7 @@ function AppsFormComponent({
elements?.forEach((element) => {
const newError = checkDialogElementForError(
element,
element.name === form.submit_buttons ? button : values[element.name],
element.name === form.submit_buttons ? button : secureGetFromRecord(values, element.name),
);
if (newError) {
hasErrors = true;
@ -408,13 +409,17 @@ function AppsFormComponent({
/>
}
{form.fields && form.fields.filter((f) => f.name !== form.submit_buttons).map((field) => {
const value = secureGetFromRecord(values, field.name);
if (!value) {
return null;
}
return (
<AppsFormField
field={field}
key={field.name}
name={field.name}
errorText={errors[field.name]}
value={values[field.name]}
errorText={secureGetFromRecord(errors, field.name)}
value={value}
performLookup={performLookup}
onChange={onChange}
/>

View file

@ -68,8 +68,7 @@ const SkinSelector = ({onSelectSkin, selected, skins}: Props) => {
/>
</View>
<View style={[styles.skins, isTablet && {marginRight: 10}]}>
{Object.keys(skins).map((key) => {
const name = skins[key];
{Object.entries(skins).map(([key, name]) => {
return (
<View
key={name}

View file

@ -44,8 +44,7 @@ const styles = StyleSheet.create({
},
});
const skins = Object.keys(skinCodes).reduce<Record<string, string>>((result, value) => {
const skin = skinCodes[value];
const skins = Object.entries(skinCodes).reduce<Record<string, string>>((result, [value, skin]) => {
if (value === 'default') {
result[value] = 'hand';
} else {

View file

@ -15,6 +15,7 @@ import {useIsTablet} from '@hooks/device';
import {dismissOverlay} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import Icon from './icon';
import Server from './server';
@ -148,7 +149,7 @@ const InAppNotification = ({componentId, serverName, serverUrl, notification}: I
// eslint-disable-next-line new-cap
const gesture = Gesture.Pan().activeOffsetY(-20).onStart(() => runOnJS(animateDismissOverlay)());
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database;
return (
<GestureHandlerRootView style={styles.gestureHandler}>

View file

@ -24,6 +24,7 @@ import {
} from '@screens/navigation';
import {filterChannelsMatchingTerm} from '@utils/channel';
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {typography} from '@utils/typography';
import ChannelListRow from './channel_list_row';
@ -68,7 +69,8 @@ const extractItemKey = (dataSource: string, item: DataType): string => {
const toggleFromMap = <T extends DialogOption | Channel | UserProfile>(current: MultiselectSelectedMap, key: string, item: T): MultiselectSelectedMap => {
const newMap = {...current};
if (current[key]) {
const hasValue = Boolean(secureGetFromRecord<any>(current, key));
if (hasValue) {
delete newMap[key];
} else {
newMap[key] = item;
@ -96,7 +98,7 @@ const filterSearchData = (source: string, searchData: DataTypeList, searchTerm:
const handleIdSelection = (dataSource: string, currentIds: {[id: string]: DataType}, item: DataType) => {
const newSelectedIds = {...currentIds};
const key = extractItemKey(dataSource, item);
const wasSelected = currentIds[key];
const wasSelected = secureGetFromRecord(currentIds, key);
if (wasSelected) {
Reflect.deleteProperty(newSelectedIds, key);
@ -239,7 +241,9 @@ function IntegrationSelector(
} else {
setMultiselectSelected((current) => {
const multiselectSelectedItems = {...current};
delete multiselectSelectedItems[itemKey];
if (secureGetFromRecord<any>(multiselectSelectedItems, itemKey) !== undefined) {
delete multiselectSelectedItems[itemKey];
}
return multiselectSelectedItems;
});
}
@ -470,7 +474,7 @@ function IntegrationSelector(
}, [multiselectSelected, theme, isMultiselect]);
const renderOptionItem = useCallback((itemProps: any) => {
const itemSelected = Boolean(multiselectSelected[itemProps.item.value]);
const itemSelected = Boolean(secureGetFromRecord<any>(multiselectSelected, itemProps.item.value));
return (
<OptionListRow
key={itemProps.id}

View file

@ -16,6 +16,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import DialogElement from './dialog_element';
import DialogIntroductionText from './dialog_introduction_text';
@ -142,7 +143,7 @@ function InteractiveDialog({
let hasErrors = false;
if (elements) {
elements.forEach((elem) => {
const newError = checkDialogElementForError(elem, values[elem.name]);
const newError = checkDialogElementForError(elem, secureGetFromRecord(values, elem.name));
if (newError) {
newErrors[elem.name] = intl.formatMessage({id: newError.id, defaultMessage: newError.defaultMessage}, newError.values);
hasErrors = true;
@ -244,6 +245,10 @@ function InteractiveDialog({
/>
}
{Boolean(elements) && elements.map((e) => {
const value = secureGetFromRecord(values, e.name);
if (value === undefined) {
return null;
}
return (
<DialogElement
key={'dialogelement' + e.name}
@ -252,13 +257,13 @@ function InteractiveDialog({
type={e.type}
subtype={e.subtype}
helpText={e.help_text}
errorText={errors[e.name]}
errorText={secureGetFromRecord(errors, e.name)}
placeholder={e.placeholder}
maxLength={e.max_length}
dataSource={e.data_source}
optional={e.optional}
options={e.options}
value={values[e.name]}
value={value}
onChange={onChange}
/>
);

View file

@ -19,6 +19,7 @@ import {dismissModal, setButtons} from '@screens/navigation';
import {isEmail} from '@utils/helpers';
import {mergeNavigationOptions} from '@utils/navigation';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {isGuest} from '@utils/user';
import Selection from './selection';
@ -183,7 +184,7 @@ export default function Invite({
const id = email ? item : (item as UserProfile).id;
const newSelectedIds = Object.assign({}, selectedIds);
if (!selectedIds[id]) {
if (!secureGetFromRecord(selectedIds, id)) {
newSelectedIds[id] = item;
}
@ -301,8 +302,9 @@ export default function Invite({
}
for (const email of emails) {
if (membersWithError[email]) {
notSent.push({userId: email, reason: membersWithError[email]});
const error = secureGetFromRecord(membersWithError, email);
if (error) {
notSent.push({userId: email, reason: error});
} else {
sent.push({userId: email, reason: formatMessage({id: 'invite.summary.email_invite', defaultMessage: 'An invitation email has been sent'})});
}

View file

@ -9,6 +9,7 @@ import CompassIcon from '@components/compass_icon';
import UserItem from '@components/user_item';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {typography} from '@utils/typography';
import TextItem, {TextItemType} from './text_item';
@ -114,7 +115,7 @@ export default function SummaryReport({
</Text>
</View>
{invites.map(({userId, reason}) => {
const item = selectedIds[userId];
const item = secureGetFromRecord(selectedIds, userId);
return (
<View

View file

@ -26,6 +26,7 @@ import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {closePermalink} from '@utils/permalink';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
import {typography} from '@utils/typography';
import PermalinkError from './permalink_error';
@ -176,7 +177,7 @@ function Permalink({
return;
}
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database;
if (!database) {
setError({unreachable: true});
setLoading(false);

6
app/utils/types.ts Normal file
View file

@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export function secureGetFromRecord<T>(v: Record<string, T> | undefined, key: string) {
return typeof v === 'object' && v && Object.prototype.hasOwnProperty.call(v, key) ? v[key] : undefined;
}