Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions api/src/__tests__/processRawContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
generateMarkdownContent,
getCompleteImageVideoUrls,
getEmojiImageUrls,
userActivityMarkdownContent,
} from '../helpers';

describe('getCompleteImageUrls return image urls from html tags', () => {
Expand Down Expand Up @@ -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 <a class="lightbox" href="https://image.jpeg" data-download-href="https://image" title="exampleImage1">[exampleImage1]</a>';
const contentImageSrc =
'<img src="https://wiki.kfox.io/uploads/default/original.jpeg" alt="download" width="276" height="183">';

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 <img src="https://image/heart.png?v=12" title=":heart:" class="emoji" alt=":heart:" loading="lazy" width="20" height="20">';

expect(userActivityMarkdownContent(emojiContent)).toEqual(
'Hello\n ![emoji-:heart:](https://image/heart.png?v=12)',
);
});
it('it should convert mention', () => {
const mentionContent =
'Is this true? <a class="mention" href="/u/marcello">@marcello</a>';

expect(userActivityMarkdownContent(mentionContent)).toEqual(
'Is this true? @marcello',
);
});
it('it should convert Link', () => {
const mentionContent =
'<a href="https://www.google.com" rel="noopener nofollow ugc">Hello</a>';

expect(userActivityMarkdownContent(mentionContent)).toEqual(
'[Hello](https://www.google.com)',
);
});
});
43 changes: 43 additions & 0 deletions api/src/helpers/processRawContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const emojiBBCodeRegex = /(?<=^|\s):\w+:(?:t\d+:)?/g;
const emojiImageTagRegex = /<img.*?class="emoji.*? alt="(.*?)">/g;
const emojiTitleRegex = /title="([^"]+)"/g;

const userActivityContentRegex =
/(?:<img[^>]*src(?:set)?="(.+?)"(?:[^>]*title="([^"]*)")?(?:[^>]*class="([^"]*)")?[^>]*>)|(?:<a[^>]* href="((https?:)?\/\/[^ ]*\.(?:jpe?g|png|gif|heic|heif|mov|mp4|webm|avi|wmv|flv|webp))"([^>]*?)title="([^"]*)"\s*>(\[.*?\])?<\/a>)|(?:<a[^>]* class="mention" href="\/u\/([^"]+)">@(.*?)<\/a>)|(?:<a[^>]* href="([^"]+)"[^>]*>(.*?)<\/a>)/g;

function handleRegexResult(
result: RegExpMatchArray,
host: string,
Expand Down Expand Up @@ -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;
}
16 changes: 13 additions & 3 deletions api/src/resolvers/site/aboutQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions api/src/typeSchemas/UserActions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { objectType } from 'nexus';

import { getNormalizedUrlTemplate } from '../resolvers/utils';
import { userActivityMarkdownContent } from '../helpers';

export let UserActions = objectType({
name: 'UserActions',
Expand Down Expand Up @@ -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);
},
});
},
});
1 change: 1 addition & 0 deletions frontend/src/components/CustomFlatList/CustomFlatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ function BaseCustomFlatList<ItemType>(
<RefreshControl
refreshing={refreshing ?? false}
onRefresh={internalOnRefresh}
tintColor={refreshControlTintColor}
/>
}
onEndReached={(info) => {
Expand Down
43 changes: 38 additions & 5 deletions frontend/src/components/LoadingOrError.tsx
Original file line number Diff line number Diff line change
@@ -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<RefreshControlProps, 'progressViewOffset' | 'onRefresh'> & {
message?: string;
loading?: boolean;
style?: StyleProp<ViewStyle>;
refreshing?: boolean;
};

export function LoadingOrError(props: Props) {
Expand All @@ -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 (
<View style={styles.container}>
return onRefresh ? (
<ScrollView
contentContainerStyle={[styles.scrollViewContentStyle, style]}
refreshControl={
<RefreshControl
refreshing={refreshing || false}
onRefresh={onRefresh}
tintColor={colors.primary}
progressViewOffset={progressViewOffset}
/>
}
>
<View style={styles.container}>
{loading ? <ActivityIndicator size="small" /> : null}
<Text>{message}</Text>
</View>
</ScrollView>
) : (
<View style={[styles.container, style]}>
{loading ? <ActivityIndicator size="small" /> : null}
<Text>{message}</Text>
</View>
Expand All @@ -58,4 +90,5 @@ const useStyles = makeStyles(() => ({
justifyContent: 'center',
width: '100%',
},
scrollViewContentStyle: { flex: 1 },
}));
24 changes: 13 additions & 11 deletions frontend/src/components/Metrics/Metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 (
<MetricsView
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/PostItem/PostItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TouchableOpacity, View, ViewProps } from 'react-native';

import { NO_EXCERPT_WORDING } from '../../constants';
import { CustomImage, Text } from '../../core-ui';
import { formatRelativeTime, useStorage } from '../../helpers';
import { formatRelativeTime, unescapeHTML, useStorage } from '../../helpers';
import { Color, makeStyles, useTheme } from '../../theme';
import { Channel, StackNavProp } from '../../types';
import { Author } from '../Author';
Expand Down Expand Up @@ -139,7 +139,7 @@ function BasePostItem(props: Props) {
color={isTapToView ? 'primary' : color}
variant={isTapToView ? 'bold' : 'normal'}
>
{content}
{unescapeHTML(content)}
</Text>
)}
</>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/PostItem/UserInformationPostItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/PostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function PostList<T extends ItemType>(props: Props<T>) {
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.primary}
tintColor={colors.loading}
progressViewOffset={progressViewOffset}
/>
}
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/constants/theme/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const BASE_COLORS = {
lightGray: '#F2F2F2',
mustardYellow: '#CC9619',
lightYellow: '#FAF4E7',

squidInk: '#262A31',
approxBlackRussian: '#1C1F24',
lightSilver: '#D8D8D8',
};

export const FUNCTIONAL_COLORS = {
Expand Down Expand Up @@ -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,
};
2 changes: 1 addition & 1 deletion frontend/src/core-ui/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <BaseActivityIndicator color={indicatorColor} {...otherProps} />;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/graphql/server/userActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const USER_ACTIONS_FRAGMENT = gql`
topicId
username
hidden
markdownContent
}
`;

Expand Down
9 changes: 9 additions & 0 deletions frontend/src/helpers/__tests__/unescapeHTML.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { unescapeHTML } from '../unescapeHTML';

it('should unescape HTML characters', () => {
expect(
unescapeHTML(
`&lt;test&gt;&#39;hello&#39; &amp; &quot;world&quot;&hellip;&lt;/test&gt;`,
),
).toEqual(`<test>'hello' & "world"...</test>`);
});
1 change: 1 addition & 0 deletions frontend/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ export * from './checkImageFile';
export * from './PrivateTopicAlert';
export * from './PushNotificationsSetupFailAlert';
export * from './parser';
export * from './unescapeHTML';
Loading