diff --git a/app/views/ThreadMessagesView/types.ts b/app/views/ThreadMessagesView/definitions.ts similarity index 61% rename from app/views/ThreadMessagesView/types.ts rename to app/views/ThreadMessagesView/definitions.ts index 470c166af1..4d21d2bc91 100644 --- a/app/views/ThreadMessagesView/types.ts +++ b/app/views/ThreadMessagesView/definitions.ts @@ -1,26 +1,16 @@ import React from "react"; -import { Filter } from "./filters"; import { IBaseScreen, TSubscriptionModel, TThreadModel } from "../../definitions"; import { ChatsStackParamList } from "../../stacks/types"; import { TSupportedThemes } from "../../theme"; -export type TSearchThreadMessages = { +export interface ISearchThreadMessages { isSearching: boolean; searchText: string; }; -export type TUseThreadMessagesProps = { - rid: string; - getFilteredThreads: (messages: TThreadModel[], subscription?: TSubscriptionModel, currentFilter?: Filter) => TThreadModel[]; - search: TSearchThreadMessages; - currentFilter: Filter; - initFilter: () => void; - viewName: string; -}; - -export type TUSeThreadMessages = { +export interface IUSeThreadMessages { subscription: TSubscriptionModel; messages: TThreadModel[]; displayingThreads: TThreadModel[]; @@ -36,7 +26,7 @@ export type TUSeThreadMessages = { }) => void; }; -export type IThreadMessagesViewProps = IBaseScreen & { +export interface IThreadMessagesViewProps extends IBaseScreen { user: { id: string }; baseUrl: string; useRealName: boolean; @@ -45,4 +35,3 @@ export type IThreadMessagesViewProps = IBaseScreen void; + setDisplayingThreads: (threads: TThreadModel[]) => void; +} + +const useThreadFilter = ({ + user, + messages, + subscription, + currentFilter, + setCurrentFilter, + setDisplayingThreads +}: IUseThreadFilter) => { + const initFilter = () => { + const savedFilter = UserPreferences.getString(THREADS_FILTER); + if (savedFilter) { + setCurrentFilter(savedFilter as Filter); + } + }; + + const onFilterSelected = useCallback( + (filter: Filter) => { + const displayingThreads = getFilteredThreads(user, messages, subscription, filter); + setCurrentFilter(filter); + setDisplayingThreads(displayingThreads); + UserPreferences.setString(THREADS_FILTER, filter); + }, + [messages, subscription] + ); + + const showFilters = () => { + showActionSheetRef({ + options: [ + { + title: I18n.t(Filter.All), + right: currentFilter === Filter.All ? () => : undefined, + onPress: () => onFilterSelected(Filter.All) + }, + { + title: I18n.t(Filter.Following), + right: currentFilter === Filter.Following ? () => : undefined, + onPress: () => onFilterSelected(Filter.Following) + }, + { + title: I18n.t(Filter.Unread), + right: currentFilter === Filter.Unread ? () => : undefined, + onPress: () => onFilterSelected(Filter.Unread) + } + ] + }); + }; + + return { + currentFilter, + initFilter, + showFilters + }; +}; + +export default useThreadFilter; diff --git a/app/views/ThreadMessagesView/hooks/useThreadMessages.tsx b/app/views/ThreadMessagesView/hooks/useThreadMessages.tsx index bfc3bbc030..aac1968559 100644 --- a/app/views/ThreadMessagesView/hooks/useThreadMessages.tsx +++ b/app/views/ThreadMessagesView/hooks/useThreadMessages.tsx @@ -1,10 +1,10 @@ -import { useLayoutEffect, useState } from 'react'; +import { useState } from 'react'; import { Q } from '@nozbe/watermelondb'; import { Observable, Subscription } from 'rxjs'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; -import { TUseThreadMessagesProps } from '../types'; -import { IMessage, TSubscriptionModel, TThreadModel } from '../../../definitions'; +import { ISearchThreadMessages } from '../definitions'; +import { IMessage, IUser, TSubscriptionModel, TThreadModel } from '../../../definitions'; import { debounce } from '../../../lib/methods/helpers'; import { Services } from '../../../lib/services'; import { sanitizeLikeString } from '../../../lib/database/utils'; @@ -12,14 +12,22 @@ import log from '../../../lib/methods/helpers/log'; import protectedFunction from '../../../lib/methods/helpers/protectedFunction'; import buildMessage from '../../../lib/methods/helpers/buildMessage'; import database from '../../../lib/database'; +import getFilteredThreads from '../utils/helper'; +import { Filter } from '../filters'; const API_FETCH_COUNT = 50; -const useThreadMessages = ({ rid, getFilteredThreads, search, currentFilter, initFilter, viewName }: TUseThreadMessagesProps) => { +interface IUseThreadMessagesProps { + user: IUser; + rid: string; + messagesObservable: Observable; + currentFilter: Filter; + search: ISearchThreadMessages; +} + +const useThreadMessages = ({ user, rid, search, currentFilter, messagesObservable }: IUseThreadMessagesProps) => { let subSubscription: Subscription; let messagesSubscription: Subscription; - let messagesObservable: Observable; - const [loading, setLoading] = useState(false); const [end, setEnd] = useState(false); const [messages, setMessages] = useState([]); @@ -28,7 +36,6 @@ const useThreadMessages = ({ rid, getFilteredThreads, search, currentFilter, ini const [offset, setOffset] = useState(0); const init = () => { - initFilter(); if (!subscription) { return load(); } @@ -81,7 +88,7 @@ const useThreadMessages = ({ rid, getFilteredThreads, search, currentFilter, ini .observeWithColumns(['_updated_at']); messagesSubscription = messagesObservable.subscribe(messages => { - const displayingThreads = getFilteredThreads(messages, subscription, currentFilter); + const displayingThreads = getFilteredThreads(user, messages, subscription, currentFilter); setMessages(messages); setDisplayingThreads(displayingThreads); }); @@ -206,29 +213,26 @@ const useThreadMessages = ({ rid, getFilteredThreads, search, currentFilter, ini } }; - useLayoutEffect(() => { - initSubscription(); - subscribeMessages({}); - init(); - return () => { - console.countReset(`${viewName}.render calls`); - if (subSubscription) { - subSubscription.unsubscribe(); - } - if (messagesSubscription) { - messagesSubscription.unsubscribe(); - } - }; - }, [currentFilter]); + const unsubscribeMessages = () => { + if (subSubscription) { + subSubscription.unsubscribe(); + } + if (messagesSubscription) { + messagesSubscription.unsubscribe(); + } + }; return { subscription, messages, + init, + initSubscription, displayingThreads, loadMore: load, loading, setDisplayingThreads, - subscribeMessages + subscribeMessages, + unsubscribeMessages }; }; diff --git a/app/views/ThreadMessagesView/hooks/useeThreadSearch.tsx b/app/views/ThreadMessagesView/hooks/useeThreadSearch.tsx new file mode 100644 index 0000000000..eaeea9a4aa --- /dev/null +++ b/app/views/ThreadMessagesView/hooks/useeThreadSearch.tsx @@ -0,0 +1,86 @@ +import { debounce } from 'lodash'; +import I18n from 'i18n-js'; +import { NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import * as HeaderButton from '../../../containers/HeaderButton'; +import SearchHeader from '../../../containers/SearchHeader'; +import { ChatsStackParamList } from '../../../stacks/types'; +import { TNavigation } from '../../../stacks/stackType'; +import { TSubscriptionModel } from '../../../definitions'; +import { ISearchThreadMessages } from '../definitions'; + +interface IUseThreadSearch { + isMasterDetail: boolean; + navigation: NativeStackNavigationProp; + search: ISearchThreadMessages; + setSearch: (search: ISearchThreadMessages) => void; + subscribeMessages: ({ subscription, searchText }: { subscription?: TSubscriptionModel; searchText?: string }) => void; + showFilters: () => void; +} + +const useThreadSearch = ({ isMasterDetail, navigation, search, setSearch, subscribeMessages, showFilters }: IUseThreadSearch) => { + const getHeader = (triggerSearch?: boolean): NativeStackNavigationOptions => { + if (search.isSearching || triggerSearch) { + return { + headerLeft: () => ( + + + + ), + headerTitle: () => , + headerRight: () => null + }; + } + + const options: NativeStackNavigationOptions = { + headerLeft: () => null, + headerTitle: I18n.t('Threads'), + headerRight: () => ( + + + + + ) + }; + + if (isMasterDetail) { + options.headerLeft = () => ; + } + + return options; + }; + + const setHeader = () => { + const options = getHeader(); + navigation.setOptions(options); + }; + + const onSearchPress = () => { + setSearch({ ...search, isSearching: true }); + const options = getHeader(true); + navigation.setOptions(options); + }; + + const onSearchChangeText = debounce((searchText: string) => { + setSearch({ isSearching: true, searchText }); + subscribeMessages({ searchText }); + }, 300); + + const onCancelSearchPress = () => { + setSearch({ + isSearching: false, + searchText: '' + }); + setHeader(); + subscribeMessages({}); + }; + + return { + setHeader, + onSearchPress, + onSearchChangeText, + onCancelSearchPress + }; +}; + +export default useThreadSearch; diff --git a/app/views/ThreadMessagesView/index.tsx b/app/views/ThreadMessagesView/index.tsx index 2c2e5ad6ca..f8f5c86425 100644 --- a/app/views/ThreadMessagesView/index.tsx +++ b/app/views/ThreadMessagesView/index.tsx @@ -1,8 +1,8 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useState } from 'react'; import { FlatList } from 'react-native'; -import { NativeStackNavigationOptions } from '@react-navigation/native-stack'; +import { Observable } from 'rxjs'; -import { IThreadMessagesViewProps, TSearchThreadMessages } from './types'; +import { IThreadMessagesViewProps, ISearchThreadMessages } from './definitions'; import { Filter } from './filters'; import { themes } from '../../lib/constants'; import { Services } from '../../lib/services'; @@ -10,31 +10,30 @@ import { useAppSelector } from '../../lib/hooks'; import { getBadgeColor, makeThreadName } from '../../lib/methods/helpers/room'; import { getUidDirectMessage, debounce, isIOS } from '../../lib/methods/helpers'; import { getUserSelector } from '../../selectors/login'; -import { showActionSheetRef } from '../../containers/ActionSheet'; -import { CustomIcon } from '../../containers/CustomIcon'; import { LISTENER } from '../../containers/Toast'; import { useTheme } from '../../theme'; -import { SubscriptionType, TSubscriptionModel, TThreadModel } from '../../definitions'; +import { SubscriptionType, TThreadModel } from '../../definitions'; import ActivityIndicator from '../../containers/ActivityIndicator'; import StatusBar from '../../containers/StatusBar'; import SafeAreaView from '../../containers/SafeAreaView'; -import * as HeaderButton from '../../containers/HeaderButton'; import * as List from '../../containers/List'; -import SearchHeader from '../../containers/SearchHeader'; import log from '../../lib/methods/helpers/log'; import EventEmitter from '../../lib/methods/helpers/events'; -import UserPreferences from '../../lib/methods/userPreferences'; import I18n from '../../i18n'; import Item from './components/Item'; import EmptyThreads from './components/EmptyThreads'; import useThreadMessages from './hooks/useThreadMessages'; import styles from './styles'; - -const THREADS_FILTER = 'threadsFilter'; +import useThreadFilter from './hooks/useThreadFilter'; +import getFilteredThreads from './utils/helper'; +import useThreadSearch from './hooks/useeThreadSearch'; const ThreadMessagesView = ({ navigation, route }: IThreadMessagesViewProps) => { const viewName = ThreadMessagesView.name; const rid = route.params?.rid; + let messagesObservable: Observable | any; + const [currentFilter, setCurrentFilter] = useState(Filter.All); + const [search, setSearch] = useState({} as ISearchThreadMessages); const { theme } = useTheme(); const { user, useRealName, isMasterDetail } = useAppSelector(state => ({ @@ -43,120 +42,43 @@ const ThreadMessagesView = ({ navigation, route }: IThreadMessagesViewProps) => isMasterDetail: state.app.isMasterDetail })); - const [search, setSearch] = useState({} as TSearchThreadMessages); - const [currentFilter, setCurrentFilter] = useState(Filter.All); + const { + init, + initSubscription, + messages, + subscription, + displayingThreads, + loadMore, + loading, + setDisplayingThreads, + subscribeMessages, + unsubscribeMessages + } = useThreadMessages({ + user, + messagesObservable, + currentFilter, + rid, + search + }); + + const { initFilter, showFilters } = useThreadFilter({ + currentFilter, + setCurrentFilter, + user, + messages, + setDisplayingThreads, + subscription + }); + + const { setHeader } = useThreadSearch({ + isMasterDetail, + navigation, + search, + setSearch, + showFilters, + subscribeMessages + }); - // helper to query threads - const getFilteredThreads = ( - messages: TThreadModel[], - subscription?: TSubscriptionModel, - currentFilter?: Filter - ): TThreadModel[] => { - if (currentFilter === Filter.Following) { - return messages.filter(item => item?.replies?.find(u => u === user.id)); - } - if (currentFilter === Filter.Unread) { - return messages?.filter(item => subscription?.tunread?.includes(item?.id)); - } - return messages; - }; - - // filter - const initFilter = () => { - const savedFilter = UserPreferences.getString(THREADS_FILTER); - if (savedFilter) { - setCurrentFilter(savedFilter as Filter); - } - }; - - const showFilters = () => { - showActionSheetRef({ - options: [ - { - title: I18n.t(Filter.All), - right: currentFilter === Filter.All ? () => : undefined, - onPress: () => onFilterSelected(Filter.All) - }, - { - title: I18n.t(Filter.Following), - right: currentFilter === Filter.Following ? () => : undefined, - onPress: () => onFilterSelected(Filter.Following) - }, - { - title: I18n.t(Filter.Unread), - right: currentFilter === Filter.Unread ? () => : undefined, - onPress: () => onFilterSelected(Filter.Unread) - } - ] - }); - }; - - const onFilterSelected = useCallback((filter: Filter) => { - const displayingThreads = getFilteredThreads(messages, subscription, filter); - setCurrentFilter(filter); - setDisplayingThreads(displayingThreads); - UserPreferences.setString(THREADS_FILTER, filter); - }, []); - - // search - const onSearchPress = () => { - setSearch({ ...search, isSearching: true }); - const options = getHeader(true); - navigation.setOptions(options); - }; - - const onSearchChangeText = debounce((searchText: string) => { - setSearch({ isSearching: true, searchText }); - subscribeMessages({ searchText }); - }, 300); - - const onCancelSearchPress = () => { - setSearch({ - isSearching: false, - searchText: '' - }); - setHeader(); - subscribeMessages({}); - }; - - // header - const getHeader = (triggerSearch?: boolean): NativeStackNavigationOptions => { - if (search.isSearching || triggerSearch) { - return { - headerLeft: () => ( - - - - ), - headerTitle: () => , - headerRight: () => null - }; - } - - const options: NativeStackNavigationOptions = { - headerLeft: () => null, - headerTitle: I18n.t('Threads'), - headerRight: () => ( - - - - - ) - }; - - if (isMasterDetail) { - options.headerLeft = () => ; - } - - return options; - }; - - const setHeader = () => { - const options = getHeader(); - navigation.setOptions(options); - }; - - // thread const onThreadPress = debounce( (item: any) => { if (isMasterDetail) { @@ -178,7 +100,7 @@ const ThreadMessagesView = ({ navigation, route }: IThreadMessagesViewProps) => try { await Services.toggleFollowMessage(tmid, !isFollowingThread); EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') }); - const updatedThreads = getFilteredThreads(messages, subscription, currentFilter); + const updatedThreads = getFilteredThreads(user, messages, subscription, currentFilter); setDisplayingThreads(updatedThreads); } catch (e) { log(e); @@ -202,15 +124,17 @@ const ThreadMessagesView = ({ navigation, route }: IThreadMessagesViewProps) => ); }; - const { messages, subscription, displayingThreads, loadMore, loading, setDisplayingThreads, subscribeMessages } = - useThreadMessages({ - rid, - getFilteredThreads, - currentFilter, - initFilter, - search, - viewName - }); + useLayoutEffect(() => { + initSubscription(); + subscribeMessages({}); + init(); + initFilter(); + + return () => { + console.countReset(`${viewName}.render calls`); + unsubscribeMessages(); + }; + }, [currentFilter]); useEffect(() => { setHeader(); diff --git a/app/views/ThreadMessagesView/utils/helper.ts b/app/views/ThreadMessagesView/utils/helper.ts new file mode 100644 index 0000000000..24b37f2103 --- /dev/null +++ b/app/views/ThreadMessagesView/utils/helper.ts @@ -0,0 +1,19 @@ +import { IUser, TSubscriptionModel, TThreadModel } from "../../../definitions"; +import { Filter } from "../filters"; + +const getFilteredThreads = ( + user: IUser, + messages: TThreadModel[], + subscription?: TSubscriptionModel, + currentFilter?: Filter +): TThreadModel[] => { + if (currentFilter === Filter.Following) { + return messages.filter(item => item?.replies?.find(u => u === user.id)); + } + if (currentFilter === Filter.Unread) { + return messages?.filter(item => subscription?.tunread?.includes(item?.id)); + } + return messages; +}; + +export default getFilteredThreads; \ No newline at end of file