From 63e37fd253d29ce3d0d01996296a8b320e56b094 Mon Sep 17 00:00:00 2001 From: Gabriella Putri Date: Fri, 26 Jan 2024 14:12:05 +0700 Subject: [PATCH] Patch fixes --- api/src/__tests__/processRawContent.ts | 48 ++++++++ api/src/helpers/processRawContent.ts | 43 +++++++ api/src/resolvers/site/aboutQuery.ts | 16 ++- api/src/typeSchemas/UserActions.ts | 6 + .../CustomFlatList/CustomFlatList.tsx | 1 + frontend/src/components/LoadingOrError.tsx | 43 ++++++- frontend/src/components/Metrics/Metrics.tsx | 24 ++-- frontend/src/components/PostItem/PostItem.tsx | 4 +- .../PostItem/UserInformationPostItem.tsx | 5 +- frontend/src/components/PostList.tsx | 2 +- frontend/src/constants/theme/colors.ts | 11 +- frontend/src/core-ui/ActivityIndicator.tsx | 2 +- frontend/src/graphql/server/userActivity.ts | 1 + .../helpers/__tests__/unescapeHTML.test.ts | 9 ++ frontend/src/helpers/index.ts | 1 + frontend/src/helpers/localStorage.tsx | 3 + frontend/src/helpers/unescapeHTML.ts | 15 +++ frontend/src/hooks/post/useLikeTopicOrPost.ts | 5 +- frontend/src/hooks/post/useTopicList.ts | 14 +-- frontend/src/hooks/site/useChannels.ts | 4 +- frontend/src/hooks/useUpdateApp.ts | 39 +++++++ frontend/src/navigation/AppNavigator.tsx | 43 ++++--- frontend/src/screens/Activity.tsx | 7 +- frontend/src/screens/Channels/Channels.tsx | 20 +++- .../src/screens/EmailAddress/EmailAddress.tsx | 5 +- frontend/src/screens/Home/Home.tsx | 105 ++++++++++++------ frontend/src/screens/InstanceLoading.tsx | 16 ++- .../screens/MessageDetail/MessageDetail.tsx | 2 +- frontend/src/screens/Messages/Messages.tsx | 2 +- .../screens/Notifications/Notifications.tsx | 2 +- .../src/screens/PostDetail/PostDetail.tsx | 6 +- .../PostDetail/PostDetailSkeletonLoading.tsx | 4 +- .../src/screens/SelectUser/SelectUser.tsx | 7 +- frontend/src/theme/theme.ts | 9 ++ 34 files changed, 421 insertions(+), 103 deletions(-) create mode 100644 frontend/src/helpers/__tests__/unescapeHTML.test.ts create mode 100644 frontend/src/helpers/unescapeHTML.ts create mode 100644 frontend/src/hooks/useUpdateApp.ts diff --git a/api/src/__tests__/processRawContent.ts b/api/src/__tests__/processRawContent.ts index beebca59..fbf71c9d 100644 --- a/api/src/__tests__/processRawContent.ts +++ b/api/src/__tests__/processRawContent.ts @@ -2,6 +2,7 @@ import { generateMarkdownContent, getCompleteImageVideoUrls, getEmojiImageUrls, + userActivityMarkdownContent, } from '../helpers'; describe('getCompleteImageUrls return image urls from html tags', () => { @@ -186,3 +187,50 @@ describe('generate emoji url from image tag', () => { expect(getEmojiImageUrls(content3)).toEqual([]); }); }); + +describe('generate new content for user activity', () => { + it('it should return Content based input', () => { + const content = 'Hello\n who is this'; + const content1 = 'Just want to test\n\n something'; + + expect(userActivityMarkdownContent(content)).toEqual(content); + expect(userActivityMarkdownContent(content1)).toEqual(content1); + }); + it('it should replace content image', () => { + const contentImage = + 'Hello\n [exampleImage1]'; + const contentImageSrc = + 'download'; + + expect(userActivityMarkdownContent(contentImage)).toEqual( + 'Hello\n ![exampleImage1](https://image.jpeg)', + ); + expect(userActivityMarkdownContent(contentImageSrc)).toEqual( + '![undefined](https://wiki.kfox.io/uploads/default/original.jpeg)', + ); + }); + it('it should convert emoji', () => { + const emojiContent = + 'Hello\n :heart:'; + + expect(userActivityMarkdownContent(emojiContent)).toEqual( + 'Hello\n ![emoji-:heart:](https://image/heart.png?v=12)', + ); + }); + it('it should convert mention', () => { + const mentionContent = + 'Is this true? @marcello'; + + expect(userActivityMarkdownContent(mentionContent)).toEqual( + 'Is this true? @marcello', + ); + }); + it('it should convert Link', () => { + const mentionContent = + 'Hello'; + + expect(userActivityMarkdownContent(mentionContent)).toEqual( + '[Hello](https://www.google.com)', + ); + }); +}); diff --git a/api/src/helpers/processRawContent.ts b/api/src/helpers/processRawContent.ts index 7656dae1..385b2161 100644 --- a/api/src/helpers/processRawContent.ts +++ b/api/src/helpers/processRawContent.ts @@ -16,6 +16,9 @@ const emojiBBCodeRegex = /(?<=^|\s):\w+:(?:t\d+:)?/g; const emojiImageTagRegex = //g; const emojiTitleRegex = /title="([^"]+)"/g; +const userActivityContentRegex = + /(?:]*src(?:set)?="(.+?)"(?:[^>]*title="([^"]*)")?(?:[^>]*class="([^"]*)")?[^>]*>)|(?:]* href="((https?:)?\/\/[^ ]*\.(?:jpe?g|png|gif|heic|heif|mov|mp4|webm|avi|wmv|flv|webp))"([^>]*?)title="([^"]*)"\s*>(\[.*?\])?<\/a>)|(?:]* class="mention" href="\/u\/([^"]+)">@(.*?)<\/a>)|(?:]* href="([^"]+)"[^>]*>(.*?)<\/a>)/g; + function handleRegexResult( result: RegExpMatchArray, host: string, @@ -166,3 +169,43 @@ export function getMention( return handleRegexResult(result, host, mentionRegex); } } + +export function userActivityMarkdownContent(content: string) { + const markdown = content.replace( + userActivityContentRegex, + ( + _, + imgSrc: string, + imgTitle: string, + imgClass: string, + aHref: string, + _https, + _dataHref, + aTitle: string, + _emptyMention, + _urlName, + nameMention, + linkHref, + linkText, + ) => { + let modifiedImageMarkdown = ``; + + if (imgSrc) { + modifiedImageMarkdown = `![${ + imgClass === 'emoji' || imgClass === 'emoji only-emoji' + ? 'emoji-' + : '' + }${imgTitle}](${imgSrc})`; + } else if (aHref) { + modifiedImageMarkdown = `![${aTitle}](${aHref})`; + } else if (nameMention) { + modifiedImageMarkdown = `@${nameMention}`; + } else if (linkHref && linkText) { + modifiedImageMarkdown = `[${linkText}](${linkHref})`; + } + + return modifiedImageMarkdown; + }, + ); + return markdown; +} diff --git a/api/src/resolvers/site/aboutQuery.ts b/api/src/resolvers/site/aboutQuery.ts index aa76e836..5095d6a8 100644 --- a/api/src/resolvers/site/aboutQuery.ts +++ b/api/src/resolvers/site/aboutQuery.ts @@ -11,17 +11,27 @@ let aboutResolver: FieldResolver<'Query', 'about'> = async ( try { let siteUrl = `/about.json`; + /** + * In here when use newest version discourse from 3.2.0.beta4-dev the name of field change into topics_count and posts_count + * + * And for the previous version discourse it use topic_count and post_count + */ let { data: { about: { - stats: { topic_count: topicCount, post_count: postCount }, + stats: { + topics_count: topicsCount, + topic_count: topicCount, + posts_count: postsCount, + post_count: postCount, + }, }, }, } = await context.client.get(siteUrl); return { - topicCount, - postCount, + topicCount: topicCount || topicsCount, + postCount: postCount || postsCount, }; } catch (error) { throw errorHandler(error); diff --git a/api/src/typeSchemas/UserActions.ts b/api/src/typeSchemas/UserActions.ts index a3aea65c..fde81a05 100644 --- a/api/src/typeSchemas/UserActions.ts +++ b/api/src/typeSchemas/UserActions.ts @@ -1,6 +1,7 @@ import { objectType } from 'nexus'; import { getNormalizedUrlTemplate } from '../resolvers/utils'; +import { userActivityMarkdownContent } from '../helpers'; export let UserActions = objectType({ name: 'UserActions', @@ -37,5 +38,10 @@ export let UserActions = objectType({ t.int('topicId'); t.int('userId'); t.string('username'); + t.nullable.string('markdownContent', { + resolve: ({ excerpt }) => { + return userActivityMarkdownContent(excerpt); + }, + }); }, }); diff --git a/frontend/src/components/CustomFlatList/CustomFlatList.tsx b/frontend/src/components/CustomFlatList/CustomFlatList.tsx index df745aeb..4e8820fe 100644 --- a/frontend/src/components/CustomFlatList/CustomFlatList.tsx +++ b/frontend/src/components/CustomFlatList/CustomFlatList.tsx @@ -219,6 +219,7 @@ function BaseCustomFlatList( } onEndReached={(info) => { diff --git a/frontend/src/components/LoadingOrError.tsx b/frontend/src/components/LoadingOrError.tsx index 6cf8d121..968e7a6b 100644 --- a/frontend/src/components/LoadingOrError.tsx +++ b/frontend/src/components/LoadingOrError.tsx @@ -1,17 +1,27 @@ import React, { useEffect } from 'react'; -import { View } from 'react-native'; +import { + StyleProp, + ScrollView, + View, + ViewStyle, + RefreshControl, + RefreshControlProps, +} from 'react-native'; + import { useNavigation } from '@react-navigation/native'; import { ActivityIndicator, Text } from '../core-ui'; import { showLogoutAlert } from '../helpers'; -import { makeStyles } from '../theme'; +import { makeStyles, useTheme } from '../theme'; import { StackNavProp } from '../types'; import { useAuth } from '../utils/AuthProvider'; import { ERROR_NOT_FOUND } from '../constants'; -type Props = { +type Props = Pick & { message?: string; loading?: boolean; + style?: StyleProp; + refreshing?: boolean; }; export function LoadingOrError(props: Props) { @@ -36,15 +46,37 @@ export function LoadingOrError(props: Props) { export function LoadingOrErrorView(props: Props) { const styles = useStyles(); + const { colors } = useTheme(); const { loading = false, message = loading ? t('Loading...') : t('Something unexpected happened. Please try again'), + onRefresh, + style, + refreshing, + progressViewOffset, } = props; - return ( - + return onRefresh ? ( + + } + > + + {loading ? : null} + {message} + + + ) : ( + {loading ? : null} {message} @@ -58,4 +90,5 @@ const useStyles = makeStyles(() => ({ justifyContent: 'center', width: '100%', }, + scrollViewContentStyle: { flex: 1 }, })); diff --git a/frontend/src/components/Metrics/Metrics.tsx b/frontend/src/components/Metrics/Metrics.tsx index 9110d02c..721deace 100644 --- a/frontend/src/components/Metrics/Metrics.tsx +++ b/frontend/src/components/Metrics/Metrics.tsx @@ -19,7 +19,7 @@ type Props = { export { Props as MetricsProp }; -const DEBOUNCE_WAIT_TIME = 500; +const DEBOUNCE_WAIT_TIME = 1000; export function Metrics(props: Props) { const { likedTopics } = useOngoingLikedTopic(); @@ -136,19 +136,21 @@ export function Metrics(props: Props) { }, [performDebouncedLike]); // TODO: Add navigation #800 - const [like] = useLikeTopicOrPost(); + const [like, { loading }] = useLikeTopicOrPost(); const onPressLike = useCallback(() => { - setLikeData(({ liked: prevLiked, likeCount: previousCount }) => { - const liked = !prevLiked; - const likeCount = getUpdatedLikeCount({ - liked, - previousCount, + if (!loading) { + setLikeData(({ liked: prevLiked, likeCount: previousCount }) => { + const liked = !prevLiked; + const likeCount = getUpdatedLikeCount({ + liked, + previousCount, + }); + performDebouncedLike(liked); + return { liked, likeCount }; }); - performDebouncedLike(liked); - return { liked, likeCount }; - }); - }, [performDebouncedLike]); + } + }, [loading, performDebouncedLike]); return ( - {content} + {unescapeHTML(content)} )} diff --git a/frontend/src/components/PostItem/UserInformationPostItem.tsx b/frontend/src/components/PostItem/UserInformationPostItem.tsx index 1ff90684..b0dda0f5 100644 --- a/frontend/src/components/PostItem/UserInformationPostItem.tsx +++ b/frontend/src/components/PostItem/UserInformationPostItem.tsx @@ -46,12 +46,13 @@ function BaseUserInformationPostItem(props: Props) { let { title, - excerpt: content, + excerpt, avatarTemplate, categoryId, hidden, createdAt, username, + markdownContent: content, } = cacheUserAction; const channels = storage.getItem('channels'); @@ -68,7 +69,7 @@ function BaseUserInformationPostItem(props: Props) { topicId={topicId} title={title} currentUser={currentUser} - content={content} + content={content || excerpt} avatar={avatar} channel={channel} hidden={!!hidden} diff --git a/frontend/src/components/PostList.tsx b/frontend/src/components/PostList.tsx index 158935f3..fc56dc05 100644 --- a/frontend/src/components/PostList.tsx +++ b/frontend/src/components/PostList.tsx @@ -54,7 +54,7 @@ function PostList(props: Props) { } diff --git a/frontend/src/constants/theme/colors.ts b/frontend/src/constants/theme/colors.ts index 89d4961f..c5ae441f 100644 --- a/frontend/src/constants/theme/colors.ts +++ b/frontend/src/constants/theme/colors.ts @@ -28,6 +28,10 @@ export const BASE_COLORS = { lightGray: '#F2F2F2', mustardYellow: '#CC9619', lightYellow: '#FAF4E7', + + squidInk: '#262A31', + approxBlackRussian: '#1C1F24', + lightSilver: '#D8D8D8', }; export const FUNCTIONAL_COLORS = { @@ -64,8 +68,11 @@ export const FUNCTIONAL_COLORS = { pureWhite: BASE_COLORS.pureWhite, pureBlack: BASE_COLORS.pureBlack, - skeletonLoadingBackGround: BASE_COLORS.darkerGray, - skeletonLoadingHighlight: BASE_COLORS.lightGray, + skeletonLoadingLightBackGround: BASE_COLORS.grey, + skeletonLoadingLightHighlight: BASE_COLORS.lightSilver, + + skeletonLoadingDarkBackGround: BASE_COLORS.squidInk, + skeletonLoadingDarkHighlight: BASE_COLORS.approxBlackRussian, yellowText: BASE_COLORS.mustardYellow, }; diff --git a/frontend/src/core-ui/ActivityIndicator.tsx b/frontend/src/core-ui/ActivityIndicator.tsx index b64eb82b..e8ce1acb 100644 --- a/frontend/src/core-ui/ActivityIndicator.tsx +++ b/frontend/src/core-ui/ActivityIndicator.tsx @@ -13,7 +13,7 @@ type Props = ActivityIndicatorProps & { export function ActivityIndicator(props: Props) { const { colors } = useTheme(); - const { color = 'primary', ...otherProps } = props; + const { color = 'loading', ...otherProps } = props; const indicatorColor = colors[color]; return ; diff --git a/frontend/src/graphql/server/userActivity.ts b/frontend/src/graphql/server/userActivity.ts index f56a15c4..e2767b94 100644 --- a/frontend/src/graphql/server/userActivity.ts +++ b/frontend/src/graphql/server/userActivity.ts @@ -13,6 +13,7 @@ export const USER_ACTIONS_FRAGMENT = gql` topicId username hidden + markdownContent } `; diff --git a/frontend/src/helpers/__tests__/unescapeHTML.test.ts b/frontend/src/helpers/__tests__/unescapeHTML.test.ts new file mode 100644 index 00000000..8790d260 --- /dev/null +++ b/frontend/src/helpers/__tests__/unescapeHTML.test.ts @@ -0,0 +1,9 @@ +import { unescapeHTML } from '../unescapeHTML'; + +it('should unescape HTML characters', () => { + expect( + unescapeHTML( + `<test>'hello' & "world"…</test>`, + ), + ).toEqual(`'hello' & "world"...`); +}); diff --git a/frontend/src/helpers/index.ts b/frontend/src/helpers/index.ts index d7cd335e..3ecceb2f 100644 --- a/frontend/src/helpers/index.ts +++ b/frontend/src/helpers/index.ts @@ -46,3 +46,4 @@ export * from './checkImageFile'; export * from './PrivateTopicAlert'; export * from './PushNotificationsSetupFailAlert'; export * from './parser'; +export * from './unescapeHTML'; diff --git a/frontend/src/helpers/localStorage.tsx b/frontend/src/helpers/localStorage.tsx index e1166d1c..13a9567b 100644 --- a/frontend/src/helpers/localStorage.tsx +++ b/frontend/src/helpers/localStorage.tsx @@ -23,6 +23,8 @@ export let PushNotificationsPreferences = Record({ shouldSetBadge: Boolean, }); +export let SelectedHomeChannelId = Number; + let [StorageProvider, useStorage] = createCachedStorage( { colorScheme: (value) => ColorScheme.check(value), @@ -31,6 +33,7 @@ let [StorageProvider, useStorage] = createCachedStorage( expoPushToken: (value) => String.check(value), channels: (value) => ChannelList.check(value), pushNotifications: (value) => PushNotificationsPreferences.check(value), + homeChannelId: (value) => SelectedHomeChannelId.check(value), }, '@Cached/', ); diff --git a/frontend/src/helpers/unescapeHTML.ts b/frontend/src/helpers/unescapeHTML.ts new file mode 100644 index 00000000..801f7ef3 --- /dev/null +++ b/frontend/src/helpers/unescapeHTML.ts @@ -0,0 +1,15 @@ +const htmlEntities: Record = { + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '&': '&', + '…': '...', +}; + +export function unescapeHTML(str: string) { + return str.replace( + /<|>|"|'|&|…/g, + (match: string) => htmlEntities[match], + ); +} diff --git a/frontend/src/hooks/post/useLikeTopicOrPost.ts b/frontend/src/hooks/post/useLikeTopicOrPost.ts index fd59811c..1ba898eb 100644 --- a/frontend/src/hooks/post/useLikeTopicOrPost.ts +++ b/frontend/src/hooks/post/useLikeTopicOrPost.ts @@ -109,7 +109,10 @@ const optimisticResponse: MutationOptimisticResponse = ({ const refetchQueries: MutationRefetchQueries = ({ data }) => { const topicDetailQuery = { query: GetTopicDetailDocument, - variables: { topicId: data?.likeTopicOrPost.topicId }, + variables: { + topicId: data?.likeTopicOrPost.topicId, + includeFirstPost: true, + }, }; return [TOPICS, topicDetailQuery]; }; diff --git a/frontend/src/hooks/post/useTopicList.ts b/frontend/src/hooks/post/useTopicList.ts index 210032e1..b29d1d9d 100644 --- a/frontend/src/hooks/post/useTopicList.ts +++ b/frontend/src/hooks/post/useTopicList.ts @@ -21,13 +21,11 @@ export function useTopicList( export function useLazyTopicList( options?: LazyQueryHookOptions, ) { - const [getTopicList, { loading, error, refetch, fetchMore }] = useLazyQuery< - TopicListType, - TopicListVariables - >(TopicsDocument, { - context: { queryDeduplication: true }, - ...options, - }); + const [getTopicList, { data, loading, error, refetch, fetchMore }] = + useLazyQuery(TopicsDocument, { + context: { queryDeduplication: true }, + ...options, + }); - return { getTopicList, loading, error, refetch, fetchMore }; + return { getTopicList, loading, error, refetch, fetchMore, data }; } diff --git a/frontend/src/hooks/site/useChannels.ts b/frontend/src/hooks/site/useChannels.ts index f6aaab0b..39e58dba 100644 --- a/frontend/src/hooks/site/useChannels.ts +++ b/frontend/src/hooks/site/useChannels.ts @@ -11,7 +11,7 @@ export function useChannels( options?: QueryHookOptions, errorAlert: ErrorAlertOptionType = 'SHOW_ALERT', ) { - const { data, loading, error } = useQuery( + const { data, loading, error, refetch } = useQuery( GetChannelsDocument, { ...options, @@ -19,5 +19,5 @@ export function useChannels( errorAlert, ); - return { data, loading, error }; + return { data, loading, error, refetch }; } diff --git a/frontend/src/hooks/useUpdateApp.ts b/frontend/src/hooks/useUpdateApp.ts new file mode 100644 index 00000000..a362676c --- /dev/null +++ b/frontend/src/hooks/useUpdateApp.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'react'; +import * as Updates from 'expo-updates'; + +export function useUpdateApp() { + const [loading, setLoading] = useState(false); + + const checkUpdate = async () => { + const { isAvailable } = await Updates.checkForUpdateAsync(); + if (isAvailable) { + setLoading(true); + performUpdate(); + } + }; + + const performUpdate = async () => { + Updates.fetchUpdateAsync() + .then(() => { + reloadApp(); + }) + .catch(() => { + reloadApp(); + }); + }; + + const reloadApp = async () => { + await Updates.reloadAsync(); + }; + + useEffect(() => { + if (!__DEV__) { + checkUpdate(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + loading, + }; +} diff --git a/frontend/src/navigation/AppNavigator.tsx b/frontend/src/navigation/AppNavigator.tsx index e8218b44..6c2ddf8e 100644 --- a/frontend/src/navigation/AppNavigator.tsx +++ b/frontend/src/navigation/AppNavigator.tsx @@ -9,8 +9,9 @@ import { import { StatusBar } from 'expo-status-bar'; import * as Linking from 'expo-linking'; import * as Notifications from 'expo-notifications'; +import { View } from 'react-native'; -import { useColorScheme } from '../theme'; +import { makeStyles, useColorScheme } from '../theme'; import { RootStackParamList } from '../types'; import { DEEP_LINK_SCREEN_CONFIG, @@ -23,34 +24,41 @@ import { isRouteBesidePost, postOrMessageDetailPathToRoutes } from '../helpers'; import { useRedirect } from '../utils'; import { useInitialLoad } from '../hooks/useInitialLoad'; import { LoadingOrErrorView } from '../components'; +import { useUpdateApp } from '../hooks/useUpdateApp'; import RootStackNavigator from './RootStackNavigator'; import { navigationRef } from './NavigationService'; +import { useAuth } from '../utils/AuthProvider'; export default function AppNavigator() { const { colorScheme } = useColorScheme(); const useInitialLoadResult = useInitialLoad(); const { setRedirectPath } = useRedirect(); + const auth = useAuth(); + const styles = useStyles(); + const { loading: appUpdateLoading } = useUpdateApp(); const darkMode = colorScheme === 'dark'; return ( <> - {useInitialLoadResult.loading ? ( - + {useInitialLoadResult.loading || auth.isLoading || appUpdateLoading ? ( + ) : ( - - - + + + + + )} ); @@ -123,3 +131,10 @@ const createLinkingConfig = (params: CreateLinkingConfigParams) => { }; return linking; }; + +const useStyles = makeStyles(({ colors }) => ({ + background: { + flex: 1, + backgroundColor: colors.background, + }, +})); diff --git a/frontend/src/screens/Activity.tsx b/frontend/src/screens/Activity.tsx index 290a24cc..54d820b3 100644 --- a/frontend/src/screens/Activity.tsx +++ b/frontend/src/screens/Activity.tsx @@ -39,7 +39,7 @@ export default function Activity() { } if (loading && activities.length < 1) { - return ; + return ; } let content; @@ -80,7 +80,7 @@ export default function Activity() { return {content}; } -const useStyles = makeStyles(({ spacing }) => ({ +const useStyles = makeStyles(({ spacing, colors }) => ({ contentContainer: { paddingTop: spacing.m, }, @@ -98,4 +98,7 @@ const useStyles = makeStyles(({ spacing }) => ({ noActivityText: { alignSelf: 'center', }, + loadingContainer: { + backgroundColor: colors.background, + }, })); diff --git a/frontend/src/screens/Channels/Channels.tsx b/frontend/src/screens/Channels/Channels.tsx index ea84652e..4e1ace59 100644 --- a/frontend/src/screens/Channels/Channels.tsx +++ b/frontend/src/screens/Channels/Channels.tsx @@ -4,7 +4,11 @@ import { useNavigation, useRoute } from '@react-navigation/native'; import { useFormContext } from 'react-hook-form'; import { CustomHeader, HeaderItem, ModalHeader } from '../../components'; -import { isNoChannelFilter, NO_CHANNEL_FILTER } from '../../constants'; +import { + isNoChannelFilter, + NO_CHANNEL_FILTER, + NO_CHANNEL_FILTER_ID, +} from '../../constants'; import { useStorage } from '../../helpers'; import { makeStyles } from '../../theme'; import { RootStackNavProp, RootStackRouteProp } from '../../types'; @@ -25,13 +29,19 @@ export default function Channels() { const { setValue, getValues } = useFormContext(); const { channelId: selectedChannelId } = getValues(); + const homeSelectedChannelId = storage.getItem('homeChannelId'); + + const selectedChannel = + prevScreen === 'Home' ? homeSelectedChannelId : selectedChannelId; + const ios = Platform.OS === 'ios'; const onPress = (id: number) => { - setValue('channelId', id); if (prevScreen === 'Home') { + storage.setItem('homeChannelId', id); navigate('TabNav', { screen: 'Home' }); } else { + setValue('channelId', id, { shouldDirty: true }); navigate(prevScreen); } }; @@ -49,7 +59,9 @@ export default function Channels() { {prevScreen === 'Home' && ( onPress(NO_CHANNEL_FILTER.id)} /> @@ -59,7 +71,7 @@ export default function Channels() { return ( onPress(id)} /> diff --git a/frontend/src/screens/EmailAddress/EmailAddress.tsx b/frontend/src/screens/EmailAddress/EmailAddress.tsx index 5ab28161..1365a742 100644 --- a/frontend/src/screens/EmailAddress/EmailAddress.tsx +++ b/frontend/src/screens/EmailAddress/EmailAddress.tsx @@ -66,7 +66,7 @@ export default function EmailAddress() { ); if (userLoading && emailAddress.length === 0) { - return ; + return ; } return ( @@ -109,4 +109,7 @@ const useStyles = makeStyles(({ colors, spacing }) => ({ right: 0, bottom: 0, }, + loadingContainer: { + backgroundColor: colors.background, + }, })); diff --git a/frontend/src/screens/Home/Home.tsx b/frontend/src/screens/Home/Home.tsx index 6aa44966..9d9dd73f 100644 --- a/frontend/src/screens/Home/Home.tsx +++ b/frontend/src/screens/Home/Home.tsx @@ -15,7 +15,6 @@ import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; -import { useFormContext } from 'react-hook-form'; import { FooterLoadingIndicator, @@ -34,7 +33,6 @@ import { TopicsSortEnum, TopicsQuery, TopicsQueryVariables, - TopicsDocument, TopicFragmentDoc, TopicFragment, } from '../../generated/server'; @@ -106,9 +104,6 @@ export default function Home() { const { addListener, navigate } = useNavigation>(); const { params } = useRoute>(); - const { getValues } = useFormContext(); - const { channelId: receivedChannelId } = getValues(); - const routeParams = params === undefined ? false : params.backToTop; const FIRST_PAGE = 0; @@ -174,7 +169,11 @@ export default function Home() { const [allTopicCount, setAllTopicCount] = useState(0); const [width, setWidth] = useState(0); - const { loading: channelsLoading, error: channelsError } = useChannels( + const { + loading: channelsLoading, + error: channelsError, + refetch: channelsRefetch, + } = useChannels( { onCompleted: (data) => { if (data && data.category.categories) { @@ -185,6 +184,10 @@ export default function Home() { storage.setItem('channels', channels); } }, + onError: () => { + setRefreshing(false); + setLoading(false); + }, }, 'HIDE_ALERT', ); @@ -230,6 +233,7 @@ export default function Home() { error: topicsError, refetch: refetchTopics, fetchMore: fetchMoreTopics, + data: topicDataList, } = useLazyTopicList({ variables: isNoChannelFilter(selectedChannelId) ? { sort: sortState, page, username } @@ -238,30 +242,34 @@ export default function Home() { setRefreshing(false); setLoading(false); }, - onCompleted: (data) => { - if (data) { - setData(data); - setLoading(false); - } + onCompleted: () => { + setLoading(false); }, }); const getData = useCallback( (variables: TopicsQueryVariables) => { - try { - const data: TopicsQuery | null = client.readQuery({ - query: TopicsDocument, - variables, - }); - data && setData(data); - } catch (e) { - setLoading(true); - } getTopicList({ variables }); }, - [getTopicList, setData], + [getTopicList], ); + useEffect(() => { + /** + * This useEffect is used to set data if there are changes in the result query. + * If the topics list is cached, it will show the result in `topicDataList`. + * But if the topics list is not in the cache, it will show `undefined` until it finishes getting data from the query. + * + */ + + if (topicDataList) { + setData(topicDataList); + setLoading(false); + } else { + setLoading(true); + } + }, [setData, topicDataList]); + useLayoutEffect(() => { if (!isFlatList(postListRef.current)) { return; @@ -273,18 +281,27 @@ export default function Home() { }, [selectedChannelId]); useEffect(() => { - let channels = storage.getItem('channels'); - if (channels && receivedChannelId) { - setSelectedChannelId(receivedChannelId); - } else if (channels) { - setSelectedChannelId(NO_CHANNEL_FILTER.id); - } - const unsubscribe = addListener('focus', () => { + /** + * We need to call `getHomeChannelId` to retrieve the initial value because this function will only be called once during the first render. + * + * During the first render, the value of `receivedChannelId` has not changed outside of the `useEffect`. This can result in the function using the previously selected channel's value. + * + * In the previous code, we utilized param screen or `watch` from `react-hook-form`, ensuring that the value had already changed during the initial render or before calling this function. + */ + + const receivedChannelId = storage.getItem('homeChannelId'); + let channels = storage.getItem('channels'); + if (channels && receivedChannelId) { + setSelectedChannelId(receivedChannelId); + } else if (channels) { + setSelectedChannelId(NO_CHANNEL_FILTER.id); + } + let categoryId = receivedChannelId; if (receivedChannelId) { categoryId = isNoChannelFilter(receivedChannelId) - ? undefined + ? null : receivedChannelId; } let currentPage = page; @@ -308,7 +325,6 @@ export default function Home() { }, [ selectedChannelId, getAbout, - receivedChannelId, username, storage, sortState, @@ -346,10 +362,15 @@ export default function Home() { setRefreshing(true); if (refetchTopics) { setPage(FIRST_PAGE); - refetchTopics().then(() => setRefreshing(false)); + refetchTopics().finally(() => setRefreshing(false)); } }; + const onRefreshError = () => { + channelsRefetch(); + onRefresh(); + }; + const onSegmentedControlItemPress = ({ name }: SortOption) => { const sortState: TopicsSortEnum = name === 'LATEST' ? TopicsSortEnum.Latest : TopicsSortEnum.Top; @@ -442,10 +463,27 @@ export default function Home() { const content = () => { if (channelsError) { - return ; + return ( + { + channelsRefetch(); + onRefresh(); + }} + /> + ); } if (topicsError) { - return ; + return ( + + ); } if (!topicsData || channelsLoading || loading) { return ; @@ -527,7 +565,6 @@ const useStyles = makeStyles(({ colors, shadow, spacing }) => ({ container: { flex: 1, width: '100%', - alignItems: 'flex-start', justifyContent: 'flex-start', backgroundColor: colors.backgroundDarker, }, diff --git a/frontend/src/screens/InstanceLoading.tsx b/frontend/src/screens/InstanceLoading.tsx index 6a11d1b2..b7c081dc 100644 --- a/frontend/src/screens/InstanceLoading.tsx +++ b/frontend/src/screens/InstanceLoading.tsx @@ -1,9 +1,11 @@ import React, { useEffect } from 'react'; import { useNavigation } from '@react-navigation/native'; +import { View } from 'react-native'; import { LoadingOrError } from '../components'; import { StackNavProp } from '../types'; import { useSiteSettings } from '../hooks'; +import { makeStyles } from '../theme'; export default function Loading() { const { reset } = useNavigation>(); @@ -12,6 +14,7 @@ export default function Loading() { canSignUp, error: siteSettingsError, } = useSiteSettings({ fetchPolicy: 'network-only' }); + const styles = useStyles(); useEffect(() => { if (loading) { @@ -31,5 +34,16 @@ export default function Loading() { } }, [canSignUp, siteSettingsError, loading, reset]); - return <>{loading && }; + return ( + + {loading && } + + ); } + +const useStyles = makeStyles(({ colors }) => ({ + background: { + flex: 1, + backgroundColor: colors.background, + }, +})); diff --git a/frontend/src/screens/MessageDetail/MessageDetail.tsx b/frontend/src/screens/MessageDetail/MessageDetail.tsx index 917adaf5..304cf0ab 100644 --- a/frontend/src/screens/MessageDetail/MessageDetail.tsx +++ b/frontend/src/screens/MessageDetail/MessageDetail.tsx @@ -596,7 +596,7 @@ export default function MessageDetail() { loadMoreMessages(false)} - tintColor={colors.primary} + tintColor={colors.loading} /> } data={data?.contents ?? []} diff --git a/frontend/src/screens/Messages/Messages.tsx b/frontend/src/screens/Messages/Messages.tsx index f36c6790..e12c1787 100644 --- a/frontend/src/screens/Messages/Messages.tsx +++ b/frontend/src/screens/Messages/Messages.tsx @@ -167,7 +167,7 @@ export default function Messages() { } getItem={getItem} diff --git a/frontend/src/screens/Notifications/Notifications.tsx b/frontend/src/screens/Notifications/Notifications.tsx index 4e62f5e7..6693baeb 100644 --- a/frontend/src/screens/Notifications/Notifications.tsx +++ b/frontend/src/screens/Notifications/Notifications.tsx @@ -236,7 +236,7 @@ export default function Notifications() { } getItem={getItem} diff --git a/frontend/src/screens/PostDetail/PostDetail.tsx b/frontend/src/screens/PostDetail/PostDetail.tsx index 7d77b197..77fa15d7 100644 --- a/frontend/src/screens/PostDetail/PostDetail.tsx +++ b/frontend/src/screens/PostDetail/PostDetail.tsx @@ -175,7 +175,7 @@ export default function PostDetail() { const showOptions = false || - !!(topic && topic.canEditTopic) || + !!(firstPost && firstPost.canEdit) || (!!firstPost && firstPost.canFlag && !firstPost.hidden); useEffect(() => { @@ -373,7 +373,7 @@ export default function PostDetail() { let { id, canFlag = !!(firstPost && firstPost.canFlag), - canEdit = !!(topic && topic.canEditTopic), + canEdit = !!(firstPost && firstPost.canEdit), flaggedByCommunity = !!(firstPost && firstPost.hidden), fromPost = true, author, @@ -551,7 +551,7 @@ export default function PostDetail() { refreshing={ (loadingRefresh || isLoadingOlderPost) && !isLoadingNewerPost } - refreshControlTintColor={colors.primary} + refreshControlTintColor={colors.loading} onEndReachedThreshold={0.1} onEndReached={() => loadMoreComments(true)} ListFooterComponent={ diff --git a/frontend/src/screens/PostDetail/PostDetailSkeletonLoading.tsx b/frontend/src/screens/PostDetail/PostDetailSkeletonLoading.tsx index 9f38cae1..caa4d7d9 100644 --- a/frontend/src/screens/PostDetail/PostDetailSkeletonLoading.tsx +++ b/frontend/src/screens/PostDetail/PostDetailSkeletonLoading.tsx @@ -16,8 +16,8 @@ export default function (props: Props) { diff --git a/frontend/src/screens/SelectUser/SelectUser.tsx b/frontend/src/screens/SelectUser/SelectUser.tsx index 56ecd34d..73981f6c 100644 --- a/frontend/src/screens/SelectUser/SelectUser.tsx +++ b/frontend/src/screens/SelectUser/SelectUser.tsx @@ -182,7 +182,12 @@ export default function SelectUser() { user.username .toLowerCase() .includes(searchValue.toLowerCase()) && - !selectedUsers.includes({ ...user, name: user.name ?? null }), + !selectedUsers.some( + (selectedUser) => + selectedUser.name === user.name && + selectedUser.username === user.username && + user.avatar === selectedUser.avatar, + ), ) .map((user) => { if (user.name && user.name !== ownerName) { diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index 128e2f43..87512ae6 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -55,6 +55,15 @@ function colorTheme(isDarkMode: boolean) { toastInfoText: isDarkMode ? FUNCTIONAL_COLORS.darkTextLighter : FUNCTIONAL_COLORS.lightTextDarker, + skeletonLoadingBackgroundMode: isDarkMode + ? FUNCTIONAL_COLORS.skeletonLoadingDarkBackGround + : FUNCTIONAL_COLORS.skeletonLoadingLightBackGround, + skeletonLoadingHighlightMode: isDarkMode + ? FUNCTIONAL_COLORS.skeletonLoadingDarkHighlight + : FUNCTIONAL_COLORS.skeletonLoadingLightHighlight, + loading: isDarkMode + ? FUNCTIONAL_COLORS.pureWhite + : FUNCTIONAL_COLORS.primary, }; }