diff --git a/app/definitions/IMention.ts b/app/definitions/IMention.ts new file mode 100644 index 000000000..81305e7e2 --- /dev/null +++ b/app/definitions/IMention.ts @@ -0,0 +1,6 @@ +export interface IMention { + _id: string; + name: string; + username: string; + type: string; +} diff --git a/app/definitions/IUrl.ts b/app/definitions/IUrl.ts new file mode 100644 index 000000000..9b72fda26 --- /dev/null +++ b/app/definitions/IUrl.ts @@ -0,0 +1,6 @@ +export interface IUrl { + title: string; + description: string; + image: string; + url: string; +} diff --git a/app/stacks/types.ts b/app/stacks/types.ts index daf366d92..c6016f2e4 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -18,7 +18,7 @@ export type ChatsStackParamList = { name?: string; fname?: string; prid?: string; - room: ISubscription; + room?: ISubscription; jumpToMessageId?: string; jumpToThreadId?: string; roomUserId?: string; diff --git a/app/views/ThreadMessagesView/Dropdown/DropdownItem.js b/app/views/ThreadMessagesView/Dropdown/DropdownItem.tsx similarity index 84% rename from app/views/ThreadMessagesView/Dropdown/DropdownItem.js rename to app/views/ThreadMessagesView/Dropdown/DropdownItem.tsx index c5d69ba7c..d78aa0d74 100644 --- a/app/views/ThreadMessagesView/Dropdown/DropdownItem.js +++ b/app/views/ThreadMessagesView/Dropdown/DropdownItem.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { StyleSheet, Text, View } from 'react-native'; import { themes } from '../../../constants/colors'; @@ -23,7 +22,14 @@ const styles = StyleSheet.create({ } }); -const DropdownItem = React.memo(({ theme, onPress, iconName, text }) => ( +interface IDropdownItem { + text: string; + iconName: string; + theme: string; + onPress: () => void; +} + +const DropdownItem = React.memo(({ theme, onPress, iconName, text }: IDropdownItem) => ( {text} @@ -32,11 +38,4 @@ const DropdownItem = React.memo(({ theme, onPress, iconName, text }) => ( )); -DropdownItem.propTypes = { - text: PropTypes.string, - iconName: PropTypes.string, - theme: PropTypes.string, - onPress: PropTypes.func -}; - export default withTheme(DropdownItem); diff --git a/app/views/ThreadMessagesView/Dropdown/DropdownItemFilter.js b/app/views/ThreadMessagesView/Dropdown/DropdownItemFilter.tsx similarity index 54% rename from app/views/ThreadMessagesView/Dropdown/DropdownItemFilter.js rename to app/views/ThreadMessagesView/Dropdown/DropdownItemFilter.tsx index 7ff7b691f..5effd8adb 100644 --- a/app/views/ThreadMessagesView/Dropdown/DropdownItemFilter.js +++ b/app/views/ThreadMessagesView/Dropdown/DropdownItemFilter.tsx @@ -1,17 +1,16 @@ import React from 'react'; -import PropTypes from 'prop-types'; import I18n from '../../../i18n'; import DropdownItem from './DropdownItem'; -const DropdownItemFilter = ({ currentFilter, value, onPress }) => ( +interface IDropdownItemFilter { + currentFilter: string; + value: string; + onPress: (value: string) => void; +} + +const DropdownItemFilter = ({ currentFilter, value, onPress }: IDropdownItemFilter): JSX.Element => ( onPress(value)} /> ); -DropdownItemFilter.propTypes = { - currentFilter: PropTypes.string, - value: PropTypes.string, - onPress: PropTypes.func -}; - export default DropdownItemFilter; diff --git a/app/views/ThreadMessagesView/Dropdown/DropdownItemHeader.js b/app/views/ThreadMessagesView/Dropdown/DropdownItemHeader.tsx similarity index 61% rename from app/views/ThreadMessagesView/Dropdown/DropdownItemHeader.js rename to app/views/ThreadMessagesView/Dropdown/DropdownItemHeader.tsx index d1ee227de..9ffe4c89e 100644 --- a/app/views/ThreadMessagesView/Dropdown/DropdownItemHeader.js +++ b/app/views/ThreadMessagesView/Dropdown/DropdownItemHeader.tsx @@ -1,17 +1,21 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { FILTER } from '../filters'; +import { Filter } from '../filters'; import I18n from '../../../i18n'; import DropdownItem from './DropdownItem'; -const DropdownItemHeader = ({ currentFilter, onPress }) => { +interface IDropdownItemHeader { + currentFilter: Filter; + onPress: () => void; +} + +const DropdownItemHeader = ({ currentFilter, onPress }: IDropdownItemHeader): JSX.Element => { let text; switch (currentFilter) { - case FILTER.FOLLOWING: + case Filter.Following: text = I18n.t('Threads_displaying_following'); break; - case FILTER.UNREAD: + case Filter.Unread: text = I18n.t('Threads_displaying_unread'); break; default: @@ -21,9 +25,4 @@ const DropdownItemHeader = ({ currentFilter, onPress }) => { return ; }; -DropdownItemHeader.propTypes = { - currentFilter: PropTypes.string, - onPress: PropTypes.func -}; - export default DropdownItemHeader; diff --git a/app/views/ThreadMessagesView/Dropdown/index.js b/app/views/ThreadMessagesView/Dropdown/index.tsx similarity index 74% rename from app/views/ThreadMessagesView/Dropdown/index.js rename to app/views/ThreadMessagesView/Dropdown/index.tsx index ac33cd89b..a7a6eca07 100644 --- a/app/views/ThreadMessagesView/Dropdown/index.js +++ b/app/views/ThreadMessagesView/Dropdown/index.tsx @@ -1,30 +1,31 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Animated, Easing, TouchableWithoutFeedback } from 'react-native'; -import { withSafeAreaInsets } from 'react-native-safe-area-context'; +import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import styles from '../styles'; import { themes } from '../../../constants/colors'; import { withTheme } from '../../../theme'; import { headerHeight } from '../../../containers/Header'; import * as List from '../../../containers/List'; -import { FILTER } from '../filters'; +import { Filter } from '../filters'; import DropdownItemFilter from './DropdownItemFilter'; import DropdownItemHeader from './DropdownItemHeader'; const ANIMATION_DURATION = 200; -class Dropdown extends React.Component { - static propTypes = { - isMasterDetail: PropTypes.bool, - theme: PropTypes.string, - insets: PropTypes.object, - currentFilter: PropTypes.string, - onClose: PropTypes.func, - onFilterSelected: PropTypes.func - }; +interface IDropdownProps { + isMasterDetail: boolean; + theme: string; + insets: EdgeInsets; + currentFilter: Filter; + onClose: () => void; + onFilterSelected: (value: string) => void; +} - constructor(props) { +class Dropdown extends React.Component { + private animatedValue: Animated.Value; + + constructor(props: IDropdownProps) { super(props); this.animatedValue = new Animated.Value(0); } @@ -85,9 +86,9 @@ class Dropdown extends React.Component { ]}> - - - + + + ); diff --git a/app/views/ThreadMessagesView/Item.js b/app/views/ThreadMessagesView/Item.tsx similarity index 81% rename from app/views/ThreadMessagesView/Item.js rename to app/views/ThreadMessagesView/Item.tsx index 1caef2c27..5f444bccb 100644 --- a/app/views/ThreadMessagesView/Item.js +++ b/app/views/ThreadMessagesView/Item.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { StyleSheet, Text, View } from 'react-native'; import Touchable from 'react-native-platform-touchable'; @@ -10,6 +9,7 @@ import { themes } from '../../constants/colors'; import Markdown from '../../containers/markdown'; import { formatDateThreads, makeThreadName } from '../../utils/room'; import ThreadDetails from '../../containers/ThreadDetails'; +import { TThreadModel } from '../../definitions/IThread'; const styles = StyleSheet.create({ container: { @@ -56,7 +56,18 @@ const styles = StyleSheet.create({ } }); -const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, toggleFollowThread }) => { +interface IItem { + item: TThreadModel; + baseUrl: string; + theme: string; + useRealName: boolean; + user: any; + badgeColor: string; + onPress: (item: TThreadModel) => void; + toggleFollowThread: (isFollowing: boolean, id: string) => void; +} + +const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, toggleFollowThread }: IItem) => { const username = (useRealName && item?.u?.name) || item?.u?.username; let time; if (item?.ts) { @@ -69,16 +80,7 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to testID={`thread-messages-view-${item.msg}`} style={{ backgroundColor: themes[theme].backgroundColor }}> - + @@ -87,10 +89,11 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to {time} + {/* @ts-ignore */} ; + route: RouteProp; + user: any; + baseUrl: string; + useRealName: boolean; + theme: string; + isMasterDetail: boolean; + insets: EdgeInsets; +} + +class ThreadMessagesView extends React.Component { + private mounted: boolean; + + private rid: string; + + private t: string; + + private subSubscription: any; + + private messagesSubscription?: Subscription; + + private messagesObservable!: Observable; + + constructor(props: IThreadMessagesViewProps) { super(props); this.mounted = false; this.rid = props.route.params?.rid; @@ -60,9 +98,9 @@ class ThreadMessagesView extends React.Component { end: false, messages: [], displayingThreads: [], - subscription: {}, + subscription: {} as TSubscriptionModel, showFilterDropdown: false, - currentFilter: FILTER.ALL, + currentFilter: Filter.All, isSearching: false, searchText: '' }; @@ -76,7 +114,7 @@ class ThreadMessagesView extends React.Component { this.init(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IThreadMessagesViewProps) { const { insets } = this.props; if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { this.setHeader(); @@ -93,7 +131,7 @@ class ThreadMessagesView extends React.Component { } } - getHeader = () => { + getHeader = (): StackNavigationOptions => { const { isSearching } = this.state; const { navigation, isMasterDetail, insets, theme } = this.props; @@ -115,7 +153,7 @@ class ThreadMessagesView extends React.Component { }; } - const options = { + const options: StackNavigationOptions = { headerLeft: () => ( navigation.pop()} tintColor={themes[theme].headerTintColor} /> ), @@ -150,7 +188,7 @@ class ThreadMessagesView extends React.Component { const db = database.active; // subscription query - const subscription = await db.collections.get('subscriptions').find(this.rid); + const subscription = (await db.collections.get('subscriptions').find(this.rid)) as TSubscriptionModel; const observable = subscription.observe(); this.subSubscription = observable.subscribe(data => { this.setState({ subscription: data }); @@ -162,7 +200,7 @@ class ThreadMessagesView extends React.Component { } }; - subscribeMessages = (subscription, searchText) => { + subscribeMessages = (subscription?: TSubscriptionModel, searchText?: string) => { try { const db = database.active; @@ -180,13 +218,17 @@ class ThreadMessagesView extends React.Component { .get('threads') .query(...whereClause) .observeWithColumns(['updated_at']); - this.messagesSubscription = this.messagesObservable.subscribe(messages => { + + // TODO: Refactor when migrate messages + this.messagesSubscription = this.messagesObservable.subscribe((messages: any) => { const { currentFilter } = this.state; - const displayingThreads = this.getFilteredThreads(messages, subscription, currentFilter); + const displayingThreads = this.getFilteredThreads(messages, subscription!, currentFilter); if (this.mounted) { this.setState({ messages, displayingThreads }); } else { + // @ts-ignore this.state.messages = messages; + // @ts-ignore this.state.displayingThreads = displayingThreads; } }); @@ -212,7 +254,15 @@ class ThreadMessagesView extends React.Component { } }; - updateThreads = async ({ update, remove, lastThreadSync }) => { + updateThreads = async ({ + update, + remove, + lastThreadSync + }: { + update: IThreadResult[]; + remove?: IThreadResult[]; + lastThreadSync: Date; + }) => { const { subscription } = this.state; // if there's no subscription, manage data on this.state.messages // note: sync will never be called without subscription @@ -222,21 +272,23 @@ class ThreadMessagesView extends React.Component { } try { - const db = database.active; + const db: Database = database.active; const threadsCollection = db.get('threads'); - const allThreadsRecords = await subscription.threads.fetch(); - let threadsToCreate = []; - let threadsToUpdate = []; - let threadsToDelete = []; + // TODO: Refactor when migrate room + // @ts-ignore + const allThreadsRecords = (await subscription.threads.fetch()) as TThreadModel[]; + let threadsToCreate: any[] = []; + let threadsToUpdate: any[] = []; + let threadsToDelete: any[] = []; if (update && update.length) { update = update.map(m => buildMessage(m)); // filter threads - threadsToCreate = update.filter(i1 => !allThreadsRecords.find(i2 => i1._id === i2.id)); - threadsToUpdate = allThreadsRecords.filter(i1 => update.find(i2 => i1.id === i2._id)); + threadsToCreate = update.filter(i1 => allThreadsRecords.find((i2: { id: string }) => i1._id === i2.id)); + threadsToUpdate = allThreadsRecords.filter((i1: { id: string }) => update.find(i2 => i1.id === i2._id)); threadsToCreate = threadsToCreate.map(thread => threadsCollection.prepareCreate( - protectedFunction(t => { + protectedFunction((t: any) => { t._raw = sanitizedRaw({ id: thread._id }, threadsCollection.schema); t.subscription.set(subscription); Object.assign(t, thread); @@ -246,7 +298,7 @@ class ThreadMessagesView extends React.Component { threadsToUpdate = threadsToUpdate.map(thread => { const newThread = update.find(t => t._id === thread.id); return thread.prepareUpdate( - protectedFunction(t => { + protectedFunction((t: any) => { Object.assign(t, newThread); }) ); @@ -254,16 +306,16 @@ class ThreadMessagesView extends React.Component { } if (remove && remove.length) { - threadsToDelete = allThreadsRecords.filter(i1 => remove.find(i2 => i1.id === i2._id)); + threadsToDelete = allThreadsRecords.filter((i1: { id: string }) => remove.find(i2 => i1.id === i2._id)); threadsToDelete = threadsToDelete.map(t => t.prepareDestroyPermanently()); } - await db.action(async () => { + await db.write(async () => { await db.batch( ...threadsToCreate, ...threadsToUpdate, ...threadsToDelete, - subscription.prepareUpdate(s => { + subscription.prepareUpdate((s: any) => { s.lastThreadSync = lastThreadSync; }) ); @@ -274,7 +326,7 @@ class ThreadMessagesView extends React.Component { }; // eslint-disable-next-line react/sort-comp - load = debounce(async lastThreadSync => { + load = debounce(async (lastThreadSync: Date) => { const { loading, end, messages, searchText } = this.state; if (end || loading || !this.mounted) { return; @@ -283,7 +335,7 @@ class ThreadMessagesView extends React.Component { this.setState({ loading: true }); try { - const result = await RocketChat.getThreadsList({ + const result: IResultFetch = await RocketChat.getThreadsList({ rid: this.rid, count: API_FETCH_COUNT, offset: messages.length, @@ -303,7 +355,7 @@ class ThreadMessagesView extends React.Component { }, 300); // eslint-disable-next-line react/sort-comp - sync = async updatedSince => { + sync = async (updatedSince: Date) => { this.setState({ loading: true }); try { @@ -336,13 +388,13 @@ class ThreadMessagesView extends React.Component { }); }; - onSearchChangeText = debounce(searchText => { + onSearchChangeText = debounce((searchText: string) => { const { subscription } = this.state; this.setState({ searchText }, () => this.subscribeMessages(subscription, searchText)); }, 300); onThreadPress = debounce( - item => { + (item: any) => { const { subscription } = this.state; const { navigation, isMasterDetail } = this.props; if (isMasterDetail) { @@ -352,7 +404,7 @@ class ThreadMessagesView extends React.Component { rid: item.subscription.id, tmid: item.id, name: makeThreadName(item), - t: 'thread', + t: SubscriptionType.THREAD, roomUserId: RocketChat.getUidDirectMessage(subscription) }); }, @@ -360,20 +412,21 @@ class ThreadMessagesView extends React.Component { true ); - getBadgeColor = item => { + getBadgeColor = (item: TThreadModel) => { const { subscription } = this.state; const { theme } = this.props; return getBadgeColor({ subscription, theme, messageId: item?.id }); }; // helper to query threads - getFilteredThreads = (messages, subscription, currentFilter) => { + getFilteredThreads = (messages: any, subscription: TSubscriptionModel, currentFilter?: Filter): TThreadModel[] => { // const { currentFilter } = this.state; const { user } = this.props; - if (currentFilter === FILTER.FOLLOWING) { - return messages?.filter(item => item?.replies?.find(u => u === user.id)); - } else if (currentFilter === FILTER.UNREAD) { - return messages?.filter(item => subscription?.tunread?.includes(item?.id)); + if (currentFilter === Filter.Following) { + return messages?.filter((item: { replies: any[] }) => item?.replies?.find(u => u === user.id)); + } + if (currentFilter === Filter.Unread) { + return messages?.filter((item: { id: string }) => subscription?.tunread?.includes(item?.id)); } return messages; }; @@ -389,13 +442,13 @@ class ThreadMessagesView extends React.Component { closeFilterDropdown = () => this.setState({ showFilterDropdown: false }); - onFilterSelected = filter => { + onFilterSelected = (filter: Filter) => { const { messages, subscription } = this.state; const displayingThreads = this.getFilteredThreads(messages, subscription, filter); this.setState({ currentFilter: filter, displayingThreads }); }; - toggleFollowThread = async (isFollowingThread, tmid) => { + toggleFollowThread = async (isFollowingThread: boolean, tmid: string) => { try { await RocketChat.toggleFollowMessage(tmid, !isFollowingThread); EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') }); @@ -404,7 +457,7 @@ class ThreadMessagesView extends React.Component { } }; - renderItem = ({ item }) => { + renderItem = ({ item }: { item: TThreadModel }) => { const { user, navigation, baseUrl, useRealName } = this.props; const badgeColor = this.getBadgeColor(item); return ( @@ -442,9 +495,9 @@ class ThreadMessagesView extends React.Component { const { theme } = this.props; if (!messages?.length || !displayingThreads?.length) { let text; - if (currentFilter === FILTER.FOLLOWING) { + if (currentFilter === Filter.Following) { text = I18n.t('No_threads_following'); - } else if (currentFilter === FILTER.UNREAD) { + } else if (currentFilter === Filter.Unread) { text = I18n.t('No_threads_unread'); } else { text = I18n.t('No_threads'); @@ -494,7 +547,7 @@ class ThreadMessagesView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ baseUrl: state.server.server, user: getUserSelector(state), useRealName: state.settings.UI_Use_Real_Name, diff --git a/app/views/ThreadMessagesView/styles.js b/app/views/ThreadMessagesView/styles.ts similarity index 93% rename from app/views/ThreadMessagesView/styles.js rename to app/views/ThreadMessagesView/styles.ts index ec24da209..a6b30bbac 100644 --- a/app/views/ThreadMessagesView/styles.js +++ b/app/views/ThreadMessagesView/styles.ts @@ -25,6 +25,6 @@ export default StyleSheet.create({ borderBottomWidth: StyleSheet.hairlineWidth }, backdrop: { - ...StyleSheet.absoluteFill + ...StyleSheet.absoluteFillObject } });