Chore: Migrate ThreadMessagesView to Typescript (#3538)

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
This commit is contained in:
Reinaldo Neto 2022-01-12 10:53:06 -03:00 committed by GitHub
parent a2b92f5e70
commit cd9ce58660
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 185 additions and 130 deletions

View File

@ -0,0 +1,6 @@
export interface IMention {
_id: string;
name: string;
username: string;
type: string;
}

6
app/definitions/IUrl.ts Normal file
View File

@ -0,0 +1,6 @@
export interface IUrl {
title: string;
description: string;
image: string;
url: string;
}

View File

@ -18,7 +18,7 @@ export type ChatsStackParamList = {
name?: string;
fname?: string;
prid?: string;
room: ISubscription;
room?: ISubscription;
jumpToMessageId?: string;
jumpToThreadId?: string;
roomUserId?: string;

View File

@ -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) => (
<Touch theme={theme} onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }}>
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{text}</Text>
@ -32,11 +38,4 @@ const DropdownItem = React.memo(({ theme, onPress, iconName, text }) => (
</Touch>
));
DropdownItem.propTypes = {
text: PropTypes.string,
iconName: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func
};
export default withTheme(DropdownItem);

View File

@ -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 => (
<DropdownItem text={I18n.t(value)} iconName={currentFilter === value ? 'check' : null} onPress={() => onPress(value)} />
);
DropdownItemFilter.propTypes = {
currentFilter: PropTypes.string,
value: PropTypes.string,
onPress: PropTypes.func
};
export default DropdownItemFilter;

View File

@ -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 <DropdownItem text={text} iconName='filter' onPress={onPress} />;
};
DropdownItemHeader.propTypes = {
currentFilter: PropTypes.string,
onPress: PropTypes.func
};
export default DropdownItemHeader;

View File

@ -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<IDropdownProps> {
private animatedValue: Animated.Value;
constructor(props: IDropdownProps) {
super(props);
this.animatedValue = new Animated.Value(0);
}
@ -85,9 +86,9 @@ class Dropdown extends React.Component {
]}>
<DropdownItemHeader currentFilter={currentFilter} onPress={this.close} />
<List.Separator />
<DropdownItemFilter currentFilter={currentFilter} value={FILTER.ALL} onPress={onFilterSelected} />
<DropdownItemFilter currentFilter={currentFilter} value={FILTER.FOLLOWING} onPress={onFilterSelected} />
<DropdownItemFilter currentFilter={currentFilter} value={FILTER.UNREAD} onPress={onFilterSelected} />
<DropdownItemFilter currentFilter={currentFilter} value={Filter.All} onPress={onFilterSelected} />
<DropdownItemFilter currentFilter={currentFilter} value={Filter.Following} onPress={onFilterSelected} />
<DropdownItemFilter currentFilter={currentFilter} value={Filter.Unread} onPress={onFilterSelected} />
</Animated.View>
</>
);

View File

@ -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 }}>
<View style={styles.container}>
<Avatar
style={styles.avatar}
text={item?.u?.username}
size={36}
borderRadius={4}
baseUrl={baseUrl}
userId={user?.id}
token={user?.token}
theme={theme}
/>
<Avatar style={styles.avatar} text={item?.u?.username} size={36} borderRadius={4} theme={theme} />
<View style={styles.contentContainer}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: themes[theme].titleText }]} numberOfLines={1}>
@ -87,10 +89,11 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View>
<View style={styles.messageContainer}>
{/* @ts-ignore */}
<Markdown
msg={makeThreadName(item)}
baseUrl={baseUrl}
username={username}
username={username!}
theme={theme}
numberOfLines={2}
style={[styles.markdown]}
@ -105,15 +108,4 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to
);
};
Item.propTypes = {
item: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
useRealName: PropTypes.bool,
user: PropTypes.object,
badgeColor: PropTypes.string,
onPress: PropTypes.func,
toggleFollowThread: PropTypes.func
};
export default withTheme(Item);

View File

@ -1,5 +0,0 @@
export const FILTER = {
ALL: 'All',
FOLLOWING: 'Following',
UNREAD: 'Unread'
};

View File

@ -0,0 +1,5 @@
export enum Filter {
All = 'All',
Following = 'Following',
Unread = 'Unread'
}

View File

@ -1,11 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList } from 'react-native';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBackButton } from '@react-navigation/stack';
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { Observable, Subscription } from 'rxjs';
import Model from '@nozbe/watermelondb/Model';
import Database from '@nozbe/watermelondb/Database';
import ActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
@ -30,27 +33,62 @@ import { getHeaderTitlePosition } from '../../containers/Header';
import EventEmitter from '../../utils/events';
import { LISTENER } from '../../containers/Toast';
import SearchHeader from '../../containers/SearchHeader';
import { FILTER } from './filters';
import { ChatsStackParamList } from '../../stacks/types';
import { IThreadResult, TThreadModel } from '../../definitions/IThread';
import { Filter } from './filters';
import DropdownItemHeader from './Dropdown/DropdownItemHeader';
import Dropdown from './Dropdown';
import Item from './Item';
import styles from './styles';
import { SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
const API_FETCH_COUNT = 50;
class ThreadMessagesView extends React.Component {
static propTypes = {
user: PropTypes.object,
navigation: PropTypes.object,
route: PropTypes.object,
baseUrl: PropTypes.string,
useRealName: PropTypes.bool,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
insets: PropTypes.object
};
interface IResultFetch {
threads: IThreadResult[];
count: number;
offset: number;
total: number;
success: boolean;
}
constructor(props) {
interface IThreadMessagesViewState {
loading: boolean;
end: boolean;
messages: any[];
displayingThreads: TThreadModel[];
subscription: TSubscriptionModel;
showFilterDropdown: boolean;
currentFilter: Filter;
isSearching: boolean;
searchText: string;
}
interface IThreadMessagesViewProps {
navigation: StackNavigationProp<ChatsStackParamList, 'ThreadMessagesView'>;
route: RouteProp<ChatsStackParamList, 'ThreadMessagesView'>;
user: any;
baseUrl: string;
useRealName: boolean;
theme: string;
isMasterDetail: boolean;
insets: EdgeInsets;
}
class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThreadMessagesViewState> {
private mounted: boolean;
private rid: string;
private t: string;
private subSubscription: any;
private messagesSubscription?: Subscription;
private messagesObservable!: Observable<Model>;
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: () => (
<HeaderBackButton labelVisible={false} onPress={() => 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,

View File

@ -25,6 +25,6 @@ export default StyleSheet.create({
borderBottomWidth: StyleSheet.hairlineWidth
},
backdrop: {
...StyleSheet.absoluteFill
...StyleSheet.absoluteFillObject
}
});