From 488074b4aea8f71f4a4f3801ac1a5cbc2d4cfa42 Mon Sep 17 00:00:00 2001 From: Alex Junior Date: Fri, 1 Apr 2022 18:52:38 -0300 Subject: [PATCH] Chore: Evaluate Message - TypeScript (#3944) Co-authored-by: GleidsonDaniel --- app/actions/messages.ts | 3 +- app/containers/markdown/interfaces.ts | 2 +- app/containers/message/Attachments.tsx | 11 +-- app/containers/message/Audio.tsx | 53 +++++----- app/containers/message/Blocks.ts | 20 ++-- app/containers/message/Broadcast.tsx | 7 +- app/containers/message/CallButton.tsx | 32 ++++--- app/containers/message/Content.tsx | 19 ++-- app/containers/message/Discussion.tsx | 9 +- app/containers/message/Encrypted.tsx | 9 +- app/containers/message/Image.tsx | 28 +++--- app/containers/message/Message.tsx | 9 +- app/containers/message/MessageError.tsx | 12 +-- app/containers/message/Reactions.tsx | 28 +++--- app/containers/message/ReadReceipt.tsx | 10 +- app/containers/message/RepliedThread.tsx | 7 +- app/containers/message/Reply.tsx | 30 ++---- app/containers/message/Thread.tsx | 8 +- app/containers/message/Urls.tsx | 39 ++------ app/containers/message/User.tsx | 20 ++-- app/containers/message/Video.tsx | 14 +-- app/containers/message/index.tsx | 117 ++++++++++------------- app/containers/message/interfaces.ts | 108 +++++++-------------- app/definitions/IAttachment.ts | 2 +- app/definitions/IMessage.ts | 6 +- app/definitions/IUrl.ts | 1 - app/stacks/types.ts | 2 +- app/views/MessagesView/index.tsx | 13 +-- app/views/RoomView/index.tsx | 2 +- app/views/SearchMessagesView/index.tsx | 8 +- 30 files changed, 271 insertions(+), 358 deletions(-) diff --git a/app/actions/messages.ts b/app/actions/messages.ts index eb0383ab..733de0a5 100644 --- a/app/actions/messages.ts +++ b/app/actions/messages.ts @@ -1,8 +1,7 @@ import { Action } from 'redux'; import { MESSAGES } from './actionsTypes'; - -type IMessage = Record; +import { IMessage } from '../definitions'; interface IReplyBroadcast extends Action { message: IMessage; diff --git a/app/containers/markdown/interfaces.ts b/app/containers/markdown/interfaces.ts index ed4984f4..ae5a8cf1 100644 --- a/app/containers/markdown/interfaces.ts +++ b/app/containers/markdown/interfaces.ts @@ -1,6 +1,6 @@ export interface IUserMention { _id: string; - username: string; + username?: string; name?: string; type?: string; } diff --git a/app/containers/message/Attachments.tsx b/app/containers/message/Attachments.tsx index 1203c55a..3989c212 100644 --- a/app/containers/message/Attachments.tsx +++ b/app/containers/message/Attachments.tsx @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import { dequal } from 'dequal'; import { Text } from 'react-native'; -import { IMessageAttachments, IMessageAttachedActions } from './interfaces'; +import { IMessageAttachments } from './interfaces'; import Image from './Image'; import Audio from './Audio'; import Video from './Video'; @@ -23,7 +23,7 @@ export type TElement = { text: string; }; -const AttachedActions = ({ attachment }: IMessageAttachedActions) => { +const AttachedActions = ({ attachment }: { attachment: IAttachment }) => { if (!attachment.actions) { return null; } @@ -55,8 +55,7 @@ const AttachedActions = ({ attachment }: IMessageAttachedActions) => { ); }; -const Attachments = React.memo( - // @ts-ignore +const Attachments: React.FC = React.memo( ({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply }: IMessageAttachments) => { if (!attachments || attachments.length === 0) { return null; @@ -64,7 +63,7 @@ const Attachments = React.memo( const { theme } = useTheme(); - return attachments.map((file: IAttachment, index: number) => { + const attachmentsElements = attachments.map((file: IAttachment, index: number) => { if (file && file.image_url) { return ( ); } @@ -109,6 +107,7 @@ const Attachments = React.memo( return ; }); + return <>{attachmentsElements}; }, (prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments) ); diff --git a/app/containers/message/Audio.tsx b/app/containers/message/Audio.tsx index d364c0d3..30d56409 100644 --- a/app/containers/message/Audio.tsx +++ b/app/containers/message/Audio.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native'; -import { Audio } from 'expo-av'; +import { Audio, AVPlaybackStatus } from 'expo-av'; import Slider from '@react-native-community/slider'; import moment from 'moment'; import { dequal } from 'dequal'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; +import { Sound } from 'expo-av/build/Audio/Sound'; import Touchable from './Touchable'; import Markdown from '../markdown'; @@ -23,7 +24,7 @@ interface IButton { paused: boolean; theme: string; disabled?: boolean; - onPress: Function; + onPress: () => void; } interface IMessageAudioProps { @@ -108,7 +109,7 @@ Button.displayName = 'MessageAudioButton'; class MessageAudio extends React.Component { static contextType = MessageContext; - private sound: any; + private sound: Sound; constructor(props: IMessageAudioProps) { super(props); @@ -141,7 +142,7 @@ class MessageAudio extends React.Component { + onPlaybackStatusUpdate = (status: AVPlaybackStatus) => { if (status) { this.onLoad(status); this.onProgress(status); @@ -190,26 +191,32 @@ class MessageAudio extends React.Component { - const duration = data.durationMillis / 1000; - this.setState({ duration: duration > 0 ? duration : 0 }); - }; - - onProgress = (data: any) => { - const { duration } = this.state; - const currentTime = data.positionMillis / 1000; - if (currentTime <= duration) { - this.setState({ currentTime }); + onLoad = (data: AVPlaybackStatus) => { + if (data.isLoaded && data.durationMillis) { + const duration = data.durationMillis / 1000; + this.setState({ duration: duration > 0 ? duration : 0 }); } }; - onEnd = async (data: any) => { - if (data.didJustFinish) { - try { - await this.sound.stopAsync(); - this.setState({ paused: true, currentTime: 0 }); - } catch { - // do nothing + onProgress = (data: AVPlaybackStatus) => { + if (data.isLoaded) { + const { duration } = this.state; + const currentTime = data.positionMillis / 1000; + if (currentTime <= duration) { + this.setState({ currentTime }); + } + } + }; + + onEnd = async (data: AVPlaybackStatus) => { + if (data.isLoaded) { + if (data.didJustFinish) { + try { + await this.sound.stopAsync(); + this.setState({ paused: true, currentTime: 0 }); + } catch { + // do nothing + } } } }; @@ -238,7 +245,7 @@ class MessageAudio extends React.Component { + onValueChange = async (value: number) => { try { this.setState({ currentTime: value }); await this.sound.setPositionAsync(value * 1000); diff --git a/app/containers/message/Blocks.ts b/app/containers/message/Blocks.ts index ef2f422c..27fa97b2 100644 --- a/app/containers/message/Blocks.ts +++ b/app/containers/message/Blocks.ts @@ -8,15 +8,17 @@ const Blocks = React.memo(({ blocks, id: mid, rid, blockAction }: IMessageBlocks const appId = blocks[0]?.appId || ''; return React.createElement( messageBlockWithContext({ - action: async ({ actionId, value, blockId }: any) => { - await blockAction({ - actionId, - appId, - value, - blockId, - rid, - mid - }); + action: async ({ actionId, value, blockId }: { actionId: string; value: string; blockId: string }) => { + if (blockAction) { + await blockAction({ + actionId, + appId, + value, + blockId, + rid, + mid + }); + } }, appId, rid diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/Broadcast.tsx index 56bd0da7..267b6282 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/Broadcast.tsx @@ -9,10 +9,13 @@ import I18n from '../../i18n'; import { themes } from '../../constants/colors'; import MessageContext from './Context'; import { IMessageBroadcast } from './interfaces'; +import { useTheme } from '../../theme'; -const Broadcast = React.memo(({ author, broadcast, theme }: IMessageBroadcast) => { +const Broadcast = React.memo(({ author, broadcast }: IMessageBroadcast) => { const { user, replyBroadcast } = useContext(MessageContext); - const isOwn = author._id === user.id; + const { theme } = useTheme(); + const isOwn = author?._id === user.id; + if (broadcast && !isOwn) { return ( diff --git a/app/containers/message/CallButton.tsx b/app/containers/message/CallButton.tsx index 7e908bf5..7df5a7d1 100644 --- a/app/containers/message/CallButton.tsx +++ b/app/containers/message/CallButton.tsx @@ -8,21 +8,25 @@ import I18n from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; import { themes } from '../../constants/colors'; import { IMessageCallButton } from './interfaces'; +import { useTheme } from '../../theme'; -const CallButton = React.memo(({ theme, callJitsi }: IMessageCallButton) => ( - - - <> - - {I18n.t('Click_to_join')} - - - -)); +const CallButton = React.memo(({ callJitsi }: IMessageCallButton) => { + const { theme } = useTheme(); + return ( + + + <> + + {I18n.t('Click_to_join')} + + + + ); +}); CallButton.displayName = 'CallButton'; diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index add46cfb..919b1dbb 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -12,15 +12,17 @@ import MessageContext from './Context'; import Encrypted from './Encrypted'; import { E2E_MESSAGE_TYPE } from '../../lib/encryption/constants'; import { IMessageContent } from './interfaces'; +import { useTheme } from '../../theme'; const Content = React.memo( (props: IMessageContent) => { + const { theme } = useTheme(); if (props.isInfo) { // @ts-ignore const infoMessage = getInfoMessage({ ...props }); const renderMessageContent = ( - + {infoMessage} ); @@ -36,14 +38,12 @@ const Content = React.memo( return renderMessageContent; } - const isPreview: any = props.tmid && !props.isThreadRoom; + const isPreview = props.tmid && !props.isThreadRoom; let content = null; if (props.isEncrypted) { content = ( - + {I18n.t('Encrypted_message')} ); @@ -65,7 +65,7 @@ const Content = React.memo( navToRoomInfo={props.navToRoomInfo} tmid={props.tmid} useRealName={props.useRealName} - theme={props.theme} + theme={theme} onLinkPress={onLinkPress} /> ); @@ -76,13 +76,13 @@ const Content = React.memo( content = ( {content} - + ); } if (props.isIgnored) { - content = {I18n.t('Message_Ignored')}; + content = {I18n.t('Message_Ignored')}; } return {content}; @@ -97,9 +97,6 @@ const Content = React.memo( if (prevProps.type !== nextProps.type) { return false; } - if (prevProps.theme !== nextProps.theme) { - return false; - } if (prevProps.isEncrypted !== nextProps.isEncrypted) { return false; } diff --git a/app/containers/message/Discussion.tsx b/app/containers/message/Discussion.tsx index 2f9a782f..6ef8f13b 100644 --- a/app/containers/message/Discussion.tsx +++ b/app/containers/message/Discussion.tsx @@ -10,10 +10,12 @@ import { DISCUSSION } from './constants'; import { themes } from '../../constants/colors'; import MessageContext from './Context'; import { formatDateThreads } from '../../utils/room'; -import { IMessageDiscussion } from './interfaces'; +import { IMessage } from '../../definitions'; +import { useTheme } from '../../theme'; const Discussion = React.memo( - ({ msg, dcount, dlm, theme }: IMessageDiscussion) => { + ({ msg, dcount, dlm }: Pick) => { + const { theme } = useTheme(); let time; if (dlm) { time = formatDateThreads(dlm); @@ -50,9 +52,6 @@ const Discussion = React.memo( if (prevProps.dlm !== nextProps.dlm) { return false; } - if (prevProps.theme !== nextProps.theme) { - return false; - } return true; } ); diff --git a/app/containers/message/Encrypted.tsx b/app/containers/message/Encrypted.tsx index 1c3ac080..c2818f22 100644 --- a/app/containers/message/Encrypted.tsx +++ b/app/containers/message/Encrypted.tsx @@ -7,13 +7,10 @@ import { themes } from '../../constants/colors'; import { BUTTON_HIT_SLOP } from './utils'; import MessageContext from './Context'; import styles from './styles'; +import { useTheme } from '../../theme'; -interface IMessageEncrypted { - type: string; - theme: string; -} - -const Encrypted = React.memo(({ type, theme }: IMessageEncrypted) => { +const Encrypted = React.memo(({ type }: { type: string }) => { + const { theme } = useTheme(); if (type !== E2E_MESSAGE_TYPE) { return null; } diff --git a/app/containers/message/Image.tsx b/app/containers/message/Image.tsx index b0937ad5..7fb20b49 100644 --- a/app/containers/message/Image.tsx +++ b/app/containers/message/Image.tsx @@ -12,25 +12,20 @@ import { formatAttachmentUrl } from '../../lib/utils'; import { themes } from '../../constants/colors'; import MessageContext from './Context'; import { TGetCustomEmoji } from '../../definitions/IEmoji'; -import { useTheme } from '../../theme'; import { IAttachment } from '../../definitions'; +import { useTheme } from '../../theme'; -type TMessageButton = { - children: JSX.Element; +interface IMessageButton { + children: React.ReactElement; disabled?: boolean; - onPress: Function; + onPress: () => void; theme: string; -}; - -type TMessageImage = { - img: string; - theme: string; -}; +} interface IMessageImage { file: IAttachment; imageUrl?: string; - showAttachment?: Function; + showAttachment?: (file: IAttachment) => void; style?: StyleProp[]; isReply?: boolean; getCustomEmoji?: TGetCustomEmoji; @@ -38,7 +33,7 @@ interface IMessageImage { const ImageProgress = createImageProgress(FastImage); -const Button = React.memo(({ children, onPress, disabled, theme }: TMessageButton) => ( +const Button = React.memo(({ children, onPress, disabled, theme }: IMessageButton) => ( )); -export const MessageImage = React.memo(({ img, theme }: TMessageImage) => ( +export const MessageImage = React.memo(({ imgUri, theme }: { imgUri: string; theme: string }) => ( - + ); @@ -97,7 +93,7 @@ const ImageContainer = React.memo( return ( ); }, diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 01185761..5c22293b 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -19,6 +19,7 @@ import ReadReceipt from './ReadReceipt'; import CallButton from './CallButton'; import { themes } from '../../constants/colors'; import { IMessage, IMessageInner, IMessageTouchable } from './interfaces'; +import { useTheme } from '../../theme'; const MessageInner = React.memo((props: IMessageInner) => { const { attachments } = props; @@ -85,7 +86,6 @@ const Message = React.memo((props: IMessage) => { {thread} - {/* @ts-ignore */} @@ -98,12 +98,11 @@ const Message = React.memo((props: IMessage) => { return ( - {/* @ts-ignore */} - + ); @@ -119,12 +118,14 @@ const MessageTouchable = React.memo((props: IMessageTouchable & IMessage) => { ); } const { onPress, onLongPress } = useContext(MessageContext); + const { theme } = useTheme(); + return ( + style={{ backgroundColor: props.highlighted ? themes[theme].headerBackground : null }}> diff --git a/app/containers/message/MessageError.tsx b/app/containers/message/MessageError.tsx index 5405f955..d72c8c91 100644 --- a/app/containers/message/MessageError.tsx +++ b/app/containers/message/MessageError.tsx @@ -6,14 +6,12 @@ import styles from './styles'; import { BUTTON_HIT_SLOP } from './utils'; import { themes } from '../../constants/colors'; import MessageContext from './Context'; - -interface IMessageError { - hasError: boolean; - theme: string; -} +import { useTheme } from '../../theme'; const MessageError = React.memo( - ({ hasError, theme }: IMessageError) => { + ({ hasError }: { hasError: boolean }) => { + const { theme } = useTheme(); + if (!hasError) { return null; } @@ -24,7 +22,7 @@ const MessageError = React.memo( ); }, - (prevProps, nextProps) => prevProps.hasError === nextProps.hasError && prevProps.theme === nextProps.theme + (prevProps, nextProps) => prevProps.hasError === nextProps.hasError ); MessageError.displayName = 'MessageError'; diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/Reactions.tsx index 2a681a8d..346b2400 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/Reactions.tsx @@ -7,30 +7,28 @@ import styles from './styles'; import Emoji from './Emoji'; import { BUTTON_HIT_SLOP } from './utils'; import { themes } from '../../constants/colors'; -import { withTheme } from '../../theme'; +import { useTheme } from '../../theme'; import MessageContext from './Context'; import { TGetCustomEmoji } from '../../definitions/IEmoji'; -interface IMessageAddReaction { - theme: string; +interface IReaction { + _id: string; + emoji: string; + usernames: string[]; } interface IMessageReaction { - reaction: { - usernames: []; - emoji: object; - }; + reaction: IReaction; getCustomEmoji: TGetCustomEmoji; theme: string; } interface IMessageReactions { - reactions?: object[]; + reactions?: IReaction[]; getCustomEmoji: TGetCustomEmoji; - theme: string; } -const AddReaction = React.memo(({ theme }: IMessageAddReaction) => { +const AddReaction = React.memo(({ theme }: { theme: string }) => { const { reactionInit } = useContext(MessageContext); return ( { const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => { const { onReactionPress, onReactionLongPress, baseUrl, user } = useContext(MessageContext); - const reacted = reaction.usernames.findIndex((item: IMessageReaction) => item === user.username) !== -1; + const reacted = reaction.usernames.findIndex((item: string) => item === user.username) !== -1; return ( onReactionPress(reaction.emoji)} @@ -76,13 +74,15 @@ const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReacti ); }); -const Reactions = React.memo(({ reactions, getCustomEmoji, theme }: IMessageReactions) => { +const Reactions = React.memo(({ reactions, getCustomEmoji }: IMessageReactions) => { + const { theme } = useTheme(); + if (!Array.isArray(reactions) || reactions.length === 0) { return null; } return ( - {reactions.map((reaction: any) => ( + {reactions.map(reaction => ( ))} @@ -94,4 +94,4 @@ Reaction.displayName = 'MessageReaction'; Reactions.displayName = 'MessageReactions'; AddReaction.displayName = 'MessageAddReaction'; -export default withTheme(Reactions); +export default Reactions; diff --git a/app/containers/message/ReadReceipt.tsx b/app/containers/message/ReadReceipt.tsx index 8a5298ee..4a68acf1 100644 --- a/app/containers/message/ReadReceipt.tsx +++ b/app/containers/message/ReadReceipt.tsx @@ -3,14 +3,10 @@ import React from 'react'; import { themes } from '../../constants/colors'; import { CustomIcon } from '../../lib/Icons'; import styles from './styles'; +import { useTheme } from '../../theme'; -interface IMessageReadReceipt { - isReadReceiptEnabled: boolean; - unread: boolean; - theme: string; -} - -const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread, theme }: IMessageReadReceipt) => { +const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread: boolean }) => { + const { theme } = useTheme(); if (isReadReceiptEnabled && !unread && unread !== null) { return ; } diff --git a/app/containers/message/RepliedThread.tsx b/app/containers/message/RepliedThread.tsx index 9f1e5639..fc0f9257 100644 --- a/app/containers/message/RepliedThread.tsx +++ b/app/containers/message/RepliedThread.tsx @@ -7,15 +7,18 @@ import { themes } from '../../constants/colors'; import I18n from '../../i18n'; import { MarkdownPreview } from '../markdown'; import { IMessageRepliedThread } from './interfaces'; +import { useTheme } from '../../theme'; + +const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted }: IMessageRepliedThread) => { + const { theme } = useTheme(); -const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme }: IMessageRepliedThread) => { if (!tmid || !isHeader) { return null; } const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg); const fetch = async () => { - const threadName = await fetchThreadName(tmid, id); + const threadName = fetchThreadName ? await fetchThreadName(tmid, id) : ''; setMsg(threadName); }; diff --git a/app/containers/message/Reply.tsx b/app/containers/message/Reply.tsx index 4f197bc4..4705c554 100644 --- a/app/containers/message/Reply.tsx +++ b/app/containers/message/Reply.tsx @@ -77,7 +77,7 @@ const styles = StyleSheet.create({ marginBottom: 4 }, image: { - // @ts-ignore + // @ts-ignore TODO - check with the team, change this to undefined width: null, height: 200, flex: 1, @@ -93,24 +93,6 @@ const styles = StyleSheet.create({ } }); -interface IMessageTitle { - attachment: IAttachment; - timeFormat?: string; - theme: string; -} - -interface IMessageDescription { - attachment: IAttachment; - getCustomEmoji: TGetCustomEmoji; - theme: string; -} - -interface IMessageFields { - attachment: IAttachment; - theme: string; - getCustomEmoji: TGetCustomEmoji; -} - interface IMessageReply { attachment: IAttachment; timeFormat?: string; @@ -118,7 +100,7 @@ interface IMessageReply { getCustomEmoji: TGetCustomEmoji; } -const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => { +const Title = React.memo(({ attachment, timeFormat, theme }: { attachment: IAttachment; timeFormat?: string; theme: string }) => { const time = attachment.message_link && attachment.ts ? moment(attachment.ts).format(timeFormat) : null; return ( @@ -132,7 +114,7 @@ const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => { }); const Description = React.memo( - ({ attachment, getCustomEmoji, theme }: IMessageDescription) => { + ({ attachment, getCustomEmoji, theme }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji; theme: string }) => { const text = attachment.text || attachment.title; if (!text) { return null; @@ -164,7 +146,7 @@ const Description = React.memo( ); const UrlImage = React.memo( - ({ image }: any) => { + ({ image }: { image?: string }) => { if (!image) { return null; } @@ -176,7 +158,7 @@ const UrlImage = React.memo( ); const Fields = React.memo( - ({ attachment, theme, getCustomEmoji }: IMessageFields) => { + ({ attachment, theme, getCustomEmoji }: { attachment: IAttachment; theme: string; getCustomEmoji: TGetCustomEmoji }) => { if (!attachment.fields) { return null; } @@ -206,12 +188,12 @@ const Fields = React.memo( const Reply = React.memo( ({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => { const [loading, setLoading] = useState(false); + const { theme } = useTheme(); if (!attachment) { return null; } - const { theme } = useTheme(); const { baseUrl, user, jumpToMessage } = useContext(MessageContext); const onPress = async () => { diff --git a/app/containers/message/Thread.tsx b/app/containers/message/Thread.tsx index 4ddfda3b..4bfed49a 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/Thread.tsx @@ -7,9 +7,12 @@ import MessageContext from './Context'; import ThreadDetails from '../ThreadDetails'; import I18n from '../../i18n'; import { IMessageThread } from './interfaces'; +import { useTheme } from '../../theme'; const Thread = React.memo( - ({ msg, tcount, tlm, isThreadRoom, theme, id }: IMessageThread) => { + ({ msg, tcount, tlm, isThreadRoom, id }: IMessageThread) => { + const { theme } = useTheme(); + if (!tlm || isThreadRoom || tcount === 0) { return null; } @@ -38,9 +41,6 @@ const Thread = React.memo( if (prevProps.tcount !== nextProps.tcount) { return false; } - if (prevProps.theme !== nextProps.theme) { - return false; - } return true; } ); diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index 022984e8..65da7cd5 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -8,11 +8,12 @@ import Touchable from './Touchable'; import openLink from '../../utils/openLink'; import sharedStyles from '../../views/Styles'; import { themes } from '../../constants/colors'; -import { withTheme } from '../../theme'; +import { useTheme, withTheme } from '../../theme'; import { LISTENER } from '../Toast'; import EventEmitter from '../../utils/events'; import I18n from '../../i18n'; import MessageContext from './Context'; +import { IUrl } from '../../definitions'; const styles = StyleSheet.create({ button: { @@ -50,29 +51,6 @@ const styles = StyleSheet.create({ } }); -interface IMessageUrlContent { - title: string; - description: string; - theme: string; -} - -interface IMessageUrl { - url: { - ignoreParse: boolean; - url: string; - image: string; - title: string; - description: string; - }; - index: number; - theme: string; -} - -interface IMessageUrls { - urls?: any; - theme?: string; -} - const UrlImage = React.memo( ({ image }: { image: string }) => { if (!image) { @@ -86,7 +64,7 @@ const UrlImage = React.memo( ); const UrlContent = React.memo( - ({ title, description, theme }: IMessageUrlContent) => ( + ({ title, description, theme }: { title: string; description: string; theme: string }) => ( {title ? ( @@ -115,7 +93,7 @@ const UrlContent = React.memo( ); const Url = React.memo( - ({ url, index, theme }: IMessageUrl) => { + ({ url, index, theme }: { url: IUrl; index: number; theme: string }) => { if (!url || url?.ignoreParse) { return null; } @@ -152,14 +130,17 @@ const Url = React.memo( ); const Urls = React.memo( - ({ urls, theme }: IMessageUrls) => { + // TODO - didn't work - (React.ReactElement | null)[] | React.ReactElement | null + ({ urls }: { urls?: IUrl[] }): any => { + const { theme } = useTheme(); + if (!urls || urls.length === 0) { return null; } - return urls.map((url: any, index: number) => ); + return urls.map((url: IUrl, index: number) => ); }, - (oldProps, newProps) => dequal(oldProps.urls, newProps.urls) && oldProps.theme === newProps.theme + (oldProps, newProps) => dequal(oldProps.urls, newProps.urls) ); UrlImage.displayName = 'MessageUrlImage'; diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index 17494dee..ed42c225 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -3,12 +3,14 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import moment from 'moment'; import { themes } from '../../constants/colors'; -import { withTheme } from '../../theme'; +import { useTheme } from '../../theme'; import MessageError from './MessageError'; import sharedStyles from '../../views/Styles'; import messageStyles from './styles'; import MessageContext from './Context'; import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME } from './utils'; +import { SubscriptionType } from '../../definitions'; +import { IRoomInfoParam } from '../../views/SearchMessagesView'; const styles = StyleSheet.create({ container: { @@ -49,15 +51,15 @@ interface IMessageUser { alias?: string; ts?: Date; timeFormat?: string; - theme: string; - navToRoomInfo?: Function; + navToRoomInfo?: (navParam: IRoomInfoParam) => void; type: string; } const User = React.memo( - ({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => { + ({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, navToRoomInfo, type, ...props }: IMessageUser) => { if (isHeader || hasError) { const { user } = useContext(MessageContext); + const { theme } = useTheme(); const username = (useRealName && author?.name) || author?.username; const aliasUsername = alias ? ( @{username} @@ -65,8 +67,8 @@ const User = React.memo( const time = moment(ts).format(timeFormat); const onUserPress = () => { navToRoomInfo?.({ - t: 'd', - rid: author?._id + t: SubscriptionType.DIRECT, + rid: author?._id || '' }); }; const isDisabled = author?._id === user.id; @@ -83,7 +85,7 @@ const User = React.memo( {textContent} @@ -98,7 +100,7 @@ const User = React.memo( {time} - {hasError && } + {hasError ? : null} ); } @@ -108,4 +110,4 @@ const User = React.memo( User.displayName = 'MessageUser'; -export default withTheme(User); +export default User; diff --git a/app/containers/message/Video.tsx b/app/containers/message/Video.tsx index 8490b66d..b493027c 100644 --- a/app/containers/message/Video.tsx +++ b/app/containers/message/Video.tsx @@ -16,9 +16,10 @@ import I18n from '../../i18n'; import { IAttachment } from '../../definitions/IAttachment'; import RCActivityIndicator from '../ActivityIndicator'; import { TGetCustomEmoji } from '../../definitions/IEmoji'; +import { useTheme } from '../../theme'; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; -const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1; +const isTypeSupported = (type: string) => SUPPORTED_TYPES.indexOf(type) !== -1; const styles = StyleSheet.create({ button: { @@ -33,23 +34,24 @@ const styles = StyleSheet.create({ interface IMessageVideo { file: IAttachment; - showAttachment?: Function; + showAttachment?: (file: IAttachment) => void; getCustomEmoji: TGetCustomEmoji; style?: StyleProp[]; isReply?: boolean; - theme: string; } const Video = React.memo( - ({ file, showAttachment, getCustomEmoji, style, isReply, theme }: IMessageVideo) => { + ({ file, showAttachment, getCustomEmoji, style, isReply }: IMessageVideo) => { const { baseUrl, user } = useContext(MessageContext); const [loading, setLoading] = useState(false); + const { theme } = useTheme(); if (!baseUrl) { return null; } + const onPress = async () => { - if (isTypeSupported(file.video_type) && showAttachment) { + if (file.video_type && isTypeSupported(file.video_type) && showAttachment) { return showAttachment(file); } @@ -93,7 +95,7 @@ const Video = React.memo( ); }, - (prevProps, nextProps) => dequal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme + (prevProps, nextProps) => dequal(prevProps.file, nextProps.file) ); export default Video; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index ab37997c..a892c15d 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Keyboard, ViewStyle } from 'react-native'; +import { Subscription } from 'rxjs'; import Message from './Message'; import MessageContext from './Context'; @@ -7,10 +8,11 @@ import debounce from '../../utils/debounce'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants'; import messagesStatus from '../../constants/messagesStatus'; -import { withTheme } from '../../theme'; +import { useTheme, withTheme } from '../../theme'; import openLink from '../../utils/openLink'; import { TGetCustomEmoji } from '../../definitions/IEmoji'; -import { TAnyMessageModel } from '../../definitions'; +import { IAttachment, TAnyMessageModel } from '../../definitions'; +import { IRoomInfoParam } from '../../views/SearchMessagesView'; interface IMessageContainerProps { item: TAnyMessageModel; @@ -20,7 +22,7 @@ interface IMessageContainerProps { token: string; }; msg?: string; - rid?: string; + rid: string; timeFormat?: string; style?: ViewStyle; archived?: boolean; @@ -37,44 +39,35 @@ interface IMessageContainerProps { isIgnored?: boolean; highlighted?: boolean; getCustomEmoji: TGetCustomEmoji; - onLongPress?: Function; - onReactionPress?: Function; - onEncryptedPress?: Function; - onDiscussionPress?: Function; - onThreadPress?: Function; - errorActionsShow?: Function; - replyBroadcast?: Function; - reactionInit?: Function; - fetchThreadName?: Function; - showAttachment?: Function; - onReactionLongPress?: Function; - navToRoomInfo?: Function; - callJitsi?: Function; - blockAction?: Function; - onAnswerButtonPress?: Function; - theme?: string; + onLongPress?: (item: TAnyMessageModel) => void; + onReactionPress?: (emoji: string, id: string) => void; + onEncryptedPress?: () => void; + onDiscussionPress?: (item: TAnyMessageModel) => void; + onThreadPress?: (item: TAnyMessageModel) => void; + errorActionsShow?: (item: TAnyMessageModel) => void; + replyBroadcast?: (item: TAnyMessageModel) => void; + reactionInit?: (item: TAnyMessageModel) => void; + fetchThreadName?: (tmid: string, id: string) => Promise; + showAttachment: (file: IAttachment) => void; + onReactionLongPress?: (item: TAnyMessageModel) => void; + navToRoomInfo: (navParam: IRoomInfoParam) => void; + callJitsi?: () => void; + blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; + onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void; threadBadgeColor?: string; - toggleFollowThread?: Function; - jumpToMessage?: Function; - onPress?: Function; + toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise; + jumpToMessage?: (link: string) => void; + onPress?: () => void; } -class MessageContainer extends React.Component { +interface IMessageContainerState { + isManualUnignored: boolean; +} + +class MessageContainer extends React.Component { static defaultProps = { getCustomEmoji: () => null, onLongPress: () => {}, - onReactionPress: () => {}, - onEncryptedPress: () => {}, - onDiscussionPress: () => {}, - onThreadPress: () => {}, - onAnswerButtonPress: () => {}, - errorActionsShow: () => {}, - replyBroadcast: () => {}, - reactionInit: () => {}, - fetchThreadName: () => {}, - showAttachment: () => {}, - onReactionLongPress: () => {}, - navToRoomInfo: () => {}, callJitsi: () => {}, blockAction: () => {}, archived: false, @@ -85,7 +78,7 @@ class MessageContainer extends React.Component { state = { isManualUnignored: false }; - private subscription: any; + private subscription?: Subscription; componentDidMount() { const { item } = this.props; @@ -97,12 +90,9 @@ class MessageContainer extends React.Component { } } - shouldComponentUpdate(nextProps: any, nextState: any) { + shouldComponentUpdate(nextProps: IMessageContainerProps, nextState: IMessageContainerState) { const { isManualUnignored } = this.state; - const { theme, threadBadgeColor, isIgnored, highlighted } = this.props; - if (nextProps.theme !== theme) { - return true; - } + const { threadBadgeColor, isIgnored, highlighted } = this.props; if (nextProps.highlighted !== highlighted) { return true; } @@ -169,7 +159,7 @@ class MessageContainer extends React.Component { } }; - onReactionPress = (emoji: any) => { + onReactionPress = (emoji: string) => { const { onReactionPress, item } = this.props; if (onReactionPress) { onReactionPress(emoji, item.id); @@ -228,7 +218,7 @@ class MessageContainer extends React.Component { previousItem.u.username === item.u.username && !(previousItem.groupable === false || item.groupable === false || broadcast === true) && // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense - item.ts - previousItem.ts < Message_GroupingPeriod! * 1000 && + item.ts - previousItem.ts < Message_GroupingPeriod * 1000 && previousItem.tmid === item.tmid ) { return false; @@ -303,10 +293,11 @@ class MessageContainer extends React.Component { }; onLinkPress = (link: string): void => { - const { item, theme, jumpToMessage } = this.props; - const isMessageLink = item?.attachments?.findIndex((att: any) => att?.message_link === link) !== -1; - if (isMessageLink) { - return jumpToMessage!(link); + const { theme } = useTheme(); + const { item, jumpToMessage } = this.props; + const isMessageLink = item?.attachments?.findIndex((att: IAttachment) => att?.message_link === link) !== -1; + if (isMessageLink && jumpToMessage) { + return jumpToMessage(link); } openLink(link, theme); }; @@ -332,7 +323,6 @@ class MessageContainer extends React.Component { callJitsi, blockAction, rid, - theme, threadBadgeColor, toggleFollowThread, jumpToMessage, @@ -371,8 +361,8 @@ class MessageContainer extends React.Component { let message = msg; // "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription // "autoTranslateMessage" is a toggle between "View Original" and "Translate" state - if (autoTranslateRoom && autoTranslateMessage) { - message = getMessageTranslation(item, autoTranslateLanguage!) || message; + if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage) { + message = getMessageTranslation(item, autoTranslateLanguage) || message; } return ( @@ -396,14 +386,15 @@ class MessageContainer extends React.Component { toggleFollowThread, replies }}> + {/* @ts-ignore*/} { emoji={emoji} timeFormat={timeFormat} style={style} - archived={archived!} - broadcast={broadcast!} + archived={archived} + broadcast={broadcast} useRealName={useRealName} - isReadReceiptEnabled={isReadReceiptEnabled!} + isReadReceiptEnabled={isReadReceiptEnabled} unread={unread} role={role} drid={drid} dcount={dcount} - // @ts-ignore dlm={dlm} tmid={tmid} tcount={tcount} - // @ts-ignore tlm={tlm} tmsg={tmsg} - fetchThreadName={fetchThreadName!} - // @ts-ignore + fetchThreadName={fetchThreadName} mentions={mentions} channels={channels} isIgnored={this.isIgnored} @@ -442,13 +430,12 @@ class MessageContainer extends React.Component { isTemp={this.isTemp} isEncrypted={this.isEncrypted} hasError={this.hasError} - showAttachment={showAttachment!} + showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} - navToRoomInfo={navToRoomInfo!} - callJitsi={callJitsi!} - blockAction={blockAction!} - theme={theme as string} - highlighted={highlighted!} + navToRoomInfo={navToRoomInfo} + callJitsi={callJitsi} + blockAction={blockAction} + highlighted={highlighted} /> ); diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index a5428176..f7156053 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -1,63 +1,45 @@ import { MarkdownAST } from '@rocket.chat/message-parser'; import { StyleProp, TextStyle } from 'react-native'; +import { ImageStyle } from '@rocket.chat/react-native-fast-image'; -import { IUserChannel, IUserMention } from '../markdown/interfaces'; +import { IUserChannel } from '../markdown/interfaces'; import { TGetCustomEmoji } from '../../definitions/IEmoji'; -import { IAttachment } from '../../definitions'; - -export type TMessageType = 'discussion-created' | 'jitsi_call_started'; +import { IAttachment, IThread, IUrl, IUserMention, IUserMessage, MessageType, TAnyMessageModel } from '../../definitions'; +import { IRoomInfoParam } from '../../views/SearchMessagesView'; export interface IMessageAttachments { attachments?: IAttachment[]; timeFormat?: string; style?: StyleProp[]; isReply?: boolean; - showAttachment?: Function; + showAttachment?: (file: IAttachment) => void; getCustomEmoji: TGetCustomEmoji; } -export interface IMessageAttachedActions { - attachment: IAttachment; -} - export interface IMessageAvatar { isHeader: boolean; - avatar: string; - emoji: string; - author: { - username: string; - _id: string; - }; + avatar?: string; + emoji?: string; + author?: IUserMessage; small?: boolean; - navToRoomInfo: Function; + navToRoomInfo: (navParam: IRoomInfoParam) => void; getCustomEmoji: TGetCustomEmoji; } export interface IMessageBlocks { - blocks: any; + blocks: { appId?: string }[]; id: string; rid: string; - blockAction: Function; + blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; } export interface IMessageBroadcast { - author: { - _id: string; - }; - broadcast: boolean; - theme: string; + author?: IUserMessage; + broadcast?: boolean; } export interface IMessageCallButton { - theme: string; - callJitsi: Function; -} - -export interface IUser { - id: string; - username: string; - token: string; - name: string; + callJitsi?: () => void; } export interface IMessageContent { @@ -68,40 +50,27 @@ export interface IMessageContent { isThreadRoom: boolean; msg?: string; md?: MarkdownAST; - theme: string; isEdited: boolean; isEncrypted: boolean; getCustomEmoji: TGetCustomEmoji; channels?: IUserChannel[]; mentions?: IUserMention[]; - navToRoomInfo?: Function; + navToRoomInfo: (navParam: IRoomInfoParam) => void; useRealName?: boolean; isIgnored: boolean; type: string; } -export interface IMessageDiscussion { - msg?: string; - dcount?: number; - dlm?: Date; - theme: string; -} - export interface IMessageEmoji { - content: any; + content: string; baseUrl: string; - standardEmojiStyle: object; - customEmojiStyle: object; + standardEmojiStyle: { fontSize: number }; + customEmojiStyle: StyleProp; getCustomEmoji: TGetCustomEmoji; } -export interface IMessageThread { - msg?: string; - tcount?: number | null; - theme: string; - tlm?: Date; +export interface IMessageThread extends Pick { isThreadRoom: boolean; - id: string; } export interface IMessageTouchable { @@ -109,40 +78,35 @@ export interface IMessageTouchable { isInfo: boolean; isThreadReply: boolean; isTemp: boolean; - archived: boolean; - highlighted: boolean; - theme: string; - ts?: any; - urls?: any; + archived?: boolean; + highlighted?: boolean; + ts?: string | Date; + urls?: IUrl[]; reactions?: any; - alias?: any; - role?: any; - drid?: any; + alias?: string; + role?: string; + drid?: string; } -export interface IMessageRepliedThread { - tmid?: string; - tmsg?: string; - id: string; +export interface IMessageRepliedThread extends Pick { isHeader: boolean; - theme: string; - fetchThreadName: Function; + fetchThreadName?: (tmid: string, id: string) => Promise; isEncrypted: boolean; } export interface IMessageInner - extends IMessageDiscussion, - IMessageContent, + extends IMessageContent, IMessageCallButton, IMessageBlocks, IMessageThread, IMessageAttachments, IMessageBroadcast { - type: TMessageType; + type: MessageType; blocks: []; + urls?: IUrl[]; } -export interface IMessage extends IMessageRepliedThread, IMessageInner { +export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar { isThreadReply: boolean; isThreadSequential: boolean; isInfo: boolean; @@ -150,9 +114,11 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner { isHeader: boolean; hasError: boolean; style: any; - onLongPress: Function; - isReadReceiptEnabled: boolean; + // style: ViewStyle; + onLongPress?: (item: TAnyMessageModel) => void; + isReadReceiptEnabled?: boolean; unread?: boolean; - theme: string; isIgnored: boolean; + dcount: number | undefined; + dlm: string | Date | undefined; } diff --git a/app/definitions/IAttachment.ts b/app/definitions/IAttachment.ts index 7aedd95a..e2f6f391 100644 --- a/app/definitions/IAttachment.ts +++ b/app/definitions/IAttachment.ts @@ -19,7 +19,7 @@ export interface IAttachment { image_size?: number; author_name?: string; author_icon?: string; - actions?: []; + actions?: { type: string; msg: string; text: string }[]; message_link?: string; text?: string; short?: boolean; diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts index 4765b2ca..7a6cf234 100644 --- a/app/definitions/IMessage.ts +++ b/app/definitions/IMessage.ts @@ -6,7 +6,7 @@ import { IAttachment } from './IAttachment'; import { IReaction } from './IReaction'; import { TThreadMessageModel } from './IThreadMessage'; import { TThreadModel } from './IThread'; -import { IUrlFromServer } from './IUrl'; +import { IUrl, IUrlFromServer } from './IUrl'; export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj' | MessageTypeLoad; @@ -75,7 +75,7 @@ export interface IMessageFromServer { ts: string | Date; // wm date issue u: IUserMessage; _updatedAt: string | Date; - urls?: IUrlFromServer[]; + urls?: IUrl[]; mentions?: IUserMention[]; channels?: IUserChannel[]; md?: MarkdownAST; @@ -111,7 +111,7 @@ export interface ILoadMoreMessage { export interface IMessage extends IMessageFromServer { id: string; - t?: MessageType; + t: MessageType; alias?: string; parseUrls?: boolean; avatar?: string; diff --git a/app/definitions/IUrl.ts b/app/definitions/IUrl.ts index 3210afa2..cf12b94d 100644 --- a/app/definitions/IUrl.ts +++ b/app/definitions/IUrl.ts @@ -45,5 +45,4 @@ export interface IUrl extends IUrlFromServer { title: string; description: string; image: string; - url: string; } diff --git a/app/stacks/types.ts b/app/stacks/types.ts index 119dfa70..ecfd988c 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -54,7 +54,7 @@ export type ChatsStackParamList = { }; RoomInfoView: { room?: ISubscription; - member: any; + member?: any; rid: string; t: SubscriptionType; showCloseModal?: boolean; diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 78983693..4eef9e8f 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -22,6 +22,7 @@ import styles from './styles'; import { ChatsStackParamList } from '../../stacks/types'; import { ISubscription, SubscriptionType } from '../../definitions/ISubscription'; import { IEmoji } from '../../definitions/IEmoji'; +import { IRoomInfoParam } from '../SearchMessagesView'; import { TMessageModel } from '../../definitions'; interface IMessagesViewProps { @@ -43,14 +44,6 @@ interface IMessagesViewProps { isMasterDetail: boolean; } -interface IRoomInfoParam { - room: ISubscription; - member: any; - rid: string; - t: SubscriptionType; - joined: boolean; -} - interface IMessagesViewState { loading: boolean; messages: []; @@ -188,7 +181,8 @@ class MessagesView extends React.Component { showAttachment: this.showAttachment, getCustomEmoji: this.getCustomEmoji, navToRoomInfo: this.navToRoomInfo, - onPress: () => this.jumpToMessage({ item }) + onPress: () => this.jumpToMessage({ item }), + rid: this.rid }); return { @@ -219,7 +213,6 @@ class MessagesView extends React.Component { } ] }} - theme={theme} /> ) }, diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index c38e1c28..c6dc8d13 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -867,7 +867,7 @@ class RoomView extends React.Component { } }; - replyBroadcast = (message: Record) => { + replyBroadcast = (message: IMessage) => { const { dispatch } = this.props; dispatch(replyBroadcast(message)); }; diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 585bf76a..c6b5579d 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -42,12 +42,12 @@ interface ISearchMessagesViewState { searchText: string; } -interface IRoomInfoParam { - room: ISubscription; - member: any; +export interface IRoomInfoParam { + room?: ISubscription; + member?: any; rid: string; t: SubscriptionType; - joined: boolean; + joined?: boolean; } interface INavigationOption {