Chore: Properly type MessageActions (#3930)

This commit is contained in:
Diego Mello 2022-03-31 09:38:20 -03:00 committed by GitHub
parent c483abfa60
commit 4416f82665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 129 additions and 105 deletions

View File

@ -2,7 +2,7 @@ import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
import ActionSheet from './ActionSheet';
export type TActionSheetOptionsItem = { title: string; icon: string; onPress: () => void };
export type TActionSheetOptionsItem = { title: string; icon: string; onPress: () => void; danger?: boolean };
export type TActionSheetOptions = {
options: TActionSheetOptionsItem[];

View File

@ -8,6 +8,7 @@ const CustomEmoji = React.memo(
<FastImage
style={style}
source={{
// @ts-ignore
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`,
priority: FastImage.priority.high
}}

View File

@ -56,6 +56,7 @@ class EmojiCategory extends React.Component<Partial<IEmojiCategory>> {
contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes
key={`emoji-category-${width}`}
// @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis}
extraData={this.props}

View File

@ -109,6 +109,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: any;
try {
// @ts-ignore
freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} catch (error) {
// Do nothing

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { withTheme } from '../../theme';
import { useTheme } from '../../theme';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
@ -10,26 +10,30 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles';
import { IEmoji } from '../../definitions/IEmoji';
import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji';
import { TAnyMessageModel } from '../../definitions';
import { IEmoji } from '../../definitions/IEmoji';
interface IHeader {
handleReaction: Function;
type TItem = TFrequentlyUsedEmojiModel | string;
export interface IHeader {
handleReaction: (emoji: TItem, message: TAnyMessageModel) => void;
server: string;
message: object;
message: TAnyMessageModel;
isMasterDetail: boolean;
theme?: string;
}
type TOnReaction = ({ emoji }: { emoji: TItem }) => void;
interface THeaderItem {
item: IEmoji;
onReaction: Function;
item: TItem;
onReaction: TOnReaction;
server: string;
theme: string;
}
interface THeaderFooter {
onReaction: any;
onReaction: TOnReaction;
theme: string;
}
@ -62,25 +66,32 @@ const styles = StyleSheet.create({
}
});
const keyExtractor = (item: any) => item?.id || item;
const keyExtractor = (item: TItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
return (emojiModel.id ? emojiModel.content : item) as string;
};
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
const HeaderItem = React.memo(({ item, onReaction, server, theme }: THeaderItem) => (
const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
const emoji = (emojiModel.id ? emojiModel.content : item) as string;
return (
<Button
testID={`message-actions-emoji-${item.content || item}`}
onPress={() => onReaction({ emoji: `:${item.content || item}:` })}
testID={`message-actions-emoji-${emoji}`}
onPress={() => onReaction({ emoji: `:${emoji}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}>
{item?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
{emojiModel?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={emojiModel as IEmoji} baseUrl={server} />
) : (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item.content || item}:`)}</Text>
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
)}
</Button>
));
);
};
const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
<Button
testID='add-reaction'
onPress={onReaction}
@ -88,17 +99,19 @@ const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
theme={theme}>
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
</Button>
));
);
const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => {
const [items, setItems] = useState<(TFrequentlyUsedEmojiModel | string)[]>([]);
const { width, height }: any = useDimensions();
const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => {
const [items, setItems] = useState<TItem[]>([]);
const { width, height } = useDimensions();
const { theme } = useTheme();
// TODO: create custom hook to re-render based on screen size
const setEmojis = async () => {
try {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis: (TFrequentlyUsedEmojiModel | string)[] = await freqEmojiCollection.query().fetch();
let freqEmojis: TItem[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
@ -115,22 +128,21 @@ const Header = React.memo(({ handleReaction, server, message, isMasterDetail, th
setEmojis();
}, []);
const onReaction = ({ emoji }: { emoji: IEmoji }) => handleReaction(emoji, message);
const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = useCallback(
({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme!} />,
[]
const renderItem = ({ item }: { item: TItem }) => (
<HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
);
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme!} />, []);
const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
return (
<View style={[styles.container, { backgroundColor: themes[theme!].focusedBackground }]}>
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList
data={items}
renderItem={renderItem}
ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme!].focusedBackground }}
style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={keyExtractor}
showsHorizontalScrollIndicator={false}
scrollEnabled={false}
@ -140,4 +152,4 @@ const Header = React.memo(({ handleReaction, server, message, isMasterDetail, th
);
});
export default withTheme(Header);
export default Header;

View File

@ -13,33 +13,33 @@ import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../utils/log/events';
import { ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
export interface IMessageActions {
room: TSubscriptionModel;
tmid?: string;
user: Pick<ILoggedUser, 'id'>;
editInit: Function;
reactionInit: Function;
onReactionPress: Function;
replyInit: Function;
editInit: (message: TAnyMessageModel) => void;
reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: (shortname: string, messageId: string) => void;
replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean;
isReadOnly: boolean;
Message_AllowDeleting: boolean;
Message_AllowDeleting_BlockDeleteInMinutes: number;
Message_AllowEditing: boolean;
Message_AllowEditing_BlockEditInMinutes: number;
Message_AllowPinning: boolean;
Message_AllowStarring: boolean;
Message_Read_Receipt_Store_Users: boolean;
Message_AllowDeleting?: boolean;
Message_AllowDeleting_BlockDeleteInMinutes?: number;
Message_AllowEditing?: boolean;
Message_AllowEditing_BlockEditInMinutes?: number;
Message_AllowPinning?: boolean;
Message_AllowStarring?: boolean;
Message_Read_Receipt_Store_Users?: boolean;
server: string;
editMessagePermission: [];
deleteMessagePermission: [];
forceDeleteMessagePermission: [];
pinMessagePermission: [];
editMessagePermission?: string[];
deleteMessagePermission?: string[];
forceDeleteMessagePermission?: string[];
pinMessagePermission?: string[];
}
const MessageActions = React.memo(
@ -69,9 +69,14 @@ const MessageActions = React.memo(
pinMessagePermission
}: IMessageActions,
ref
): any => {
let permissions: any = {};
const { showActionSheet, hideActionSheet }: any = useActionSheet();
) => {
let permissions = {
hasEditPermission: false,
hasDeletePermission: false,
hasForceDeletePermission: false,
hasPinPermission: false
};
const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async () => {
try {
@ -88,9 +93,9 @@ const MessageActions = React.memo(
}
};
const isOwn = (message: any) => message.u && message.u._id === user.id;
const isOwn = (message: TAnyMessageModel) => message.u && message.u._id === user.id;
const allowEdit = (message: any) => {
const allowEdit = (message: TAnyMessageModel) => {
if (isReadOnly) {
return false;
}
@ -105,7 +110,7 @@ const MessageActions = React.memo(
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff: any;
let currentTsDiff = 0;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
@ -114,7 +119,7 @@ const MessageActions = React.memo(
return true;
};
const allowDelete = (message: any) => {
const allowDelete = (message: TAnyMessageModel) => {
if (isReadOnly) {
return false;
}
@ -136,7 +141,7 @@ const MessageActions = React.memo(
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff: any;
let currentTsDiff = 0;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
@ -145,19 +150,19 @@ const MessageActions = React.memo(
return true;
};
const getPermalink = (message: any) => RocketChat.getPermalinkMessage(message);
const getPermalink = (message: TAnyMessageModel) => RocketChat.getPermalinkMessage(message);
const handleReply = (message: any) => {
const handleReply = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true);
};
const handleEdit = (message: any) => {
const handleEdit = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message);
};
const handleCreateDiscussion = (message: any) => {
const handleCreateDiscussion = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) {
@ -167,7 +172,7 @@ const MessageActions = React.memo(
}
};
const handleUnread = async (message: any) => {
const handleUnread = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message;
const { rid } = room;
@ -179,7 +184,7 @@ const MessageActions = React.memo(
const subRecord = await subCollection.find(rid);
await db.write(async () => {
try {
await subRecord.update(sub => (sub.lastOpen = ts));
await subRecord.update(sub => (sub.lastOpen = ts as Date)); // TODO: reevaluate IMessage
} catch {
// do nothing
}
@ -192,42 +197,44 @@ const MessageActions = React.memo(
}
};
const handlePermalink = async (message: any) => {
const handlePermalink = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try {
const permalink: any = await getPermalink(message);
Clipboard.setString(permalink);
const permalink = await getPermalink(message);
Clipboard.setString(permalink ?? '');
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch {
logEvent(events.ROOM_MSG_ACTION_PERMALINK_F);
}
};
const handleCopy = async (message: any) => {
const handleCopy = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString(message?.attachments?.[0]?.description || message.msg);
await Clipboard.setString((message?.attachments?.[0]?.description || message.msg) ?? '');
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async (message: any) => {
const handleShare = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_SHARE);
try {
const permalink: any = await getPermalink(message);
const permalink = await getPermalink(message);
if (permalink) {
Share.share({ message: permalink });
}
} catch {
logEvent(events.ROOM_MSG_ACTION_SHARE_F);
}
};
const handleQuote = (message: any) => {
const handleQuote = (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false);
};
const handleStar = async (message: any) => {
const handleStar = async (message: TAnyMessageModel) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
await RocketChat.toggleStarMessage(message.id, message.starred as boolean); // TODO: reevaluate `message.starred` type on IMessage
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_STAR_F);
@ -235,20 +242,21 @@ const MessageActions = React.memo(
}
};
const handlePin = async (message: any) => {
const handlePin = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_PIN);
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
await RocketChat.togglePinMessage(message.id, message.pinned as boolean); // TODO: reevaluate `message.pinned` type on IMessage
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_PIN_F);
log(e);
}
};
const handleReaction = (shortname: any, message: any) => {
const handleReaction: IHeader['handleReaction'] = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) {
onReactionPress(shortname, message.id);
// TODO: evaluate unification with IEmoji
onReactionPress(shortname as any, message.id);
} else {
reactionInit(message);
}
@ -256,7 +264,7 @@ const MessageActions = React.memo(
hideActionSheet();
};
const handleReadReceipt = (message: any) => {
const handleReadReceipt = (message: TAnyMessageModel) => {
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else {
@ -291,7 +299,7 @@ const MessageActions = React.memo(
}
};
const handleReport = async (message: any) => {
const handleReport = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_REPORT);
try {
await RocketChat.reportMessage(message.id);
@ -302,14 +310,14 @@ const MessageActions = React.memo(
}
};
const handleDelete = (message: any) => {
const handleDelete = (message: TAnyMessageModel) => {
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'),
onPress: async () => {
try {
logEvent(events.ROOM_MSG_ACTION_DELETE);
await RocketChat.deleteMessage(message.id, message.subscription.id);
await RocketChat.deleteMessage(message.id, message.subscription ? message.subscription.id : '');
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e);
@ -319,7 +327,7 @@ const MessageActions = React.memo(
};
const getOptions = (message: TAnyMessageModel) => {
let options: any = [];
let options: TActionSheetOptionsItem[] = [];
// Reply
if (!isReadOnly) {
@ -463,16 +471,15 @@ const MessageActions = React.memo(
}
)
);
const mapStateToProps = (state: any) => ({
const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server,
Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
Message_AllowDeleting: state.settings.Message_AllowDeleting as boolean,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes as number,
Message_AllowEditing: state.settings.Message_AllowEditing as boolean,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes as number,
Message_AllowPinning: state.settings.Message_AllowPinning as boolean,
Message_AllowStarring: state.settings.Message_AllowStarring as boolean,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users as boolean,
isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'],

View File

@ -1,8 +1,9 @@
// TODO: evaluate unification with IEmoji
export interface IEmoji {
content: any;
name: string;
extension: any;
isCustom: boolean;
content?: string;
name?: string;
extension?: string;
isCustom?: boolean;
}
export interface ICustomEmoji {

View File

@ -1,5 +1,6 @@
import Model from '@nozbe/watermelondb/Model';
// TODO: evaluate unification with IEmoji
export interface IFrequentlyUsedEmoji {
content?: string;
extension?: string;

View File

@ -7,7 +7,7 @@ import { TNavigationOptions } from './definitions/navigationTypes';
export interface IDimensionsContextProps {
width: number;
height: number;
scale?: number;
scale: number;
fontScale: number;
setDimensions?: ({
width,