From 6b3730ce433de6131d4044e2ae8143a19942dbaa Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 2 Mar 2022 11:18:01 -0300 Subject: [PATCH] Chore: Migrate RoomView to Typescript (#3754) --- app/containers/MessageActions/index.tsx | 26 +- app/containers/MessageBox/index.tsx | 2 +- app/containers/ReactionsModal.tsx | 11 +- app/containers/ThreadDetails.tsx | 8 +- app/containers/message/Message.tsx | 2 +- app/containers/message/Reply.tsx | 4 +- app/containers/message/User.tsx | 19 +- app/containers/message/index.tsx | 60 +- app/containers/message/interfaces.ts | 34 +- app/containers/message/utils.ts | 5 +- app/definitions/IEmoji.ts | 8 +- app/definitions/IMessage.ts | 19 +- app/definitions/ISubscription.ts | 4 +- app/definitions/IThread.ts | 40 +- app/definitions/IThreadMessage.ts | 42 +- app/lib/database/model/Thread.js | 2 + app/lib/database/model/migrations.js | 9 + app/lib/database/schema/app.js | 5 +- app/lib/methods/canOpenRoom.ts | 2 +- app/lib/methods/loadMessagesForRoom.ts | 12 +- app/lib/methods/loadMissedMessages.ts | 4 +- app/lib/methods/loadNextMessages.ts | 12 +- app/lib/methods/readMessages.ts | 7 +- app/lib/methods/sendFileMessage.ts | 4 +- app/lib/methods/sendMessage.ts | 8 +- app/lib/methods/updateMessages.ts | 4 +- app/lib/rocketchat/rocketchat.js | 2 +- app/lib/rocketchat/services/restApi.ts | 2 +- app/reducers/settings.ts | 2 +- app/stacks/types.ts | 11 +- app/utils/isReadOnly.ts | 13 +- app/utils/room.ts | 6 +- .../DiscussionsView/DiscussionDetails.tsx | 9 +- app/views/DiscussionsView/Item.tsx | 1 + app/views/MessagesView/index.tsx | 7 +- app/views/RoomView/{Banner.js => Banner.tsx} | 23 +- .../RoomView/{EmptyRoom.js => EmptyRoom.tsx} | 12 +- .../RoomView/{JoinCode.js => JoinCode.tsx} | 31 +- app/views/RoomView/LeftButtons.js | 56 -- app/views/RoomView/LeftButtons.tsx | 72 +++ app/views/RoomView/List/{List.js => List.tsx} | 10 +- .../{NavBottomFAB.js => NavBottomFAB.tsx} | 23 +- .../RoomView/List/{index.js => index.tsx} | 122 ++-- .../RoomView/LoadMore/{index.js => index.tsx} | 9 +- .../{ReactionPicker.js => ReactionPicker.tsx} | 36 +- .../{RightButtons.js => RightButtons.tsx} | 85 ++- .../RoomView/{Separator.js => Separator.tsx} | 11 +- .../{UploadProgress.js => UploadProgress.tsx} | 73 +-- app/views/RoomView/{index.js => index.tsx} | 528 +++++++++++------- app/views/RoomView/services/getMessageInfo.js | 41 -- app/views/RoomView/services/getMessageInfo.ts | 41 ++ app/views/RoomView/services/getMessages.js | 10 - app/views/RoomView/services/getMessages.ts | 23 + .../RoomView/services/getMoreMessages.js | 27 - .../RoomView/services/getMoreMessages.ts | 40 ++ ...ThreadMessages.js => getThreadMessages.ts} | 2 +- .../RoomView/services/{index.js => index.ts} | 0 app/views/RoomView/services/readMessages.js | 5 - app/views/RoomView/services/readMessages.ts | 5 + app/views/RoomView/{styles.js => styles.ts} | 0 app/views/SearchMessagesView/index.tsx | 1 + app/views/ShareView/index.tsx | 7 +- app/views/ThreadMessagesView/Item.tsx | 1 + e2e/tests/room/04-discussion.spec.js | 3 - e2e/tests/team/03-moveconvert.spec.js | 3 + 65 files changed, 960 insertions(+), 746 deletions(-) rename app/views/RoomView/{Banner.js => Banner.tsx} (79%) rename app/views/RoomView/{EmptyRoom.js => EmptyRoom.tsx} (61%) rename app/views/RoomView/{JoinCode.js => JoinCode.tsx} (76%) delete mode 100644 app/views/RoomView/LeftButtons.js create mode 100644 app/views/RoomView/LeftButtons.tsx rename app/views/RoomView/List/{List.js => List.tsx} (77%) rename app/views/RoomView/List/{NavBottomFAB.js => NavBottomFAB.tsx} (82%) rename app/views/RoomView/List/{index.js => index.tsx} (77%) rename app/views/RoomView/LoadMore/{index.js => index.tsx} (88%) rename app/views/RoomView/{ReactionPicker.js => ReactionPicker.tsx} (75%) rename app/views/RoomView/{RightButtons.js => RightButtons.tsx} (69%) rename app/views/RoomView/{Separator.js => Separator.tsx} (88%) rename app/views/RoomView/{UploadProgress.js => UploadProgress.tsx} (69%) rename app/views/RoomView/{index.js => index.tsx} (69%) delete mode 100644 app/views/RoomView/services/getMessageInfo.js create mode 100644 app/views/RoomView/services/getMessageInfo.ts delete mode 100644 app/views/RoomView/services/getMessages.js create mode 100644 app/views/RoomView/services/getMessages.ts delete mode 100644 app/views/RoomView/services/getMoreMessages.js create mode 100644 app/views/RoomView/services/getMoreMessages.ts rename app/views/RoomView/services/{getThreadMessages.js => getThreadMessages.ts} (61%) rename app/views/RoomView/services/{index.js => index.ts} (100%) delete mode 100644 app/views/RoomView/services/readMessages.js create mode 100644 app/views/RoomView/services/readMessages.ts rename app/views/RoomView/{styles.js => styles.ts} (100%) diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index 13a1e369d..b6c6b89c1 100644 --- a/app/containers/MessageActions/index.tsx +++ b/app/containers/MessageActions/index.tsx @@ -15,19 +15,12 @@ import { showConfirmationAlert } from '../../utils/info'; import { useActionSheet } from '../ActionSheet'; import Header, { HEADER_HEIGHT } from './Header'; import events from '../../utils/log/events'; -import { TMessageModel } from '../../definitions/IMessage'; +import { ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; -interface IMessageActions { - room: { - rid: string; - autoTranslateLanguage: any; - autoTranslate: any; - reactWhenReadOnly: any; - }; - tmid: string; - user: { - id: string | number; - }; +export interface IMessageActions { + room: TSubscriptionModel; + tmid?: string; + user: Pick; editInit: Function; reactionInit: Function; onReactionPress: Function; @@ -270,8 +263,11 @@ const MessageActions = React.memo( } }; - const handleToggleTranslation = async (message: TMessageModel) => { + const handleToggleTranslation = async (message: TAnyMessageModel) => { try { + if (!room.autoTranslateLanguage) { + return; + } const db = database.active; await db.write(async () => { await message.update(m => { @@ -321,7 +317,7 @@ const MessageActions = React.memo( }); }; - const getOptions = (message: TMessageModel) => { + const getOptions = (message: TAnyMessageModel) => { let options: any = []; // Reply @@ -447,7 +443,7 @@ const MessageActions = React.memo( return options; }; - const showMessageActions = async (message: TMessageModel) => { + const showMessageActions = async (message: TAnyMessageModel) => { logEvent(events.ROOM_SHOW_MSG_ACTIONS); await getPermissions(); showActionSheet({ diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index 3c924df6f..13eefd7fa 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -72,7 +72,7 @@ const videoPickerConfig = { mediaType: 'video' }; -interface IMessageBoxProps { +export interface IMessageBoxProps { rid: string; baseUrl: string; message: IMessage; diff --git a/app/containers/ReactionsModal.tsx b/app/containers/ReactionsModal.tsx index e55f418d5..3240baff6 100644 --- a/app/containers/ReactionsModal.tsx +++ b/app/containers/ReactionsModal.tsx @@ -10,6 +10,7 @@ import sharedStyles from '../views/Styles'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; import { TGetCustomEmoji } from '../definitions/IEmoji'; +import { TMessageModel, ILoggedUser } from '../definitions'; import SafeAreaView from './SafeAreaView'; const styles = StyleSheet.create({ @@ -65,23 +66,25 @@ interface IItem { usernames: any; emoji: string; }; - user?: { username: any }; + user?: Pick; baseUrl?: string; getCustomEmoji?: TGetCustomEmoji; theme?: string; } interface IModalContent { - message?: { - reactions: any; - }; + message?: TMessageModel; onClose: Function; theme: string; } interface IReactionsModal { + message?: any; + user?: Pick; isVisible: boolean; onClose(): void; + baseUrl: string; + getCustomEmoji?: TGetCustomEmoji; theme: string; } diff --git a/app/containers/ThreadDetails.tsx b/app/containers/ThreadDetails.tsx index 5dc9fb954..2edb451e8 100644 --- a/app/containers/ThreadDetails.tsx +++ b/app/containers/ThreadDetails.tsx @@ -52,9 +52,9 @@ interface IThreadDetails { const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => { const { theme } = useTheme(); - let { tcount } = item; - if (tcount && tcount >= 1000) { - tcount = '+999'; + let count: string | number | undefined = item.tcount; + if (count && count >= 1000) { + count = '+999'; } let replies: number | string = item?.replies?.length ?? 0; @@ -70,7 +70,7 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT - {tcount} + {count} diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 7751fa028..5406add8c 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -87,7 +87,7 @@ const Message = React.memo((props: IMessage) => { - + ); diff --git a/app/containers/message/Reply.tsx b/app/containers/message/Reply.tsx index 0da83d6fd..7a7089248 100644 --- a/app/containers/message/Reply.tsx +++ b/app/containers/message/Reply.tsx @@ -94,7 +94,7 @@ const styles = StyleSheet.create({ interface IMessageTitle { attachment: IAttachment; - timeFormat: string; + timeFormat?: string; theme: string; } @@ -112,7 +112,7 @@ interface IMessageFields { interface IMessageReply { attachment: IAttachment; - timeFormat: string; + timeFormat?: string; index: number; theme: string; getCustomEmoji: TGetCustomEmoji; diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index b03620d9a..cc96df11d 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -40,7 +40,7 @@ const styles = StyleSheet.create({ interface IMessageUser { isHeader?: boolean; hasError?: boolean; - useRealName: boolean; + useRealName?: boolean; author?: { _id: string; name?: string; @@ -50,25 +50,26 @@ interface IMessageUser { ts?: Date; timeFormat?: string; theme: string; - navToRoomInfo: Function; + navToRoomInfo?: Function; type: string; } const User = React.memo( ({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => { if (isHeader || hasError) { - const navParam = { - t: 'd', - rid: author!._id - }; const { user } = useContext(MessageContext); - const username = (useRealName && author!.name) || author!.username; + const username = (useRealName && author?.name) || author?.username; const aliasUsername = alias ? ( @{username} ) : null; const time = moment(ts).format(timeFormat); - const onUserPress = () => navToRoomInfo(navParam); - const isDisabled = author!._id === user.id; + const onUserPress = () => { + navToRoomInfo?.({ + t: 'd', + rid: author?._id + }); + }; + const isDisabled = author?._id === user.id; const textContent = ( <> diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 1740fba49..ab37997c2 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -10,9 +10,10 @@ import messagesStatus from '../../constants/messagesStatus'; import { withTheme } from '../../theme'; import openLink from '../../utils/openLink'; import { TGetCustomEmoji } from '../../definitions/IEmoji'; +import { TAnyMessageModel } from '../../definitions'; interface IMessageContainerProps { - item: any; + item: TAnyMessageModel; user: { id: string; username: string; @@ -20,24 +21,16 @@ interface IMessageContainerProps { }; msg?: string; rid?: string; - timeFormat: string; + timeFormat?: string; style?: ViewStyle; archived?: boolean; broadcast?: boolean; - previousItem?: { - ts: any; - u: any; - groupable: any; - id: string; - tmid: string; - status: any; - }; - isHeader: boolean; + previousItem?: TAnyMessageModel; baseUrl: string; Message_GroupingPeriod?: number; isReadReceiptEnabled?: boolean; isThreadRoom: boolean; - useRealName: boolean; + useRealName?: boolean; autoTranslateRoom?: boolean; autoTranslateLanguage?: string; status?: number; @@ -59,11 +52,11 @@ interface IMessageContainerProps { callJitsi?: Function; blockAction?: Function; onAnswerButtonPress?: Function; - theme: string; + theme?: string; threadBadgeColor?: string; toggleFollowThread?: Function; jumpToMessage?: Function; - onPress: Function; + onPress?: Function; } class MessageContainer extends React.Component { @@ -222,7 +215,7 @@ class MessageContainer extends React.Component { this.setState({ isManualUnignored: true }); }; - get isHeader() { + get isHeader(): boolean { const { item, previousItem, broadcast, Message_GroupingPeriod } = this.props; if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { return true; @@ -230,9 +223,11 @@ class MessageContainer extends React.Component { try { if ( previousItem && + // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense previousItem.ts.toDateString() === item.ts.toDateString() && 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 && previousItem.tmid === item.tmid ) { @@ -244,7 +239,7 @@ class MessageContainer extends React.Component { } } - get isThreadReply() { + get isThreadReply(): boolean { const { item, previousItem, isThreadRoom } = this.props; if (isThreadRoom) { return false; @@ -255,37 +250,40 @@ class MessageContainer extends React.Component { return false; } - get isThreadSequential() { + get isThreadSequential(): boolean { const { item, isThreadRoom } = this.props; if (isThreadRoom) { return false; } - return item.tmid; + return !!item.tmid; } - get isEncrypted() { + get isEncrypted(): boolean { const { item } = this.props; const { t, e2e } = item; return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE; } - get isInfo() { + get isInfo(): boolean { const { item } = this.props; - return SYSTEM_MESSAGES.includes(item.t); + return (item.t && SYSTEM_MESSAGES.includes(item.t)) ?? false; } - get isTemp() { + get isTemp(): boolean { const { item } = this.props; return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR; } - get isIgnored() { + get isIgnored(): boolean { const { isManualUnignored } = this.state; const { isIgnored } = this.props; - return isManualUnignored ? false : isIgnored; + if (isManualUnignored) { + return false; + } + return isIgnored ?? false; } - get hasError() { + get hasError(): boolean { const { item } = this.props; return item.status === messagesStatus.ERROR; } @@ -405,13 +403,12 @@ class MessageContainer extends React.Component { rid={rid!} author={u} ts={ts} - type={t} + type={t as any} attachments={attachments} blocks={blocks} urls={urls} reactions={reactions} alias={alias} - /* @ts-ignore*/ avatar={avatar} emoji={emoji} timeFormat={timeFormat} @@ -424,16 +421,19 @@ class MessageContainer extends React.Component { role={role} drid={drid} dcount={dcount} + // @ts-ignore dlm={dlm} tmid={tmid} tcount={tcount} + // @ts-ignore tlm={tlm} tmsg={tmsg} fetchThreadName={fetchThreadName!} + // @ts-ignore mentions={mentions} channels={channels} - isIgnored={this.isIgnored!} - isEdited={editedBy && !!editedBy.username} + isIgnored={this.isIgnored} + isEdited={(editedBy && !!editedBy.username) ?? false} isHeader={this.isHeader} isThreadReply={this.isThreadReply} isThreadSequential={this.isThreadSequential} @@ -447,7 +447,7 @@ class MessageContainer extends React.Component { navToRoomInfo={navToRoomInfo!} callJitsi={callJitsi!} blockAction={blockAction!} - theme={theme} + theme={theme as string} highlighted={highlighted!} /> diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index a068cd9cd..ff0b0d421 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -7,7 +7,7 @@ export type TMessageType = 'discussion-created' | 'jitsi_call_started'; export interface IMessageAttachments { attachments: any; - timeFormat: string; + timeFormat?: string; showAttachment: Function; getCustomEmoji: TGetCustomEmoji; theme: string; @@ -66,26 +66,26 @@ export interface IMessageContent { _id: string; isTemp: boolean; isInfo: boolean; - tmid: string; + tmid?: string; isThreadRoom: boolean; - msg: string; - md: MarkdownAST; + msg?: string; + md?: MarkdownAST; theme: string; isEdited: boolean; isEncrypted: boolean; getCustomEmoji: TGetCustomEmoji; - channels: IUserChannel[]; - mentions: IUserMention[]; - navToRoomInfo: Function; - useRealName: boolean; + channels?: IUserChannel[]; + mentions?: IUserMention[]; + navToRoomInfo?: Function; + useRealName?: boolean; isIgnored: boolean; type: string; } export interface IMessageDiscussion { - msg: string; - dcount: number; - dlm: Date; + msg?: string; + dcount?: number; + dlm?: Date; theme: string; } @@ -98,10 +98,10 @@ export interface IMessageEmoji { } export interface IMessageThread { - msg: string; - tcount: number; + msg?: string; + tcount?: number; theme: string; - tlm: Date; + tlm?: Date; isThreadRoom: boolean; id: string; } @@ -123,8 +123,8 @@ export interface IMessageTouchable { } export interface IMessageRepliedThread { - tmid: string; - tmsg: string; + tmid?: string; + tmsg?: string; id: string; isHeader: boolean; theme: string; @@ -154,7 +154,7 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner { style: any; onLongPress: Function; isReadReceiptEnabled: boolean; - unread: boolean; + unread?: boolean; theme: string; isIgnored: boolean; } diff --git a/app/containers/message/utils.ts b/app/containers/message/utils.ts index b10f6f741..ea155028b 100644 --- a/app/containers/message/utils.ts +++ b/app/containers/message/utils.ts @@ -2,9 +2,12 @@ import { TMessageModel } from '../../definitions/IMessage'; import I18n from '../../i18n'; import { DISCUSSION } from './constants'; -export const formatMessageCount = (count: number, type: string) => { +export const formatMessageCount = (count?: number, type?: string): string => { const discussion = type === DISCUSSION; let text = discussion ? I18n.t('No_messages_yet') : null; + if (!count) { + return text; + } if (count === 1) { text = `${count} ${discussion ? I18n.t('message') : I18n.t('reply')}`; } else if (count > 1 && count < 1000) { diff --git a/app/definitions/IEmoji.ts b/app/definitions/IEmoji.ts index 58be90a24..94841aab1 100644 --- a/app/definitions/IEmoji.ts +++ b/app/definitions/IEmoji.ts @@ -21,4 +21,10 @@ export interface IEmojiCategory { tabLabel: string; } -export type TGetCustomEmoji = (name: string) => IEmoji | null; +// TODO: copied from reducers/customEmojis. We can unify later. +export interface IReduxEmoji { + name: string; + extension: any; +} + +export type TGetCustomEmoji = (name: string) => any; diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts index fcbf90d5f..0d0a4683f 100644 --- a/app/definitions/IMessage.ts +++ b/app/definitions/IMessage.ts @@ -3,9 +3,25 @@ import { MarkdownAST } from '@rocket.chat/message-parser'; import { IAttachment } from './IAttachment'; import { IReaction } from './IReaction'; +import { + MESSAGE_TYPE_LOAD_MORE, + MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, + MESSAGE_TYPE_LOAD_NEXT_CHUNK +} from '../constants/messageTypeLoad'; +import { TThreadMessageModel } from './IThreadMessage'; +import { TThreadModel } from './IThread'; import { IUrlFromServer } from './IUrl'; -export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj'; +export type MessageType = + | 'jitsi_call_started' + | 'discussion-created' + | 'e2e' + | 'load_more' + | 'rm' + | 'uj' + | typeof MESSAGE_TYPE_LOAD_MORE + | typeof MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK + | typeof MESSAGE_TYPE_LOAD_NEXT_CHUNK; export interface IUserMessage { _id: string; @@ -139,4 +155,5 @@ export interface IMessage extends IMessageFromServer { export type TMessageModel = IMessage & Model; +export type TAnyMessageModel = TMessageModel | TThreadModel | TThreadMessageModel; export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage; diff --git a/app/definitions/ISubscription.ts b/app/definitions/ISubscription.ts index f794b3c53..22caf04c6 100644 --- a/app/definitions/ISubscription.ts +++ b/app/definitions/ISubscription.ts @@ -73,8 +73,8 @@ export interface ISubscription { lastThreadSync?: Date; jitsiTimeout?: number; autoTranslate?: boolean; - autoTranslateLanguage: string; - lastMessage?: ILastMessage; + autoTranslateLanguage?: string; + lastMessage?: ILastMessage; // TODO: we need to use IMessage here hideUnreadStatus?: boolean; sysMes?: string[] | boolean; uids?: string[]; diff --git a/app/definitions/IThread.ts b/app/definitions/IThread.ts index 1294981c6..eebb392d8 100644 --- a/app/definitions/IThread.ts +++ b/app/definitions/IThread.ts @@ -2,8 +2,7 @@ import Model from '@nozbe/watermelondb/Model'; import { MarkdownAST } from '@rocket.chat/message-parser'; import { IAttachment } from './IAttachment'; -import { IEditedBy, IUserChannel, IUserMention, IUserMessage, MessageType } from './IMessage'; -import { IReaction } from './IReaction'; +import { IMessage, IUserChannel, IUserMention, IUserMessage } from './IMessage'; import { IUrl } from './IUrl'; interface IFileThread { @@ -34,42 +33,9 @@ export interface IThreadResult { tlm?: string | Date; } -export interface IThread { - id: string; +export interface IThread extends IMessage { tmsg?: string; - msg?: string; - t?: MessageType; - rid: string; - _updatedAt?: string | Date; - ts?: string | Date; - u?: IUserMessage; - alias?: string; - parseUrls?: boolean; - groupable?: boolean; - avatar?: string; - emoji?: string; - attachments?: IAttachment[]; - urls?: IUrl[]; - status?: number; - pinned?: boolean; - starred?: boolean; - editedBy?: IEditedBy; - reactions?: IReaction[]; - role?: string; - drid?: string; - dcount?: number | string; - dlm?: string | Date; - tmid?: string; - tcount?: number | string; - tlm?: string | Date; - replies?: string[]; - mentions?: IUserMention[]; - channels?: IUserChannel[]; - unread?: boolean; - autoTranslate?: boolean; - translations?: any; - e2e?: string; - subscription?: { id: string }; + draftMessage?: string; } export type TThreadModel = IThread & Model; diff --git a/app/definitions/IThreadMessage.ts b/app/definitions/IThreadMessage.ts index 4461dd4e7..7383d69eb 100644 --- a/app/definitions/IThreadMessage.ts +++ b/app/definitions/IThreadMessage.ts @@ -1,47 +1,9 @@ import Model from '@nozbe/watermelondb/Model'; -import { IAttachment } from './IAttachment'; -import { IEditedBy, ITranslations, IUserChannel, IUserMention, IUserMessage, MessageType } from './IMessage'; -import { IReaction } from './IReaction'; -import { IUrl } from './IUrl'; +import { IMessage } from './IMessage'; -export interface IThreadMessage { - id: string; - _id: string; +export interface IThreadMessage extends IMessage { tmsg?: string; - msg?: string; - t?: MessageType; - rid: string; - ts: string | Date; - u: IUserMessage; - alias?: string; - parseUrls?: boolean; - groupable?: boolean; - avatar?: string; - emoji?: string; - attachments?: IAttachment[]; - urls?: IUrl[]; - _updatedAt?: string | Date; - status?: number; - pinned?: boolean; - starred?: boolean; - editedBy?: IEditedBy; - reactions?: IReaction[]; - role?: string; - drid?: string; - dcount?: number; - dlm?: string | Date; - tmid?: string; - tcount?: number; - tlm?: string | Date; - replies?: string[]; - mentions?: IUserMention[]; - channels?: IUserChannel[]; - unread?: boolean; - autoTranslate?: boolean; - translations?: ITranslations[]; - e2e?: string; - subscription?: { id: string }; } export type TThreadMessageModel = IThreadMessage & Model; diff --git a/app/lib/database/model/Thread.js b/app/lib/database/model/Thread.js index 1224554b6..ff534c85d 100644 --- a/app/lib/database/model/Thread.js +++ b/app/lib/database/model/Thread.js @@ -75,4 +75,6 @@ export default class Thread extends Model { @json('translations', sanitizer) translations; @field('e2e') e2e; + + @field('draft_message') draftMessage; } diff --git a/app/lib/database/model/migrations.js b/app/lib/database/model/migrations.js index 15c1331fe..5ce47045b 100644 --- a/app/lib/database/model/migrations.js +++ b/app/lib/database/model/migrations.js @@ -199,6 +199,15 @@ export default schemaMigrations({ columns: [{ name: 'md', type: 'string', isOptional: true }] }) ] + }, + { + toVersion: 15, + steps: [ + addColumns({ + table: 'threads', + columns: [{ name: 'draft_message', type: 'string', isOptional: true }] + }) + ] } ] }); diff --git a/app/lib/database/schema/app.js b/app/lib/database/schema/app.js index 043ddb8e9..c76be539c 100644 --- a/app/lib/database/schema/app.js +++ b/app/lib/database/schema/app.js @@ -1,7 +1,7 @@ import { appSchema, tableSchema } from '@nozbe/watermelondb'; export default appSchema({ - version: 14, + version: 15, tables: [ tableSchema({ name: 'subscriptions', @@ -153,7 +153,8 @@ export default appSchema({ { name: 'unread', type: 'boolean', isOptional: true }, { name: 'auto_translate', type: 'boolean', isOptional: true }, { name: 'translations', type: 'string', isOptional: true }, - { name: 'e2e', type: 'string', isOptional: true } + { name: 'e2e', type: 'string', isOptional: true }, + { name: 'draft_message', type: 'string', isOptional: true } ] }), tableSchema({ diff --git a/app/lib/methods/canOpenRoom.ts b/app/lib/methods/canOpenRoom.ts index 7d3e72687..f9a567689 100644 --- a/app/lib/methods/canOpenRoom.ts +++ b/app/lib/methods/canOpenRoom.ts @@ -43,7 +43,7 @@ async function open({ type, rid, name }: { type: ERoomTypes; rid: string; name: if ((type === ERoomTypes.CHANNEL || type === ERoomTypes.GROUP) && !rid) { // RC 0.72.0 // @ts-ignore - const result: any = await sdk.get(`channel.info`, params); + const result: any = await sdk.get(`${restTypes[type]}.info`, params); if (result.success) { const room = result[type]; room.rid = room._id; diff --git a/app/lib/methods/loadMessagesForRoom.ts b/app/lib/methods/loadMessagesForRoom.ts index 6eef4fa0c..b72371bfb 100644 --- a/app/lib/methods/loadMessagesForRoom.ts +++ b/app/lib/methods/loadMessagesForRoom.ts @@ -10,7 +10,7 @@ import updateMessages from './updateMessages'; const COUNT = 50; -async function load({ rid: roomId, latest, t }: { rid: string; latest?: string; t: RoomTypes }) { +async function load({ rid: roomId, latest, t }: { rid: string; latest?: Date; t: RoomTypes }) { let params = { roomId, count: COUNT } as { roomId: string; count: number; latest?: string }; if (latest) { params = { ...params, latest: new Date(latest).toISOString() }; @@ -32,9 +32,9 @@ async function load({ rid: roomId, latest, t }: { rid: string; latest?: string; export default function loadMessagesForRoom(args: { rid: string; t: RoomTypes; - latest: string; - loaderItem: TMessageModel; -}): Promise[]> { + latest?: Date; + loaderItem?: TMessageModel; +}): Promise { return new Promise(async (resolve, reject) => { try { const data: Partial[] = await load(args); @@ -52,9 +52,9 @@ export default function loadMessagesForRoom(args: { data.push(loadMoreMessage); } await updateMessages({ rid: args.rid, update: data, loaderItem: args.loaderItem }); - return resolve(data); + return resolve(); } - return resolve([]); + return resolve(); } catch (e) { log(e); reject(e); diff --git a/app/lib/methods/loadMissedMessages.ts b/app/lib/methods/loadMissedMessages.ts index e4578a867..2674e8a19 100644 --- a/app/lib/methods/loadMissedMessages.ts +++ b/app/lib/methods/loadMissedMessages.ts @@ -16,7 +16,7 @@ const getLastUpdate = async (rid: string) => { return null; }; -async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: string }) { +async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: Date }) { let lastUpdate; if (lastOpen) { lastUpdate = new Date(lastOpen).toISOString(); @@ -29,7 +29,7 @@ async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: string } return result; } -export default function loadMissedMessages(args: { rid: string; lastOpen: string }): Promise { +export default function loadMissedMessages(args: { rid: string; lastOpen: Date }): Promise { return new Promise(async (resolve, reject) => { try { const data = await load({ rid: args.rid, lastOpen: args.lastOpen }); diff --git a/app/lib/methods/loadNextMessages.ts b/app/lib/methods/loadNextMessages.ts index 74f91d1f2..45a007d63 100644 --- a/app/lib/methods/loadNextMessages.ts +++ b/app/lib/methods/loadNextMessages.ts @@ -7,19 +7,19 @@ import { getMessageById } from '../database/services/Message'; import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad'; import { generateLoadMoreId } from '../utils'; import updateMessages from './updateMessages'; -import { IMessage, TMessageModel } from '../../definitions'; +import { TMessageModel } from '../../definitions'; import RocketChat from '../rocketchat'; const COUNT = 50; interface ILoadNextMessages { rid: string; - ts: string; - tmid: string; + ts: Date; + tmid?: string; loaderItem: TMessageModel; } -export default function loadNextMessages(args: ILoadNextMessages): Promise { +export default function loadNextMessages(args: ILoadNextMessages): Promise { return new Promise(async (resolve, reject) => { try { const data = await RocketChat.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT); @@ -39,9 +39,9 @@ export default function loadNextMessages(args: ILoadNextMessages): Promise { +export default async function readMessages(rid: string, ls: Date, updateLastOpen = false): Promise { try { const db = database.active; const subscription = await db.get('subscriptions').find(rid); // RC 0.61.0 - await this.sdk.post('subscriptions.read', { rid }); + // @ts-ignore + await sdk.post('subscriptions.read', { rid }); await db.write(async () => { try { diff --git a/app/lib/methods/sendFileMessage.ts b/app/lib/methods/sendFileMessage.ts index cc11606b6..e282f5555 100644 --- a/app/lib/methods/sendFileMessage.ts +++ b/app/lib/methods/sendFileMessage.ts @@ -37,9 +37,9 @@ export async function cancelUpload(item: TUploadModel): Promise { export function sendFileMessage( rid: string, fileInfo: IUpload, - tmid: string, + tmid: string | undefined, server: string, - user: IUser + user: Partial> ): Promise { return new Promise(async (resolve, reject) => { try { diff --git a/app/lib/methods/sendMessage.ts b/app/lib/methods/sendMessage.ts index b4bd9bf9e..5fbbe317f 100644 --- a/app/lib/methods/sendMessage.ts +++ b/app/lib/methods/sendMessage.ts @@ -85,7 +85,13 @@ export async function resendMessage(message: TMessageModel, tmid?: string) { } } -export default async function (rid: string, msg: string, tmid: string, user: IUser, tshow?: boolean) { +export default async function ( + rid: string, + msg: string, + tmid: string | undefined, + user: Partial>, + tshow?: boolean +): Promise { try { const db = database.active; const subsCollection = db.get('subscriptions'); diff --git a/app/lib/methods/updateMessages.ts b/app/lib/methods/updateMessages.ts index 6d4115ae9..cf57e02f4 100644 --- a/app/lib/methods/updateMessages.ts +++ b/app/lib/methods/updateMessages.ts @@ -105,7 +105,9 @@ export default async function updateMessages({ threadCollection.prepareCreate( protectedFunction((t: TThreadModel) => { t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema); - if (t.subscription) t.subscription.id = sub.id; + if (t.subscription) { + t.subscription.id = sub.id; + } Object.assign(t, thread); }) ) diff --git a/app/lib/rocketchat/rocketchat.js b/app/lib/rocketchat/rocketchat.js index b2171eacd..3035af71b 100644 --- a/app/lib/rocketchat/rocketchat.js +++ b/app/lib/rocketchat/rocketchat.js @@ -1032,7 +1032,7 @@ const RocketChat = { } const autoTranslatePermission = reduxStore.getState().permissions['auto-translate']; const userRoles = reduxStore.getState().login?.user?.roles ?? []; - return autoTranslatePermission?.some(role => userRoles.includes(role)); + return autoTranslatePermission?.some(role => userRoles.includes(role)) ?? false; } catch (e) { log(e); return false; diff --git a/app/lib/rocketchat/services/restApi.ts b/app/lib/rocketchat/services/restApi.ts index b24129af5..2f1bf4a5c 100644 --- a/app/lib/rocketchat/services/restApi.ts +++ b/app/lib/rocketchat/services/restApi.ts @@ -247,7 +247,7 @@ export const convertTeamToChannel = ({ teamId, selected }: { teamId: string; sel return sdk.post('teams.convertToChannel', params); }; -export const joinRoom = (roomId: string, joinCode: string, type: 'c' | 'p'): any => { +export const joinRoom = (roomId: string, joinCode: string | null, type: 'c' | 'p'): any => { // TODO: join code // RC 0.48.0 if (type === 'p') { diff --git a/app/reducers/settings.ts b/app/reducers/settings.ts index 028431ed0..498027c06 100644 --- a/app/reducers/settings.ts +++ b/app/reducers/settings.ts @@ -1,7 +1,7 @@ import { IActionSettings } from '../actions/settings'; import { SETTINGS } from '../actions/actionsTypes'; -export type TSettings = string | number | boolean; +export type TSettings = string | number | boolean | string[]; export type ISettings = Record; diff --git a/app/stacks/types.ts b/app/stacks/types.ts index e843ca9a2..95e30aa2d 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -23,17 +23,18 @@ export type ChatsStackParamList = { rid: string; t: SubscriptionType; tmid?: string; - message?: string; + message?: object; // TODO: TMessageModel? name?: string; fname?: string; prid?: string; - room?: ISubscription; + room?: TSubscriptionModel | { rid: string; t: string; name?: string; fname?: string; prid?: string }; jumpToMessageId?: string; jumpToThreadId?: string; - roomUserId?: string; + roomUserId?: string | null; + usedCannedResponse?: string; }; RoomActionsView: { - room: ISubscription; + room?: ISubscription; member: any; rid: string; t: SubscriptionType; @@ -240,7 +241,7 @@ export type InsideStackParamList = { isShareExtension: boolean; serverInfo: IServer; text: string; - room: ISubscription; + room: TSubscriptionModel; thread: any; // TODO: Change }; ModalBlockView: { diff --git a/app/utils/isReadOnly.ts b/app/utils/isReadOnly.ts index 6afc2d962..deee17d83 100644 --- a/app/utils/isReadOnly.ts +++ b/app/utils/isReadOnly.ts @@ -9,21 +9,18 @@ const canPostReadOnly = async ({ rid }: { rid: string }) => { return permission[0]; }; -const isMuted = (room: ISubscription, user: { username: string }) => - room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); +const isMuted = (room: Partial, username: string) => + room && room.muted && room.muted.find && !!room.muted.find(m => m === username); -export const isReadOnly = async ( - room: ISubscription, - user: { id?: string; username: string; token?: string } -): Promise => { +export const isReadOnly = async (room: Partial, username: string): Promise => { if (room.archived) { return true; } - if (isMuted(room, user)) { + if (isMuted(room, username)) { return true; } if (room?.ro) { - const allowPost = await canPostReadOnly(room); + const allowPost = await canPostReadOnly({ rid: room.rid as string }); if (allowPost) { return false; } diff --git a/app/utils/room.ts b/app/utils/room.ts index 4bb8a29b9..bf6cd0804 100644 --- a/app/utils/room.ts +++ b/app/utils/room.ts @@ -3,9 +3,9 @@ import moment from 'moment'; import { themes } from '../constants/colors'; import I18n from '../i18n'; import { IAttachment } from '../definitions/IAttachment'; -import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; +import { SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription'; -export const isBlocked = (room: ISubscription): boolean => { +export const isBlocked = (room: TSubscriptionModel): boolean => { if (room) { const { t, blocked, blocker } = room; if (t === SubscriptionType.DIRECT && (blocked || blocker)) { @@ -62,4 +62,4 @@ export const getBadgeColor = ({ export const makeThreadName = (messageRecord: { id?: string; msg?: string; attachments?: IAttachment[] }): string | undefined => messageRecord.msg || messageRecord?.attachments?.[0]?.title; -export const isTeamRoom = ({ teamId, joined }: { teamId: string; joined: boolean }): boolean => !!teamId && joined; +export const isTeamRoom = ({ teamId, joined }: { teamId?: string; joined?: boolean }): boolean => (!!teamId && joined) ?? false; diff --git a/app/views/DiscussionsView/DiscussionDetails.tsx b/app/views/DiscussionsView/DiscussionDetails.tsx index 45635558f..06d245c81 100644 --- a/app/views/DiscussionsView/DiscussionDetails.tsx +++ b/app/views/DiscussionsView/DiscussionDetails.tsx @@ -37,10 +37,9 @@ interface IDiscussionDetails { const DiscussionDetails = ({ item, date }: IDiscussionDetails): JSX.Element => { const { theme } = useTheme(); - let { dcount } = item; - - if (dcount && dcount >= 1000) { - dcount = '+999'; + let count: string | number | undefined = item.dcount; + if (count && count >= 1000) { + count = '+999'; } return ( @@ -49,7 +48,7 @@ const DiscussionDetails = ({ item, date }: IDiscussionDetails): JSX.Element => { - {dcount} + {count} diff --git a/app/views/DiscussionsView/Item.tsx b/app/views/DiscussionsView/Item.tsx index 578b92be4..171f613b3 100644 --- a/app/views/DiscussionsView/Item.tsx +++ b/app/views/DiscussionsView/Item.tsx @@ -63,6 +63,7 @@ const Item = ({ item, onPress }: IItem): JSX.Element => { if (item?.ts) { messageTime = moment(item.ts).format('LT'); + // @ts-ignore TODO: Unify IMessage messageDate = formatDateThreads(item.ts); } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 0912ccf67..b3b6a8494 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -79,7 +79,7 @@ interface IParams { rid: string; t: SubscriptionType; tmid?: string; - message?: string; + message?: object; name?: string; fname?: string; prid?: string; @@ -202,7 +202,7 @@ class MessagesView extends React.Component { }, noDataMsg: I18n.t('No_files'), testID: 'room-files-view', - renderItem: (item: IMessageItem) => ( + renderItem: (item: any) => ( { }, noDataMsg: I18n.t('No_mentioned_messages'), testID: 'mentioned-messages-view', + // @ts-ignore TODO: unify IMessage renderItem: (item: IMessageItem) => }, // Starred Messages Screen @@ -244,6 +245,7 @@ class MessagesView extends React.Component { noDataMsg: I18n.t('No_starred_messages'), testID: 'starred-messages-view', renderItem: (item: IMessageItem) => ( + // @ts-ignore TODO: unify IMessage this.onLongPress(item)} theme={theme} /> ), action: (message: IMessageItem) => ({ @@ -264,6 +266,7 @@ class MessagesView extends React.Component { noDataMsg: I18n.t('No_pinned_messages'), testID: 'pinned-messages-view', renderItem: (item: IMessageItem) => ( + // @ts-ignore TODO: unify IMessage this.onLongPress(item)} theme={theme} /> ), action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }), diff --git a/app/views/RoomView/Banner.js b/app/views/RoomView/Banner.tsx similarity index 79% rename from app/views/RoomView/Banner.js rename to app/views/RoomView/Banner.tsx index 568e5f26f..e88cfe7e2 100644 --- a/app/views/RoomView/Banner.js +++ b/app/views/RoomView/Banner.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import { Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import { BorderlessButton, ScrollView } from 'react-native-gesture-handler'; import Modal from 'react-native-modal'; @@ -8,10 +7,19 @@ import Markdown, { MarkdownPreview } from '../../containers/markdown'; import { CustomIcon } from '../../lib/Icons'; import { themes } from '../../constants/colors'; import styles from './styles'; +import { useTheme } from '../../theme'; + +interface IBannerProps { + text?: string; + title?: string; + bannerClosed?: boolean; + closeBanner: () => void; +} const Banner = React.memo( - ({ text, title, theme, bannerClosed, closeBanner }) => { + ({ text, title, bannerClosed, closeBanner }: IBannerProps) => { const [showModal, openModal] = useState(false); + const { theme } = useTheme(); const toggleModal = () => openModal(prevState => !prevState); @@ -47,16 +55,7 @@ const Banner = React.memo( return null; }, - (prevProps, nextProps) => - prevProps.text === nextProps.text && prevProps.theme === nextProps.theme && prevProps.bannerClosed === nextProps.bannerClosed + (prevProps, nextProps) => prevProps.text === nextProps.text && prevProps.bannerClosed === nextProps.bannerClosed ); -Banner.propTypes = { - text: PropTypes.string, - title: PropTypes.string, - theme: PropTypes.string, - bannerClosed: PropTypes.bool, - closeBanner: PropTypes.func -}; - export default Banner; diff --git a/app/views/RoomView/EmptyRoom.js b/app/views/RoomView/EmptyRoom.tsx similarity index 61% rename from app/views/RoomView/EmptyRoom.js rename to app/views/RoomView/EmptyRoom.tsx index 1a9a248f2..99ac35e45 100644 --- a/app/views/RoomView/EmptyRoom.js +++ b/app/views/RoomView/EmptyRoom.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { ImageBackground, StyleSheet } from 'react-native'; -import PropTypes from 'prop-types'; + +import { useTheme } from '../../theme'; const styles = StyleSheet.create({ image: { @@ -10,17 +11,12 @@ const styles = StyleSheet.create({ } }); -const EmptyRoom = React.memo(({ length, mounted, theme, rid }) => { +const EmptyRoom = React.memo(({ length, mounted, rid }: { length: number; mounted: boolean; rid: string }) => { + const { theme } = useTheme(); if ((length === 0 && mounted) || !rid) { return ; } return null; }); -EmptyRoom.propTypes = { - length: PropTypes.number.isRequired, - mounted: PropTypes.bool, - theme: PropTypes.string, - rid: PropTypes.string -}; export default EmptyRoom; diff --git a/app/views/RoomView/JoinCode.js b/app/views/RoomView/JoinCode.tsx similarity index 76% rename from app/views/RoomView/JoinCode.js rename to app/views/RoomView/JoinCode.tsx index ceccc1ae7..bdd0d8363 100644 --- a/app/views/RoomView/JoinCode.js +++ b/app/views/RoomView/JoinCode.tsx @@ -1,5 +1,4 @@ import React, { forwardRef, useImperativeHandle, useState } from 'react'; -import PropTypes from 'prop-types'; import { InteractionManager, StyleSheet, Text, View } from 'react-native'; import Modal from 'react-native-modal'; import { connect } from 'react-redux'; @@ -10,6 +9,7 @@ import TextInput from '../../containers/TextInput'; import RocketChat from '../../lib/rocketchat'; import sharedStyles from '../Styles'; import { themes } from '../../constants/colors'; +import { IApplicationState } from '../../definitions'; const styles = StyleSheet.create({ container: { @@ -41,8 +41,16 @@ const styles = StyleSheet.create({ } }); +export interface IJoinCodeProps { + rid: string; + t: string; + onJoin: Function; + isMasterDetail: boolean; + theme: string; +} + const JoinCode = React.memo( - forwardRef(({ rid, t, onJoin, isMasterDetail, theme }, ref) => { + forwardRef(({ rid, t, onJoin, isMasterDetail, theme }: IJoinCodeProps, ref) => { const [visible, setVisible] = useState(false); const [error, setError] = useState(false); const [code, setCode] = useState(''); @@ -53,7 +61,7 @@ const JoinCode = React.memo( const joinRoom = async () => { try { - await RocketChat.joinRoom(rid, code, t); + await RocketChat.joinRoom(rid, code, t as any); onJoin(); hide(); } catch (e) { @@ -64,7 +72,8 @@ const JoinCode = React.memo( useImperativeHandle(ref, () => ({ show })); return ( - + // @ts-ignore TODO: `transparent` seems to exist, but types are incorrect on the lib + InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())} + // TODO: find a way to type this ref + inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())} returnKeyType='send' autoCapitalize='none' onChangeText={setCode} onSubmitEditing={joinRoom} placeholder={I18n.t('Join_Code')} secureTextEntry - error={error && { error: 'error-code-invalid', reason: I18n.t('Code_or_password_invalid') }} + error={error ? { error: 'error-code-invalid', reason: I18n.t('Code_or_password_invalid') } : undefined} testID='join-code-input' /> @@ -111,15 +121,8 @@ const JoinCode = React.memo( ); }) ); -JoinCode.propTypes = { - rid: PropTypes.string, - t: PropTypes.string, - onJoin: PropTypes.func, - isMasterDetail: PropTypes.bool, - theme: PropTypes.string -}; -const mapStateToProps = state => ({ +const mapStateToProps = (state: IApplicationState) => ({ isMasterDetail: state.app.isMasterDetail }); export default connect(mapStateToProps, null, null, { forwardRef: true })(JoinCode); diff --git a/app/views/RoomView/LeftButtons.js b/app/views/RoomView/LeftButtons.js deleted file mode 100644 index aa41b2f36..000000000 --- a/app/views/RoomView/LeftButtons.js +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useCallback } from 'react'; -import PropTypes from 'prop-types'; -import { StyleSheet } from 'react-native'; -import { HeaderBackButton } from '@react-navigation/stack'; - -import { themes } from '../../constants/colors'; -import Avatar from '../../containers/Avatar'; - -const styles = StyleSheet.create({ - avatar: { - borderRadius: 10, - marginHorizontal: 16 - } -}); - -const LeftButtons = React.memo( - ({ tmid, unreadsCount, navigation, baseUrl, userId, token, title, t, theme, goRoomActionsView, isMasterDetail }) => { - if (!isMasterDetail || tmid) { - const onPress = useCallback(() => navigation.goBack()); - const label = unreadsCount > 99 ? '+99' : unreadsCount || ' '; - const labelLength = label.length ? label.length : 1; - const marginLeft = -2 * labelLength; - const fontSize = labelLength > 1 ? 14 : 17; - return ( - - ); - } - const onPress = useCallback(() => goRoomActionsView(), []); - - if (baseUrl && userId && token) { - return ; - } - return null; - } -); - -LeftButtons.propTypes = { - tmid: PropTypes.string, - unreadsCount: PropTypes.number, - navigation: PropTypes.object, - baseUrl: PropTypes.string, - userId: PropTypes.string, - token: PropTypes.string, - title: PropTypes.string, - t: PropTypes.string, - theme: PropTypes.string, - goRoomActionsView: PropTypes.func, - isMasterDetail: PropTypes.bool -}; - -export default LeftButtons; diff --git a/app/views/RoomView/LeftButtons.tsx b/app/views/RoomView/LeftButtons.tsx new file mode 100644 index 000000000..b57409c10 --- /dev/null +++ b/app/views/RoomView/LeftButtons.tsx @@ -0,0 +1,72 @@ +import React, { useCallback } from 'react'; +import { StyleSheet } from 'react-native'; +import { HeaderBackButton, StackNavigationProp } from '@react-navigation/stack'; + +import { themes } from '../../constants/colors'; +import Avatar from '../../containers/Avatar'; +import { ChatsStackParamList } from '../../stacks/types'; + +const styles = StyleSheet.create({ + avatar: { + borderRadius: 10, + marginHorizontal: 16 + } +}); + +interface ILeftButtonsProps { + tmid?: string; + unreadsCount: number | null; + navigation: StackNavigationProp; + baseUrl: string; + userId?: string; + token?: string; + title?: string; + t: string; + theme: string; + goRoomActionsView: Function; + isMasterDetail: boolean; +} + +const LeftButtons = ({ + tmid, + unreadsCount, + navigation, + baseUrl, + userId, + token, + title, + t, + theme, + goRoomActionsView, + isMasterDetail +}: ILeftButtonsProps): React.ReactElement | null => { + if (!isMasterDetail || tmid) { + const onPress = () => navigation.goBack(); + let label = ' '; + let labelLength = 1; + let marginLeft = 0; + let fontSize = 0; + if (unreadsCount) { + label = unreadsCount > 99 ? '+99' : unreadsCount.toString() || ' '; + labelLength = label.length ? label.length : 1; + marginLeft = -2 * labelLength; + fontSize = labelLength > 1 ? 14 : 17; + } + return ( + + ); + } + const onPress = useCallback(() => goRoomActionsView(), []); + + if (baseUrl && userId && token) { + return ; + } + return null; +}; + +export default LeftButtons; diff --git a/app/views/RoomView/List/List.js b/app/views/RoomView/List/List.tsx similarity index 77% rename from app/views/RoomView/List/List.js rename to app/views/RoomView/List/List.tsx index 407dcbf10..f28f2836f 100644 --- a/app/views/RoomView/List/List.js +++ b/app/views/RoomView/List/List.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { FlatList, StyleSheet } from 'react-native'; +import { FlatList, FlatListProps, StyleSheet } from 'react-native'; import Animated from 'react-native-reanimated'; import PropTypes from 'prop-types'; @@ -17,11 +17,15 @@ const styles = StyleSheet.create({ } }); -const List = ({ listRef, ...props }) => ( +export interface IListProps extends FlatListProps { + listRef: any; +} + +const List = ({ listRef, ...props }: IListProps) => ( item.id} + keyExtractor={(item: any) => item.id} contentContainerStyle={styles.contentContainer} style={styles.list} inverted diff --git a/app/views/RoomView/List/NavBottomFAB.js b/app/views/RoomView/List/NavBottomFAB.tsx similarity index 82% rename from app/views/RoomView/List/NavBottomFAB.js rename to app/views/RoomView/List/NavBottomFAB.tsx index e3456c122..0fb3c7f19 100644 --- a/app/views/RoomView/List/NavBottomFAB.js +++ b/app/views/RoomView/List/NavBottomFAB.tsx @@ -1,6 +1,5 @@ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { StyleSheet, View } from 'react-native'; -import PropTypes from 'prop-types'; import Animated, { call, cond, greaterOrEq, useCode } from 'react-native-reanimated'; import { themes } from '../../../constants/colors'; @@ -30,11 +29,19 @@ const styles = StyleSheet.create({ } }); -const NavBottomFAB = ({ y, onPress, isThread }) => { +const NavBottomFAB = ({ + y, + onPress, + isThread +}: { + y: Animated.Value; + onPress: Function; + isThread: boolean; +}): React.ReactElement | null => { const { theme } = useTheme(); const [show, setShow] = useState(false); - const handleOnPress = useCallback(() => onPress()); - const toggle = v => setShow(v); + const handleOnPress = () => onPress(); + const toggle = (v: boolean) => setShow(v); useCode( () => @@ -65,10 +72,4 @@ const NavBottomFAB = ({ y, onPress, isThread }) => { ); }; -NavBottomFAB.propTypes = { - y: Animated.Value, - onPress: PropTypes.func, - isThread: PropTypes.bool -}; - export default NavBottomFAB; diff --git a/app/views/RoomView/List/index.js b/app/views/RoomView/List/index.tsx similarity index 77% rename from app/views/RoomView/List/index.js rename to app/views/RoomView/List/index.tsx index f7dc0372a..6f136748f 100644 --- a/app/views/RoomView/List/index.js +++ b/app/views/RoomView/List/index.tsx @@ -1,26 +1,27 @@ -import React from 'react'; -import { RefreshControl } from 'react-native'; -import PropTypes from 'prop-types'; import { Q } from '@nozbe/watermelondb'; -import moment from 'moment'; import { dequal } from 'dequal'; -import { Value, event } from 'react-native-reanimated'; +import moment from 'moment'; +import React from 'react'; +import { FlatListProps, RefreshControl, ViewToken } from 'react-native'; +import { event, Value } from 'react-native-reanimated'; +import { Observable, Subscription } from 'rxjs'; +import { themes } from '../../../constants/colors'; +import ActivityIndicator from '../../../containers/ActivityIndicator'; +import { TAnyMessageModel, TMessageModel, TThreadMessageModel, TThreadModel } from '../../../definitions'; import database from '../../../lib/database'; import RocketChat from '../../../lib/rocketchat'; +import { compareServerVersion } from '../../../lib/utils'; +import debounce from '../../../utils/debounce'; +import { animateNextTransition } from '../../../utils/layoutAnimation'; import log from '../../../utils/log'; import EmptyRoom from '../EmptyRoom'; -import { animateNextTransition } from '../../../utils/layoutAnimation'; -import ActivityIndicator from '../../../containers/ActivityIndicator'; -import { themes } from '../../../constants/colors'; -import debounce from '../../../utils/debounce'; -import { compareServerVersion } from '../../../lib/utils'; -import List from './List'; +import List, { IListProps } from './List'; import NavBottomFAB from './NavBottomFAB'; const QUERY_SIZE = 50; -const onScroll = ({ y }) => +const onScroll = ({ y }: { y: Value }) => event( [ { @@ -32,44 +33,59 @@ const onScroll = ({ y }) => { useNativeDriver: true } ); -class ListContainer extends React.Component { - static propTypes = { - renderRow: PropTypes.func, - rid: PropTypes.string, - tmid: PropTypes.string, - theme: PropTypes.string, - loading: PropTypes.bool, - listRef: PropTypes.func, - hideSystemMessages: PropTypes.array, - tunread: PropTypes.array, - ignored: PropTypes.array, - navigation: PropTypes.object, - showMessageInMainThread: PropTypes.bool, - serverVersion: PropTypes.string - }; +export { IListProps }; - constructor(props) { +export interface IListContainerProps { + renderRow: Function; + rid: string; + tmid?: string; + theme: string; + loading: boolean; + listRef: React.RefObject; + hideSystemMessages?: string[]; + tunread?: string[]; + ignored?: string[]; + navigation: any; // TODO: type me + showMessageInMainThread: boolean; + serverVersion: string | null; +} + +interface IListContainerState { + messages: TAnyMessageModel[]; + refreshing: boolean; + highlightedMessage: string | null; +} + +class ListContainer extends React.Component { + private count = 0; + private mounted = false; + private animated = false; + private jumping = false; + private y = new Value(0); + private onScroll = onScroll({ y: this.y }); + private unsubscribeFocus: () => void; + private viewabilityConfig = { + itemVisiblePercentThreshold: 10 + }; + private highlightedMessageTimeout: number | undefined | false; + private thread?: TThreadModel; + private messagesObservable?: Observable; + private messagesSubscription?: Subscription; + private viewableItems?: ViewToken[]; + + constructor(props: IListContainerProps) { super(props); console.time(`${this.constructor.name} init`); console.time(`${this.constructor.name} mount`); - this.count = 0; - this.mounted = false; - this.animated = false; - this.jumping = false; this.state = { messages: [], refreshing: false, highlightedMessage: null }; - this.y = new Value(0); - this.onScroll = onScroll({ y: this.y }); this.query(); this.unsubscribeFocus = props.navigation.addListener('focus', () => { this.animated = true; }); - this.viewabilityConfig = { - itemVisiblePercentThreshold: 10 - }; console.timeEnd(`${this.constructor.name} init`); } @@ -78,7 +94,7 @@ class ListContainer extends React.Component { console.timeEnd(`${this.constructor.name} mount`); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IListContainerProps, nextState: IListContainerState) { const { refreshing, highlightedMessage } = this.state; const { hideSystemMessages, theme, tunread, ignored, loading } = this.props; if (theme !== nextProps.theme) { @@ -105,7 +121,7 @@ class ListContainer extends React.Component { return false; } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IListContainerProps) { const { hideSystemMessages } = this.props; if (!dequal(hideSystemMessages, prevProps.hideSystemMessages)) { this.reload(); @@ -114,9 +130,6 @@ class ListContainer extends React.Component { componentWillUnmount() { this.unsubscribeMessages(); - if (this.onEndReached && this.onEndReached.stop) { - this.onEndReached.stop(); - } if (this.unsubscribeFocus) { this.unsubscribeFocus(); } @@ -158,7 +171,7 @@ class ListContainer extends React.Component { Q.experimentalSortBy('ts', Q.desc), Q.experimentalSkip(0), Q.experimentalTake(this.count) - ]; + ] as (Q.WhereDescription | Q.Or)[]; if (!showMessageInMainThread) { whereClause.push(Q.or(Q.where('tmid', null), Q.where('tshow', Q.eq(true)))); } @@ -170,7 +183,7 @@ class ListContainer extends React.Component { if (rid) { this.unsubscribeMessages(); - this.messagesSubscription = this.messagesObservable.subscribe(messages => { + this.messagesSubscription = this.messagesObservable?.subscribe(messages => { if (tmid && this.thread) { messages = [...messages, this.thread]; } @@ -186,6 +199,7 @@ class ListContainer extends React.Component { if (this.mounted) { this.setState({ messages }, () => this.update()); } else { + // @ts-ignore this.state.messages = messages; } // TODO: move it away from here @@ -246,7 +260,7 @@ class ListContainer extends React.Component { } }; - getLastMessage = () => { + getLastMessage = (): TMessageModel | TThreadMessageModel | null => { const { messages } = this.state; if (messages.length > 0) { return messages[0]; @@ -254,21 +268,23 @@ class ListContainer extends React.Component { return null; }; - handleScrollToIndexFailed = params => { + handleScrollToIndexFailed: FlatListProps['onScrollToIndexFailed'] = params => { const { listRef } = this.props; + // @ts-ignore listRef.current.getNode().scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false }); }; - jumpToMessage = messageId => - new Promise(async resolve => { + jumpToMessage = (messageId: string) => + new Promise(async resolve => { this.jumping = true; const { messages } = this.state; const { listRef } = this.props; const index = messages.findIndex(item => item.id === messageId); if (index > -1) { + // @ts-ignore listRef.current.getNode().scrollToIndex({ index, viewPosition: 0.5, viewOffset: 100 }); await new Promise(res => setTimeout(res, 300)); - if (!this.viewableItems.map(vi => vi.key).includes(messageId)) { + if (!this.viewableItems?.map(vi => vi.key).includes(messageId)) { if (!this.jumping) { return resolve(); } @@ -282,6 +298,7 @@ class ListContainer extends React.Component { }, 10000); await setTimeout(() => resolve(), 300); } else { + // @ts-ignore listRef.current.getNode().scrollToIndex({ index: messages.length - 1, animated: false }); if (!this.jumping) { return resolve(); @@ -297,6 +314,7 @@ class ListContainer extends React.Component { jumpToBottom = () => { const { listRef } = this.props; + // @ts-ignore listRef.current.getNode().scrollToOffset({ offset: -100 }); }; @@ -308,13 +326,13 @@ class ListContainer extends React.Component { return null; }; - renderItem = ({ item, index }) => { + renderItem: FlatListProps['renderItem'] = ({ item, index }) => { const { messages, highlightedMessage } = this.state; const { renderRow } = this.props; return renderRow(item, messages[index + 1], highlightedMessage); }; - onViewableItemsChanged = ({ viewableItems }) => { + onViewableItemsChanged: FlatListProps['onViewableItemsChanged'] = ({ viewableItems }) => { this.viewableItems = viewableItems; }; @@ -325,7 +343,7 @@ class ListContainer extends React.Component { const { theme } = this.props; return ( <> - + { +const LoadMore = ({ load, type, runOnRender }: { load: Function; type: string; runOnRender: boolean }): React.ReactElement => { const { theme } = useTheme(); const [loading, setLoading] = useState(false); @@ -62,10 +61,4 @@ const LoadMore = ({ load, type, runOnRender }) => { ); }; -LoadMore.propTypes = { - load: PropTypes.func, - type: PropTypes.string, - runOnRender: PropTypes.bool -}; - export default LoadMore; diff --git a/app/views/RoomView/ReactionPicker.js b/app/views/RoomView/ReactionPicker.tsx similarity index 75% rename from app/views/RoomView/ReactionPicker.js rename to app/views/RoomView/ReactionPicker.tsx index 892b2e43a..b47711b6e 100644 --- a/app/views/RoomView/ReactionPicker.js +++ b/app/views/RoomView/ReactionPicker.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { View } from 'react-native'; import { connect } from 'react-redux'; import Modal from 'react-native-modal'; @@ -9,34 +8,37 @@ import { isAndroid } from '../../utils/deviceInfo'; import { themes } from '../../constants/colors'; import { withTheme } from '../../theme'; import styles from './styles'; +import { IApplicationState } from '../../definitions'; const margin = isAndroid ? 40 : 20; const maxSize = 400; -class ReactionPicker extends React.Component { - static propTypes = { - baseUrl: PropTypes.string.isRequired, - message: PropTypes.object, - show: PropTypes.bool, - isMasterDetail: PropTypes.bool, - reactionClose: PropTypes.func, - onEmojiSelected: PropTypes.func, - width: PropTypes.number, - height: PropTypes.number, - theme: PropTypes.string - }; +interface IReactionPickerProps { + baseUrl: string; + message?: any; + show: boolean; + isMasterDetail: boolean; + reactionClose: () => void; + onEmojiSelected: Function; // TODO: properly type this + width: number; + height: number; + theme: string; +} - shouldComponentUpdate(nextProps) { +class ReactionPicker extends React.Component { + shouldComponentUpdate(nextProps: IReactionPickerProps) { const { show, width, height } = this.props; return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height; } - onEmojiSelected = (emoji, shortname) => { + onEmojiSelected = (emoji: string, shortname: string) => { // standard emojis: `emoji` is unicode and `shortname` is :joy: // custom emojis: only `emoji` is returned with shortname type (:joy:) // to set reactions, we need shortname type const { onEmojiSelected, message } = this.props; - onEmojiSelected(shortname || emoji, message.id); + if (message) { + onEmojiSelected(shortname || emoji, message.id); + } }; render() { @@ -79,7 +81,7 @@ class ReactionPicker extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: IApplicationState) => ({ baseUrl: state.server.server, isMasterDetail: state.app.isMasterDetail }); diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.tsx similarity index 69% rename from app/views/RoomView/RightButtons.js rename to app/views/RoomView/RightButtons.tsx index 9bbf717fd..079e46682 100644 --- a/app/views/RoomView/RightButtons.js +++ b/app/views/RoomView/RightButtons.tsx @@ -1,30 +1,43 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; +import { Observable, Subscription } from 'rxjs'; +import { StackNavigationProp } from '@react-navigation/stack'; import * as HeaderButton from '../../containers/HeaderButton'; import database from '../../lib/database'; import { getUserSelector } from '../../selectors/login'; import { events, logEvent } from '../../utils/log'; import { isTeamRoom } from '../../utils/room'; +import { IApplicationState, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions'; +import { ChatsStackParamList } from '../../stacks/types'; -class RightButtonsContainer extends Component { - static propTypes = { - userId: PropTypes.string, - threadsEnabled: PropTypes.bool, - rid: PropTypes.string, - t: PropTypes.string, - tmid: PropTypes.string, - teamId: PropTypes.string, - navigation: PropTypes.object, - isMasterDetail: PropTypes.bool, - toggleFollowThread: PropTypes.func, - joined: PropTypes.bool, - encrypted: PropTypes.bool - }; +interface IRightButtonsProps { + userId?: string; + threadsEnabled: boolean; + rid?: string; + t: string; + tmid?: string; + teamId?: string; + isMasterDetail: boolean; + toggleFollowThread: Function; + joined: boolean; + encrypted?: boolean; + navigation: StackNavigationProp; +} - constructor(props) { +interface IRigthButtonsState { + isFollowingThread: boolean; + tunread: string[]; + tunreadUser: string[]; + tunreadGroup: string[]; +} + +class RightButtonsContainer extends Component { + private threadSubscription?: Subscription; + private subSubscription?: Subscription; + + constructor(props: IRightButtonsProps) { super(props); this.state = { isFollowingThread: true, @@ -56,7 +69,7 @@ class RightButtonsContainer extends Component { } } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IRightButtonsProps, nextState: IRigthButtonsState) { const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; const { teamId } = this.props; if (nextProps.teamId !== teamId) { @@ -86,37 +99,41 @@ class RightButtonsContainer extends Component { } } - observeThread = threadRecord => { - const threadObservable = threadRecord.observe(); + observeThread = (threadRecord: TMessageModel) => { + const threadObservable: Observable = threadRecord.observe(); this.threadSubscription = threadObservable.subscribe(thread => this.updateThread(thread)); }; - updateThread = thread => { + updateThread = (thread: TMessageModel) => { const { userId } = this.props; this.setState({ - isFollowingThread: thread.replies && !!thread.replies.find(t => t === userId) + isFollowingThread: (thread.replies && !!thread.replies.find(t => t === userId)) ?? false }); }; - observeSubscription = subRecord => { + observeSubscription = (subRecord: TSubscriptionModel) => { const subObservable = subRecord.observe(); this.subSubscription = subObservable.subscribe(sub => { this.updateSubscription(sub); }); }; - updateSubscription = sub => { + updateSubscription = (sub: TSubscriptionModel) => { this.setState({ - tunread: sub?.tunread, - tunreadUser: sub?.tunreadUser, - tunreadGroup: sub?.tunreadGroup + tunread: sub?.tunread ?? [], + tunreadUser: sub?.tunreadUser ?? [], + tunreadGroup: sub?.tunreadGroup ?? [] }); }; goTeamChannels = () => { logEvent(events.ROOM_GO_TEAM_CHANNELS); const { navigation, isMasterDetail, teamId } = this.props; + if (!teamId) { + return; + } if (isMasterDetail) { + // @ts-ignore TODO: find a way to make this work navigation.navigate('ModalStackNavigator', { screen: 'TeamChannelsView', params: { teamId } @@ -129,23 +146,31 @@ class RightButtonsContainer extends Component { goThreadsView = () => { logEvent(events.ROOM_GO_THREADS); const { rid, t, navigation, isMasterDetail } = this.props; + if (!rid) { + return; + } if (isMasterDetail) { + // @ts-ignore TODO: find a way to make this work navigation.navigate('ModalStackNavigator', { screen: 'ThreadMessagesView', params: { rid, t } }); } else { - navigation.navigate('ThreadMessagesView', { rid, t }); + navigation.navigate('ThreadMessagesView', { rid, t: t as SubscriptionType }); } }; goSearchView = () => { logEvent(events.ROOM_GO_SEARCH); const { rid, t, navigation, isMasterDetail, encrypted } = this.props; + if (!rid) { + return; + } if (isMasterDetail) { + // @ts-ignore TODO: find a way to make this work navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true, encrypted } }); } else { - navigation.navigate('SearchMessagesView', { rid, t, encrypted }); + navigation.navigate('SearchMessagesView', { rid, t: t as SubscriptionType, encrypted }); } }; @@ -194,9 +219,9 @@ class RightButtonsContainer extends Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: IApplicationState) => ({ userId: getUserSelector(state).id, - threadsEnabled: state.settings.Threads_enabled, + threadsEnabled: state.settings.Threads_enabled as boolean, isMasterDetail: state.app.isMasterDetail }); diff --git a/app/views/RoomView/Separator.js b/app/views/RoomView/Separator.tsx similarity index 88% rename from app/views/RoomView/Separator.js rename to app/views/RoomView/Separator.tsx index b992f2560..8a125cf7d 100644 --- a/app/views/RoomView/Separator.js +++ b/app/views/RoomView/Separator.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import moment from 'moment'; import I18n from '../../i18n'; import sharedStyles from '../Styles'; import { themes } from '../../constants/colors'; +import { useTheme } from '../../theme'; const styles = StyleSheet.create({ container: { @@ -34,7 +34,8 @@ const styles = StyleSheet.create({ } }); -const DateSeparator = React.memo(({ ts, unread, theme }) => { +const DateSeparator = ({ ts, unread }: { ts: Date | string | null; unread: boolean }): React.ReactElement => { + const { theme } = useTheme(); const date = ts ? moment(ts).format('LL') : null; const unreadLine = { backgroundColor: themes[theme].dangerColor }; const unreadText = { color: themes[theme].dangerColor }; @@ -61,12 +62,6 @@ const DateSeparator = React.memo(({ ts, unread, theme }) => { ); -}); - -DateSeparator.propTypes = { - ts: PropTypes.instanceOf(Date), - unread: PropTypes.bool, - theme: PropTypes.string }; export default DateSeparator; diff --git a/app/views/RoomView/UploadProgress.js b/app/views/RoomView/UploadProgress.tsx similarity index 69% rename from app/views/RoomView/UploadProgress.js rename to app/views/RoomView/UploadProgress.tsx index 1b61c0ac1..fba6e9358 100644 --- a/app/views/RoomView/UploadProgress.js +++ b/app/views/RoomView/UploadProgress.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import PropTypes from 'prop-types'; import { Q } from '@nozbe/watermelondb'; +import { Observable, Subscription } from 'rxjs'; import database from '../../lib/database'; import RocketChat from '../../lib/rocketchat'; @@ -11,6 +11,7 @@ import { CustomIcon } from '../../lib/Icons'; import { themes } from '../../constants/colors'; import sharedStyles from '../Styles'; import { withTheme } from '../../theme'; +import { IUser, TUploadModel } from '../../definitions'; const styles = StyleSheet.create({ container: { @@ -51,23 +52,26 @@ const styles = StyleSheet.create({ } }); -class UploadProgress extends Component { - static propTypes = { - width: PropTypes.number, - rid: PropTypes.string, - theme: PropTypes.string, - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - token: PropTypes.string.isRequired - }), - baseUrl: PropTypes.string.isRequired - }; +interface IUploadProgressProps { + width: number; + rid: string; + user: Pick; + baseUrl: string; + theme?: string; +} - constructor(props) { +interface IUploadProgressState { + uploads: TUploadModel[]; +} + +class UploadProgress extends Component { + private mounted = false; + private ranInitialUploadCheck = false; + private uploadsObservable?: Observable; + private uploadsSubscription?: Subscription; + + constructor(props: IUploadProgressProps) { super(props); - this.mounted = false; - this.ranInitialUploadCheck = false; this.state = { uploads: [] }; @@ -97,6 +101,7 @@ class UploadProgress extends Component { if (this.mounted) { this.setState({ uploads }); } else { + // @ts-ignore this.state.uploads = uploads; } if (!this.ranInitialUploadCheck) { @@ -112,7 +117,7 @@ class UploadProgress extends Component { if (!RocketChat.isUploadActive(u.path)) { try { const db = database.active; - await db.action(async () => { + await db.write(async () => { await u.update(() => { u.error = true; }); @@ -124,10 +129,10 @@ class UploadProgress extends Component { }); }; - deleteUpload = async item => { + deleteUpload = async (item: TUploadModel) => { try { const db = database.active; - await db.action(async () => { + await db.write(async () => { await item.destroyPermanently(); }); } catch (e) { @@ -135,7 +140,7 @@ class UploadProgress extends Component { } }; - cancelUpload = async item => { + cancelUpload = async (item: TUploadModel) => { try { await RocketChat.cancelUpload(item); } catch (e) { @@ -143,12 +148,12 @@ class UploadProgress extends Component { } }; - tryAgain = async item => { + tryAgain = async (item: TUploadModel) => { const { rid, baseUrl: server, user } = this.props; try { const db = database.active; - await db.action(async () => { + await db.write(async () => { await item.update(() => { item.error = false; }); @@ -159,44 +164,44 @@ class UploadProgress extends Component { } }; - renderItemContent = item => { + renderItemContent = (item: TUploadModel) => { const { width, theme } = this.props; if (!item.error) { return [ - + {I18n.t('Uploading')} {item.name} - this.cancelUpload(item)} /> + this.cancelUpload(item)} /> , ]; } return ( - + - + {I18n.t('Error_uploading')} {item.name} this.tryAgain(item)}> - {I18n.t('Try_again')} + {I18n.t('Try_again')} - this.deleteUpload(item)} /> + this.deleteUpload(item)} /> ); }; // TODO: transform into stateless and update based on its own observable changes - renderItem = (item, index) => { + renderItem = (item: TUploadModel, index: number) => { const { theme } = this.props; return ( @@ -206,8 +211,8 @@ class UploadProgress extends Component { styles.item, index !== 0 ? { marginTop: 10 } : {}, { - backgroundColor: themes[theme].chatComponentBackground, - borderColor: themes[theme].borderColor + backgroundColor: themes[theme!].chatComponentBackground, + borderColor: themes[theme!].borderColor } ]}> {this.renderItemContent(item)} diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.tsx similarity index 69% rename from app/views/RoomView/index.js rename to app/views/RoomView/index.tsx index 2517cc175..da60d62e7 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { InteractionManager, Text, View } from 'react-native'; import { connect } from 'react-redux'; import parse from 'url-parse'; @@ -7,16 +6,18 @@ import moment from 'moment'; import * as Haptics from 'expo-haptics'; import { Q } from '@nozbe/watermelondb'; import { dequal } from 'dequal'; -import { withSafeAreaInsets } from 'react-native-safe-area-context'; +import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; +import { Subscription } from 'rxjs'; +import { IReduxEmoji } from '../../definitions/IEmoji'; import Touch from '../../utils/touch'; -import { replyBroadcast as replyBroadcastAction } from '../../actions/messages'; +import { replyBroadcast } from '../../actions/messages'; import database from '../../lib/database'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; -import MessageActions from '../../containers/MessageActions'; +import MessageActions, { IMessageActions } from '../../containers/MessageActions'; import MessageErrorActions from '../../containers/MessageErrorActions'; -import MessageBox from '../../containers/MessageBox'; +import MessageBox, { IMessageBoxProps } from '../../containers/MessageBox'; import log, { events, logEvent } from '../../utils/log'; import EventEmitter from '../../utils/events'; import I18n from '../../i18n'; @@ -37,7 +38,8 @@ import { handleCommandReplyLatest, handleCommandRoomActions, handleCommandScroll, - handleCommandSearchMessages + handleCommandSearchMessages, + IKeyCommandEvent } from '../../commands'; import { Review } from '../../utils/review'; import RoomClass from '../../lib/methods/subscriptions/room'; @@ -60,10 +62,25 @@ import Separator from './Separator'; import RightButtons from './RightButtons'; import LeftButtons from './LeftButtons'; import styles from './styles'; -import JoinCode from './JoinCode'; +import JoinCode, { IJoinCodeProps } from './JoinCode'; import UploadProgress from './UploadProgress'; import ReactionPicker from './ReactionPicker'; -import List from './List'; +import List, { IListContainerProps, IListProps } from './List'; +import { ChatsStackParamList } from '../../stacks/types'; +import { + IApplicationState, + IAttachment, + IBaseScreen, + ILoggedUser, + IMessage, + ISubscription, + IVisitor, + SubscriptionType, + TAnyMessageModel, + TSubscriptionModel, + TThreadModel +} from '../../definitions'; +import { ICustomEmojis } from '../../reducers/customEmojis'; const stateAttrsUpdate = [ 'joined', @@ -100,50 +117,95 @@ const roomAttrsUpdate = [ 'joinCodeRequired', 'teamMain', 'teamId' -]; +] as const; -class RoomView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - user: PropTypes.shape({ - id: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - token: PropTypes.string.isRequired, - showMessageInMainThread: PropTypes.bool - }), - appState: PropTypes.string, - useRealName: PropTypes.bool, - isAuthenticated: PropTypes.bool, - Message_GroupingPeriod: PropTypes.number, - Message_TimeFormat: PropTypes.string, - Message_Read_Receipt_Enabled: PropTypes.bool, - Hide_System_Messages: PropTypes.array, - baseUrl: PropTypes.string, - serverVersion: PropTypes.string, - customEmojis: PropTypes.object, - isMasterDetail: PropTypes.bool, - theme: PropTypes.string, - replyBroadcast: PropTypes.func, - width: PropTypes.number, - height: PropTypes.number, - insets: PropTypes.object +interface IRoomViewProps extends IBaseScreen { + user: Pick; + appState: string; + useRealName?: boolean; + isAuthenticated: boolean; + Message_GroupingPeriod?: number; + Message_TimeFormat?: string; + Message_Read_Receipt_Enabled?: boolean; + Hide_System_Messages?: string[]; + baseUrl: string; + serverVersion: string | null; + customEmojis: ICustomEmojis; + isMasterDetail: boolean; + replyBroadcast: Function; + width: number; + height: number; + insets: EdgeInsets; +} + +type TRoomUpdate = typeof roomAttrsUpdate[number]; + +interface IRoomViewState { + [key: string]: any; + joined: boolean; + room: TSubscriptionModel | { rid: string; t: string; name?: string; fname?: string; prid?: string; joinCodeRequired?: boolean }; + roomUpdate: { + [K in TRoomUpdate]?: any; // TODO: get type from TSubscriptionModel }; + member: any; + lastOpen: Date | null; + reactionsModalVisible: boolean; + selectedMessage?: Object; + canAutoTranslate: boolean; + loading: boolean; + showingBlockingLoader: boolean; + editing: boolean; + replying: boolean; + replyWithMention: boolean; + reacting: boolean; + readOnly: boolean; + unreadsCount: number | null; + roomUserId?: string | null; +} - constructor(props) { +class RoomView extends React.Component { + private rid?: string; + private t?: string; + private tmid?: string; + private jumpToMessageId?: string; + private jumpToThreadId?: string; + // TODO: review these refs + private messagebox: React.RefObject; + private list: React.RefObject; + private joinCode: React.RefObject; + private flatList: React.RefObject; + private mounted: boolean; + private sub?: any; + private offset = 0; + private didMountInteraction: any; + private subSubscription?: Subscription; + private queryUnreads?: Subscription; + private retryInit = 0; + private retryInitTimeout?: number; + private retryFindCount = 0; + private retryFindTimeout?: number; + private messageErrorActions?: React.RefObject; // TODO: type me + private messageActions?: React.RefObject; + + constructor(props: IRoomViewProps) { super(props); console.time(`${this.constructor.name} init`); console.time(`${this.constructor.name} mount`); this.rid = props.route.params?.rid; this.t = props.route.params?.t; + /** + * On threads, we don't have a subscription. + * `this.state.room` is going to have only a few properties sent during navigation. + * Use `this.tmid` as thread id. + */ this.tmid = props.route.params?.tmid; const selectedMessage = props.route.params?.message; const name = props.route.params?.name; const fname = props.route.params?.fname; const prid = props.route.params?.prid; const room = props.route.params?.room ?? { - rid: this.rid, - t: this.t, + rid: this.rid as string, + t: this.t as string, name, fname, prid @@ -158,7 +220,7 @@ class RoomView extends React.Component { member: {}, lastOpen: null, reactionsModalVisible: false, - selectedMessage: selectedMessage || {}, + selectedMessage, canAutoTranslate: false, loading: true, showingBlockingLoader: false, @@ -172,7 +234,8 @@ class RoomView extends React.Component { }; this.setHeader(); - if (room && room.observe) { + if ('id' in room) { + // @ts-ignore TODO: type guard isn't helping here :( this.observeRoom(room); } else if (this.rid) { this.findAndObserveRoom(this.rid); @@ -195,7 +258,6 @@ class RoomView extends React.Component { componentDidMount() { this.mounted = true; - this.offset = 0; this.didMountInteraction = InteractionManager.runAfterInteractions(() => { const { isAuthenticated } = this.props; this.setHeader(); @@ -224,7 +286,7 @@ class RoomView extends React.Component { console.timeEnd(`${this.constructor.name} mount`); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: IRoomViewProps, nextState: IRoomViewState) { const { state } = this; const { roomUpdate, member } = state; const { appState, theme, insets, route } = this.props; @@ -250,21 +312,22 @@ class RoomView extends React.Component { return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key])); } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: IRoomViewProps, prevState: IRoomViewState) { const { roomUpdate } = this.state; const { appState, insets, route } = this.props; - if (route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) { + if (route?.params?.jumpToMessageId && route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) { this.jumpToMessage(route?.params?.jumpToMessageId); } - if (route?.params?.jumpToThreadId !== prevProps.route?.params?.jumpToThreadId) { + if (route?.params?.jumpToThreadId && route?.params?.jumpToThreadId !== prevProps.route?.params?.jumpToThreadId) { this.navToThread({ tmid: route?.params?.jumpToThreadId }); } if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { // Fire List.query() just to keep observables working if (this.list && this.list.current) { + // @ts-ignore TODO: is this working? this.list.current?.query?.(); } } @@ -303,8 +366,9 @@ class RoomView extends React.Component { const db = database.active; this.mounted = false; if (!editing && this.messagebox && this.messagebox.current) { + // @ts-ignore const { text } = this.messagebox.current; - let obj; + let obj: TSubscriptionModel | TThreadModel | null = null; if (this.tmid) { try { const threadsCollection = db.get('threads'); @@ -313,12 +377,13 @@ class RoomView extends React.Component { // Do nothing } } else { - obj = room; + obj = room as TSubscriptionModel; } if (obj) { try { - await db.action(async () => { - await obj.update(r => { + await db.write(async () => { + // FIXME: why do I need to tell ts this is non null if we have that if condition above? + await obj!.update(r => { r.draftMessage = text; }); }); @@ -331,15 +396,18 @@ class RoomView extends React.Component { if (this.didMountInteraction && this.didMountInteraction.cancel) { this.didMountInteraction.cancel(); } - if (this.willBlurListener && this.willBlurListener.remove) { - this.willBlurListener.remove(); - } if (this.subSubscription && this.subSubscription.unsubscribe) { this.subSubscription.unsubscribe(); } if (this.queryUnreads && this.queryUnreads.unsubscribe) { this.queryUnreads.unsubscribe(); } + if (this.retryInitTimeout) { + clearTimeout(this.retryInitTimeout); + } + if (this.retryFindTimeout) { + clearTimeout(this.retryFindTimeout); + } EventEmitter.removeListener('connected', this.handleConnected); if (isTablet) { EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); @@ -357,32 +425,45 @@ class RoomView extends React.Component { const { room, unreadsCount, roomUserId, joined } = this.state; const { navigation, isMasterDetail, theme, baseUrl, user, insets, route } = this.props; const { rid, tmid } = this; + if (!room.rid) { + return; + } + const prid = room?.prid; - const isGroupChat = RocketChat.isGroupChat(room); + const isGroupChat = RocketChat.isGroupChat(room as ISubscription); let title = route.params?.name; - let parentTitle; - if ((room.id || room.rid) && !tmid) { + let parentTitle = ''; + // TODO: I think it's safe to remove this, but we need to test tablet without rooms + if (!tmid) { title = RocketChat.getRoomTitle(room); } if (tmid) { parentTitle = RocketChat.getRoomTitle(room); } - const subtitle = room?.topic; - const t = room?.t; - const teamMain = room?.teamMain; - const teamId = room?.teamId; - const encrypted = room?.encrypted; - const { id: userId, token } = user; - const avatar = room?.name; - const visitor = room?.visitor; - if (!room?.rid) { - return; + let subtitle: string | undefined; + let t: string; + let teamMain: boolean | undefined; + let teamId: string | undefined; + let encrypted: boolean | undefined; + let userId: string | undefined; + let token: string | undefined; + let avatar: string | undefined; + let visitor: IVisitor | undefined; + if ('id' in room) { + subtitle = room.topic; + t = room.t; + teamMain = room.teamMain; + teamId = room.teamId; + encrypted = room.encrypted; + ({ id: userId, token } = user); + avatar = room.name; + visitor = room.visitor; } let numIconsRight = 2; if (tmid) { numIconsRight = 1; - } else if (isTeamRoom({ teamId, joined })) { + } else if (teamId && isTeamRoom({ teamId, joined })) { numIconsRight = 3; } const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight }); @@ -431,7 +512,6 @@ class RoomView extends React.Component { rid={rid} tmid={tmid} teamId={teamId} - teamMain={teamMain} joined={joined} t={t} encrypted={encrypted} @@ -442,27 +522,30 @@ class RoomView extends React.Component { }); }; - goRoomActionsView = screen => { + goRoomActionsView = (screen?: string) => { logEvent(events.ROOM_GO_RA); const { room, member, joined } = this.state; const { navigation, isMasterDetail } = this.props; if (isMasterDetail) { + // @ts-ignore TODO: find a way to make it work navigation.navigate('ModalStackNavigator', { + // @ts-ignore screen: screen ?? 'RoomActionsView', params: { - rid: this.rid, - t: this.t, + rid: this.rid as string, + t: this.t as SubscriptionType, + // @ts-ignore room, member, showCloseModal: !!screen, joined } }); - } else { + } else if (this.rid && this.t) { navigation.push('RoomActionsView', { rid: this.rid, - t: this.t, - room, + t: this.t as SubscriptionType, + room: room as ISubscription, member, joined }); @@ -472,7 +555,7 @@ class RoomView extends React.Component { setReadOnly = async () => { const { room } = this.state; const { user } = this.props; - const readOnly = await isReadOnly(room, user); + const readOnly = await isReadOnly(room as ISubscription, user.username as string); this.setState({ readOnly }); }; @@ -480,6 +563,9 @@ class RoomView extends React.Component { try { this.setState({ loading: true }); const { room, joined } = this.state; + if (!this.rid) { + return; + } if (this.tmid) { await RoomServices.getThreadMessages(this.tmid, this.rid); } else { @@ -487,13 +573,13 @@ class RoomView extends React.Component { await RoomServices.getMessages(room); // if room is joined - if (joined) { + if (joined && 'id' in room) { if (room.alert || room.unread || room.userMentions) { this.setLastOpen(room.ls); } else { this.setLastOpen(null); } - RoomServices.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e)); + RoomServices.readMessages(room.rid, newLastOpen).catch(e => console.log(e)); } } @@ -503,7 +589,7 @@ class RoomView extends React.Component { this.setState({ canAutoTranslate, member, loading: false }); } catch (e) { this.setState({ loading: false }); - this.retryInit = this.retryInit + 1 || 1; + this.retryInit += 1; if (this.retryInit <= 1) { this.retryInitTimeout = setTimeout(() => { this.init(); @@ -516,7 +602,7 @@ class RoomView extends React.Component { const { room } = this.state; const { t } = room; - if (t === 'd' && !RocketChat.isGroupChat(room)) { + if ('id' in room && t === 'd' && !RocketChat.isGroupChat(room)) { try { const roomUserId = RocketChat.getUidDirectMessage(room); this.setState({ roomUserId }, () => this.setHeader()); @@ -533,7 +619,7 @@ class RoomView extends React.Component { return {}; }; - findAndObserveRoom = async rid => { + findAndObserveRoom = async (rid: string) => { try { const db = database.active; const subCollection = await db.get('subscriptions'); @@ -569,30 +655,34 @@ class RoomView extends React.Component { delete this.sub; }; - observeRoom = room => { + observeRoom = (room: TSubscriptionModel) => { const observable = room.observe(); this.subSubscription = observable.subscribe(changes => { - const roomUpdate = roomAttrsUpdate.reduce((ret, attr) => { + const roomUpdate = roomAttrsUpdate.reduce((ret: any, attr) => { ret[attr] = changes[attr]; return ret; }, {}); if (this.mounted) { this.internalSetState({ room: changes, roomUpdate }); } else { + // @ts-ignore this.state.room = changes; + // @ts-ignore this.state.roomUpdate = roomUpdate; } }); }; - errorActionsShow = message => { + errorActionsShow = (message: TAnyMessageModel) => { + // @ts-ignore this.messageErrorActions?.showMessageErrorActions(message); }; - onEditInit = message => { + onEditInit = (message: TAnyMessageModel) => { const newMessage = { id: message.id, subscription: { + // @ts-ignore TODO: we can remove this after we merge a PR separating IMessage vs IMessageFromServer id: message.subscription.id }, msg: message?.attachments?.[0]?.description || message.msg @@ -601,11 +691,11 @@ class RoomView extends React.Component { }; onEditCancel = () => { - this.setState({ selectedMessage: {}, editing: false }); + this.setState({ selectedMessage: undefined, editing: false }); }; - onEditRequest = async message => { - this.setState({ selectedMessage: {}, editing: false }); + onEditRequest = async (message: TAnyMessageModel) => { + this.setState({ selectedMessage: undefined, editing: false }); try { await RocketChat.editMessage(message); } catch (e) { @@ -613,7 +703,7 @@ class RoomView extends React.Component { } }; - onReplyInit = (message, mention) => { + onReplyInit = (message: TAnyMessageModel, mention: boolean) => { this.setState({ selectedMessage: message, replying: true, @@ -622,27 +712,29 @@ class RoomView extends React.Component { }; onReplyCancel = () => { - this.setState({ selectedMessage: {}, replying: false, replyWithMention: false }); + this.setState({ selectedMessage: undefined, replying: false, replyWithMention: false }); }; - onReactionInit = message => { + onReactionInit = (message: TAnyMessageModel) => { this.setState({ selectedMessage: message, reacting: true }); }; onReactionClose = () => { - this.setState({ selectedMessage: {}, reacting: false }); + this.setState({ selectedMessage: undefined, reacting: false }); }; - onMessageLongPress = message => { + onMessageLongPress = (message: TAnyMessageModel) => { + // @ts-ignore this.messageActions?.showMessageActions(message); }; - showAttachment = attachment => { + showAttachment = (attachment: IAttachment) => { const { navigation } = this.props; + // @ts-ignore navigation.navigate('AttachmentView', { attachment }); }; - onReactionPress = async (shortname, messageId) => { + onReactionPress = async (shortname: string, messageId: string) => { try { await RocketChat.setReaction(shortname, messageId); this.onReactionClose(); @@ -652,13 +744,13 @@ class RoomView extends React.Component { } }; - onReactionLongPress = message => { + onReactionLongPress = (message: TAnyMessageModel) => { this.setState({ selectedMessage: message, reactionsModalVisible: true }); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); }; onCloseReactionsModal = () => { - this.setState({ selectedMessage: {}, reactionsModalVisible: false }); + this.setState({ selectedMessage: undefined, reactionsModalVisible: false }); }; onEncryptedPress = () => { @@ -668,19 +760,21 @@ class RoomView extends React.Component { const screen = { screen: 'E2EHowItWorksView', params: { showCloseModal: true } }; if (isMasterDetail) { + // @ts-ignore return navigation.navigate('ModalStackNavigator', screen); } + // @ts-ignore navigation.navigate('E2ESaveYourPasswordStackNavigator', screen); }; onDiscussionPress = debounce( - item => { + (item: TAnyMessageModel) => { const { navigation } = this.props; navigation.push('RoomView', { - rid: item.drid, + rid: item.drid as string, prid: item.rid, name: item.msg, - t: 'p' + t: 'p' as SubscriptionType }); }, 1000, @@ -689,6 +783,9 @@ class RoomView extends React.Component { // eslint-disable-next-line react/sort-comp updateUnreadCount = async () => { + if (!this.rid) { + return; + } const db = database.active; const observable = await db .get('subscriptions') @@ -704,9 +801,9 @@ class RoomView extends React.Component { }); }; - onThreadPress = debounce(item => this.navToThread(item), 1000, true); + onThreadPress = debounce((item: TAnyMessageModel) => this.navToThread(item), 1000, true); - shouldNavigateToRoom = message => { + shouldNavigateToRoom = (message: IMessage) => { if (message.tmid && message.tmid === this.tmid) { return false; } @@ -716,7 +813,7 @@ class RoomView extends React.Component { return true; }; - jumpToMessageByUrl = async messageUrl => { + jumpToMessageByUrl = async (messageUrl?: string) => { if (!messageUrl) { return; } @@ -724,7 +821,9 @@ class RoomView extends React.Component { this.setState({ showingBlockingLoader: true }); const parsedUrl = parse(messageUrl, true); const messageId = parsedUrl.query.msg; - await this.jumpToMessage(messageId); + if (messageId) { + await this.jumpToMessage(messageId); + } this.setState({ showingBlockingLoader: false }); } catch (e) { this.setState({ showingBlockingLoader: false }); @@ -732,7 +831,7 @@ class RoomView extends React.Component { } }; - jumpToMessage = async messageId => { + jumpToMessage = async (messageId: string) => { try { this.setState({ showingBlockingLoader: true }); const message = await RoomServices.getMessageInfo(messageId); @@ -755,7 +854,9 @@ class RoomView extends React.Component { if (message.fromServer && !message.tmid) { await RocketChat.loadSurroundingMessages({ messageId, rid: this.rid }); } + // @ts-ignore await Promise.race([this.list.current.jumpToMessage(message.id), new Promise(res => setTimeout(res, 5000))]); + // @ts-ignore this.list.current.cancelJumpToMessage(); } } catch (e) { @@ -765,9 +866,9 @@ class RoomView extends React.Component { } }; - replyBroadcast = message => { - const { replyBroadcast } = this.props; - replyBroadcast(message); + replyBroadcast = (message: Record) => { + const { dispatch } = this.props; + dispatch(replyBroadcast(message)); }; handleConnected = () => { @@ -775,7 +876,7 @@ class RoomView extends React.Component { EventEmitter.removeListener('connected', this.handleConnected); }; - handleRoomRemoved = ({ rid }) => { + handleRoomRemoved = ({ rid }: { rid: string }) => { const { room } = this.state; if (rid === this.rid) { Navigation.navigate('RoomsListView'); @@ -784,18 +885,21 @@ class RoomView extends React.Component { } }; - internalSetState = (...args) => { + internalSetState = (...args: any[]) => { if (!this.mounted) { return; } + // @ts-ignore TODO: TS is complaining about this, but I don't feel like changing rn since it should be working this.setState(...args); }; - sendMessage = (message, tmid, tshow) => { + sendMessage = (message: string, tmid?: string, tshow?: boolean) => { logEvent(events.ROOM_SEND_MESSAGE); + const { rid } = this.state.room; const { user } = this.props; - RocketChat.sendMessage(this.rid, message, this.tmid || tmid, user, tshow).then(() => { + RocketChat.sendMessage(rid, message, this.tmid || tmid, user, tshow).then(() => { if (this.list && this.list.current) { + // @ts-ignore this.list.current.update(); } this.setLastOpen(null); @@ -803,7 +907,8 @@ class RoomView extends React.Component { }); }; - getCustomEmoji = name => { + // TODO: We need to unify + getCustomEmoji = (name: string): IReduxEmoji | null => { const { customEmojis } = this.props; const emoji = customEmojis[name]; if (emoji) { @@ -812,7 +917,7 @@ class RoomView extends React.Component { return null; }; - setLastOpen = lastOpen => this.setState({ lastOpen }); + setLastOpen = (lastOpen: Date | null) => this.setState({ lastOpen }); onJoin = () => { this.internalSetState({ @@ -826,14 +931,17 @@ class RoomView extends React.Component { const { room } = this.state; if (this.isOmnichannel) { - await takeInquiry(room._id); + if ('_id' in room) { + await takeInquiry(room._id); + } this.onJoin(); } else { - const { joinCodeRequired } = room; + const { joinCodeRequired, rid } = room; if (joinCodeRequired) { + // @ts-ignore this.joinCode.current?.show(); } else { - await RocketChat.joinRoom(this.rid, null, this.t); + await RocketChat.joinRoom(rid, null, this.t as any); this.onJoin(); } } @@ -842,24 +950,31 @@ class RoomView extends React.Component { } }; - getThreadName = (tmid, messageId) => getThreadName(this.rid, tmid, messageId); + getThreadName = (tmid: string, messageId: string) => { + const { rid } = this.state.room; + return getThreadName(rid, tmid, messageId); + }; - toggleFollowThread = async (isFollowingThread, tmid) => { + toggleFollowThread = async (isFollowingThread: boolean, tmid?: string) => { try { - await RocketChat.toggleFollowMessage(tmid ?? this.tmid, !isFollowingThread); + const threadMessageId = tmid ?? this.tmid; + if (!threadMessageId) { + return; + } + await RocketChat.toggleFollowMessage(threadMessageId, !isFollowingThread); EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') }); } catch (e) { log(e); } }; - getBadgeColor = messageId => { + getBadgeColor = (messageId: string) => { const { room } = this.state; const { theme } = this.props; return getBadgeColor({ subscription: room, theme, messageId }); }; - navToRoomInfo = navParam => { + navToRoomInfo = (navParam: any) => { const { navigation, user, isMasterDetail } = this.props; logEvent(events[`ROOM_GO_${navParam.t === 'd' ? 'USER' : 'ROOM'}_INFO`]); if (navParam.rid === user.id) { @@ -867,55 +982,65 @@ class RoomView extends React.Component { } if (isMasterDetail) { navParam.showCloseModal = true; + // @ts-ignore navigation.navigate('ModalStackNavigator', { screen: 'RoomInfoView', params: navParam }); } else { navigation.navigate('RoomInfoView', navParam); } }; - navToThread = async item => { + navToThread = async (item: TAnyMessageModel | { tmid: string }) => { const { roomUserId } = this.state; const { navigation } = this.props; + if (!this.rid) { + return; + } + if (item.tmid) { - let name = item.tmsg; + let name = ''; + let jumpToMessageId = ''; + if ('id' in item) { + name = item.tmsg ?? ''; + jumpToMessageId = item.id; + } if (!name) { - const result = await this.getThreadName(item.tmid, item.id); + const result = await this.getThreadName(item.tmid, jumpToMessageId); // test if there isn't a thread if (!result) { return; } name = result; } - if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) { + if ('id' in item && item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) { name = I18n.t('Encrypted_message'); } return navigation.push('RoomView', { rid: this.rid, tmid: item.tmid, name, - t: 'thread', + t: SubscriptionType.THREAD, roomUserId, - jumpToMessageId: item.id + jumpToMessageId }); } - if (item.tlm) { + if ('tlm' in item) { return navigation.push('RoomView', { rid: this.rid, tmid: item.id, name: makeThreadName(item), - t: 'thread', + t: SubscriptionType.THREAD, roomUserId }); } }; - navToRoom = async message => { + navToRoom = async (message: TAnyMessageModel) => { const { navigation, isMasterDetail } = this.props; const roomInfo = await getRoomInfo(message.rid); return goRoom({ - item: roomInfo, + item: roomInfo as Partial, isMasterDetail, navigationMethod: navigation.push, jumpToMessageId: message.id @@ -924,20 +1049,23 @@ class RoomView extends React.Component { callJitsi = () => { const { room } = this.state; - const { jitsiTimeout } = room; - if (jitsiTimeout < Date.now()) { - showErrorAlert(I18n.t('Call_already_ended')); - } else { - RocketChat.callJitsi(room); + if ('id' in room) { + const { jitsiTimeout } = room; + if (jitsiTimeout && jitsiTimeout < Date.now()) { + showErrorAlert(I18n.t('Call_already_ended')); + } else { + RocketChat.callJitsi(room); + } } }; - handleCommands = ({ event }) => { + handleCommands = ({ event }: { event: IKeyCommandEvent }) => { if (this.rid) { const { input } = event; if (handleCommandScroll(event)) { const offset = input === 'UIKeyInputUpArrow' ? 100 : -100; this.offset += offset; + // @ts-ignore this.flatList?.scrollToOffset({ offset: this.offset }); } else if (handleCommandRoomActions(event)) { this.goRoomActionsView(); @@ -945,6 +1073,7 @@ class RoomView extends React.Component { this.goRoomActionsView('SearchMessagesView'); } else if (handleCommandReplyLatest(event)) { if (this.list && this.list.current) { + // @ts-ignore const message = this.list.current.getLastMessage(); this.onReplyInit(message, false); } @@ -952,7 +1081,21 @@ class RoomView extends React.Component { } }; - blockAction = ({ actionId, appId, value, blockId, rid, mid }) => + blockAction = ({ + actionId, + appId, + value, + blockId, + rid, + mid + }: { + actionId: string; + appId: string; + value: any; + blockId: string; + rid: string; + mid: string; + }) => RocketChat.triggerBlockAction({ blockId, actionId, @@ -968,35 +1111,41 @@ class RoomView extends React.Component { closeBanner = async () => { const { room } = this.state; - try { - const db = database.active; - await db.action(async () => { - await room.update(r => { - r.bannerClosed = true; + if ('id' in room) { + try { + const db = database.active; + await db.write(async () => { + await room.update(r => { + r.bannerClosed = true; + }); }); - }); - } catch { - // do nothing + } catch { + // do nothing + } } }; - isIgnored = message => { + isIgnored = (message: TAnyMessageModel): boolean => { const { room } = this.state; - return room?.ignored?.includes?.(message?.u?._id) ?? false; + if ('id' in room) { + return room?.ignored?.includes?.(message?.u?._id) ?? false; + } + return false; }; - onLoadMoreMessages = loaderItem => - RoomServices.getMoreMessages({ - rid: this.rid, + onLoadMoreMessages = (loaderItem: TAnyMessageModel) => { + const { room } = this.state; + return RoomServices.getMoreMessages({ + rid: room.rid, tmid: this.tmid, - t: this.t, + t: room.t as any, loaderItem }); + }; - renderItem = (item, previousItem, highlightedMessage) => { + renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => { const { room, lastOpen, canAutoTranslate } = this.state; - const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme } = - this.props; + const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled } = this.props; let dateSeparator = null; let showUnreadSeparator = false; @@ -1004,14 +1153,15 @@ class RoomView extends React.Component { dateSeparator = item.ts; showUnreadSeparator = moment(item.ts).isAfter(lastOpen); } else { - showUnreadSeparator = lastOpen && moment(item.ts).isSameOrAfter(lastOpen) && moment(previousItem.ts).isBefore(lastOpen); + showUnreadSeparator = + (lastOpen && moment(item.ts).isSameOrAfter(lastOpen) && moment(previousItem.ts).isBefore(lastOpen)) ?? false; if (!moment(item.ts).isSame(previousItem.ts, 'day')) { dateSeparator = item.ts; } } let content = null; - if (MESSAGE_TYPE_ANY_LOAD.includes(item.t)) { + if (item.t && MESSAGE_TYPE_ANY_LOAD.includes(item.t)) { content = ( this.onLoadMoreMessages(item)} @@ -1023,10 +1173,10 @@ class RoomView extends React.Component { content = ( {content} - + ); } @@ -1114,7 +1264,7 @@ class RoomView extends React.Component { ); } - if (isBlocked(room)) { + if ('id' in room && isBlocked(room)) { return ( {I18n.t('This_room_is_blocked')} @@ -1147,9 +1297,13 @@ class RoomView extends React.Component { renderActions = () => { const { room, readOnly } = this.state; const { user } = this.props; + if (!('id' in room)) { + return null; + } return ( <> (this.messageActions = ref)} tmid={this.tmid} room={room} @@ -1160,6 +1314,7 @@ class RoomView extends React.Component { onReactionPress={this.onReactionPress} isReadOnly={readOnly} /> + {/* @ts-ignore TODO: missing interface on MessageErrorActions */} (this.messageErrorActions = ref)} tmid={this.tmid} /> ); @@ -1169,33 +1324,35 @@ class RoomView extends React.Component { console.count(`${this.constructor.name}.render calls`); const { room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader } = this.state; const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height, serverVersion } = this.props; - const { rid, t, sysMes, bannerClosed, announcement } = room; + const { rid, t } = room; + let sysMes; + let bannerClosed; + let announcement; + let tunread; + let ignored; + if ('id' in room) { + ({ sysMes, bannerClosed, announcement, tunread, ignored } = room); + } return ( - + {this.renderFooter()} @@ -1209,7 +1366,7 @@ class RoomView extends React.Component { height={height} theme={theme} /> - + @@ -1225,23 +1383,19 @@ class RoomView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: IApplicationState) => ({ user: getUserSelector(state), isMasterDetail: state.app.isMasterDetail, appState: state.app.ready && state.app.foreground ? 'foreground' : 'background', - useRealName: state.settings.UI_Use_Real_Name, + useRealName: state.settings.UI_Use_Real_Name as boolean, isAuthenticated: state.login.isAuthenticated, - Message_GroupingPeriod: state.settings.Message_GroupingPeriod, - Message_TimeFormat: state.settings.Message_TimeFormat, + Message_GroupingPeriod: state.settings.Message_GroupingPeriod as number, + Message_TimeFormat: state.settings.Message_TimeFormat as string, customEmojis: state.customEmojis, baseUrl: state.server.server, serverVersion: state.server.version, - Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled, - Hide_System_Messages: state.settings.Hide_System_Messages + Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled as boolean, + Hide_System_Messages: state.settings.Hide_System_Messages as string[] }); -const mapDispatchToProps = dispatch => ({ - replyBroadcast: message => dispatch(replyBroadcastAction(message)) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomView)))); +export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomView)))); diff --git a/app/views/RoomView/services/getMessageInfo.js b/app/views/RoomView/services/getMessageInfo.js deleted file mode 100644 index e922f4e10..000000000 --- a/app/views/RoomView/services/getMessageInfo.js +++ /dev/null @@ -1,41 +0,0 @@ -import { getMessageById } from '../../../lib/database/services/Message'; -import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage'; -import getSingleMessage from '../../../lib/methods/getSingleMessage'; - -const getMessageInfo = async messageId => { - let result; - result = await getMessageById(messageId); - if (result) { - return { - id: result.id, - rid: result.subscription.id, - tmid: result.tmid, - msg: result.msg - }; - } - - result = await getThreadMessageById(messageId); - if (result) { - return { - id: result.id, - rid: result.subscription.id, - tmid: result.rid, - msg: result.msg - }; - } - - result = await getSingleMessage(messageId); - if (result) { - return { - id: result._id, - rid: result.rid, - tmid: result.tmid, - msg: result.msg, - fromServer: true - }; - } - - return null; -}; - -export default getMessageInfo; diff --git a/app/views/RoomView/services/getMessageInfo.ts b/app/views/RoomView/services/getMessageInfo.ts new file mode 100644 index 000000000..66d85c63a --- /dev/null +++ b/app/views/RoomView/services/getMessageInfo.ts @@ -0,0 +1,41 @@ +import { TMessageModel, TThreadMessageModel } from '../../../definitions'; +import { getMessageById } from '../../../lib/database/services/Message'; +import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage'; +import getSingleMessage from '../../../lib/methods/getSingleMessage'; + +const getMessageInfo = async (messageId: string): Promise => { + const message = await getMessageById(messageId); + if (message) { + return { + id: message.id, + rid: message?.subscription?.id, + tmid: message.tmid, + msg: message.msg + }; + } + + const threadMessage = await getThreadMessageById(messageId); + if (threadMessage) { + return { + id: threadMessage.id, + rid: threadMessage?.subscription?.id, + tmid: threadMessage.rid, + msg: threadMessage.msg + }; + } + + const singleMessage: any = await getSingleMessage(messageId); + if (singleMessage) { + return { + id: singleMessage._id, + rid: singleMessage.rid, + tmid: singleMessage.tmid, + msg: singleMessage.msg, + fromServer: true + }; + } + + return null; +}; + +export default getMessageInfo; diff --git a/app/views/RoomView/services/getMessages.js b/app/views/RoomView/services/getMessages.js deleted file mode 100644 index 516f68845..000000000 --- a/app/views/RoomView/services/getMessages.js +++ /dev/null @@ -1,10 +0,0 @@ -import RocketChat from '../../../lib/rocketchat'; - -const getMessages = room => { - if (room.lastOpen) { - return RocketChat.loadMissedMessages(room); - } else { - return RocketChat.loadMessagesForRoom(room); - } -}; -export default getMessages; diff --git a/app/views/RoomView/services/getMessages.ts b/app/views/RoomView/services/getMessages.ts new file mode 100644 index 000000000..a50cc2d9c --- /dev/null +++ b/app/views/RoomView/services/getMessages.ts @@ -0,0 +1,23 @@ +import loadMessagesForRoom from '../../../lib/methods/loadMessagesForRoom'; +import loadMissedMessages from '../../../lib/methods/loadMissedMessages'; + +// TODO: clarify latest vs lastOpen +const getMessages = ({ + rid, + t, + latest, + lastOpen, + loaderItem +}: { + rid: string; + t?: string; + latest?: Date; + lastOpen?: Date; + loaderItem?: any; // TODO: type this +}): Promise => { + if (lastOpen) { + return loadMissedMessages({ rid, lastOpen }); + } + return loadMessagesForRoom({ rid, t: t as any, latest, loaderItem }); +}; +export default getMessages; diff --git a/app/views/RoomView/services/getMoreMessages.js b/app/views/RoomView/services/getMoreMessages.js deleted file mode 100644 index eaa1d3d90..000000000 --- a/app/views/RoomView/services/getMoreMessages.js +++ /dev/null @@ -1,27 +0,0 @@ -import { - MESSAGE_TYPE_LOAD_MORE, - MESSAGE_TYPE_LOAD_NEXT_CHUNK, - MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK -} from '../../../constants/messageTypeLoad'; -import RocketChat from '../../../lib/rocketchat'; - -const getMoreMessages = ({ rid, t, tmid, loaderItem }) => { - if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t)) { - return RocketChat.loadMessagesForRoom({ - rid, - t, - latest: loaderItem.ts, - loaderItem - }); - } - - if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) { - return RocketChat.loadNextMessages({ - rid, - tmid, - ts: loaderItem.ts, - loaderItem - }); - } -}; -export default getMoreMessages; diff --git a/app/views/RoomView/services/getMoreMessages.ts b/app/views/RoomView/services/getMoreMessages.ts new file mode 100644 index 000000000..dae63c3b0 --- /dev/null +++ b/app/views/RoomView/services/getMoreMessages.ts @@ -0,0 +1,40 @@ +import { MessageType, SubscriptionType, TAnyMessageModel } from '../../../definitions'; +import loadMessagesForRoom from '../../../lib/methods/loadMessagesForRoom'; +import loadNextMessages from '../../../lib/methods/loadNextMessages'; +import { + MESSAGE_TYPE_LOAD_MORE, + MESSAGE_TYPE_LOAD_NEXT_CHUNK, + MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK +} from '../../../constants/messageTypeLoad'; + +const getMoreMessages = ({ + rid, + t, + tmid, + loaderItem +}: { + rid: string; + t: SubscriptionType; + tmid?: string; + loaderItem: TAnyMessageModel; +}): Promise => { + if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t as MessageType)) { + return loadMessagesForRoom({ + rid, + t: t as any, + latest: loaderItem.ts as Date, + loaderItem + }); + } + + if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) { + return loadNextMessages({ + rid, + tmid, + ts: loaderItem.ts as Date, + loaderItem + }); + } + return Promise.resolve(); +}; +export default getMoreMessages; diff --git a/app/views/RoomView/services/getThreadMessages.js b/app/views/RoomView/services/getThreadMessages.ts similarity index 61% rename from app/views/RoomView/services/getThreadMessages.js rename to app/views/RoomView/services/getThreadMessages.ts index 0f9529cfc..fd4aa20b4 100644 --- a/app/views/RoomView/services/getThreadMessages.js +++ b/app/views/RoomView/services/getThreadMessages.ts @@ -1,6 +1,6 @@ import RocketChat from '../../../lib/rocketchat'; // unlike getMessages, sync isn't required for threads, because loadMissedMessages does it already -const getThreadMessages = (tmid, rid) => RocketChat.loadThreadMessages({ tmid, rid }); +const getThreadMessages = (tmid: string, rid: string): Promise => RocketChat.loadThreadMessages({ tmid, rid }); export default getThreadMessages; diff --git a/app/views/RoomView/services/index.js b/app/views/RoomView/services/index.ts similarity index 100% rename from app/views/RoomView/services/index.js rename to app/views/RoomView/services/index.ts diff --git a/app/views/RoomView/services/readMessages.js b/app/views/RoomView/services/readMessages.js deleted file mode 100644 index 060d9aa7e..000000000 --- a/app/views/RoomView/services/readMessages.js +++ /dev/null @@ -1,5 +0,0 @@ -import RocketChat from '../../../lib/rocketchat'; - -const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true); - -export default readMessages; diff --git a/app/views/RoomView/services/readMessages.ts b/app/views/RoomView/services/readMessages.ts new file mode 100644 index 000000000..573027d0b --- /dev/null +++ b/app/views/RoomView/services/readMessages.ts @@ -0,0 +1,5 @@ +import RocketChat from '../../../lib/rocketchat'; + +const readMessages = (rid: string, newLastOpen: Date): Promise => RocketChat.readMessages(rid, newLastOpen, true); + +export default readMessages; diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.ts similarity index 100% rename from app/views/RoomView/styles.js rename to app/views/RoomView/styles.ts diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 2da0c7a62..e41728711 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -257,6 +257,7 @@ class SearchMessagesView extends React.Component { getReadOnly = async () => { const { room } = this.state; const { user } = this.props; - const readOnly = await isReadOnly(room, user); + const readOnly = await isReadOnly(room, user.username); return readOnly; }; diff --git a/app/views/ThreadMessagesView/Item.tsx b/app/views/ThreadMessagesView/Item.tsx index 780bc9341..b7453f011 100644 --- a/app/views/ThreadMessagesView/Item.tsx +++ b/app/views/ThreadMessagesView/Item.tsx @@ -70,6 +70,7 @@ const Item = ({ item, useRealName, user, badgeColor, onPress, toggleFollowThread const username = (useRealName && item?.u?.name) || item?.u?.username; let time; if (item?.ts) { + // @ts-ignore TODO: to be fixed after we unify our types time = formatDateThreads(item.ts); } diff --git a/e2e/tests/room/04-discussion.spec.js b/e2e/tests/room/04-discussion.spec.js index 74956e8d1..76dec6623 100644 --- a/e2e/tests/room/04-discussion.spec.js +++ b/e2e/tests/room/04-discussion.spec.js @@ -215,9 +215,6 @@ describe('Discussion', () => { await waitFor(element(by.id(`room-view-title-${discussionName}`))) .toExist() .withTimeout(5000); - await waitFor(element(by.id('messagebox'))) - .toBeVisible() - .withTimeout(60000); }); }); }); diff --git a/e2e/tests/team/03-moveconvert.spec.js b/e2e/tests/team/03-moveconvert.spec.js index 7d4add2af..258434900 100644 --- a/e2e/tests/team/03-moveconvert.spec.js +++ b/e2e/tests/team/03-moveconvert.spec.js @@ -5,6 +5,9 @@ const toBeConverted = `to-be-converted-${data.random}`; const toBeMoved = `to-be-moved-${data.random}`; const createChannel = async room => { + await waitFor(element(by.id('rooms-list-view-create-channel'))) + .toBeVisible() + .withTimeout(5000); await element(by.id('rooms-list-view-create-channel')).tap(); await waitFor(element(by.id('new-message-view'))) .toExist()