Gekidou fixes (#6274)
* Add error boundry around latex to avoid crash * Do not update channel display name when displayName is empty * Show Muted unread channel as muted unless it has mentions * Video size to wrapperWidth if thumbnail is not available * Hide Threads button in channel list when only unreads filter if it does not have any unreads or mentions * Ensure channel_item muted state is bolded when unread * Bump network default retry to 3 attempts * Sort only unreads by last root post if CRT is enabled * Default canPost to true if permission is not found * rename to ErrorBoundary (typo fix) * simplify filterAndSortMyChannels * Fix channel name collision with mention badges
This commit is contained in:
parent
2323f4aa31
commit
65366e53a9
17 changed files with 241 additions and 76 deletions
|
|
@ -410,7 +410,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
|
|||
}
|
||||
}
|
||||
|
||||
if (channel.displayName !== newDisplayName) {
|
||||
if (newDisplayName && channel.displayName !== newDisplayName) {
|
||||
channel.prepareUpdate((c) => {
|
||||
c.displayName = extractChannelDisplayName({
|
||||
type: c.type,
|
||||
|
|
|
|||
|
|
@ -93,12 +93,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
|||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingHorizontal": 12,
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 20,
|
||||
},
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.display_name"
|
||||
|
|
@ -204,12 +206,14 @@ exports[`components/channel_list/categories/body/channel_item should match snaps
|
|||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingHorizontal": 12,
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 20,
|
||||
},
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
}
|
||||
testID="channel_list_item.hello.display_name"
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
text: {
|
||||
marginTop: -1,
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
paddingHorizontal: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 20,
|
||||
},
|
||||
highlight: {
|
||||
color: theme.sidebarText,
|
||||
|
|
@ -68,6 +69,9 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
muted: {
|
||||
color: changeOpacity(theme.sidebarText, 0.32),
|
||||
},
|
||||
mutedInfo: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.32),
|
||||
},
|
||||
badge: {
|
||||
borderColor: theme.sidebarBg,
|
||||
position: 'relative',
|
||||
|
|
@ -99,6 +103,9 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
marginTop: 4,
|
||||
...typography('Body', 75),
|
||||
},
|
||||
teamNameMuted: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.32),
|
||||
},
|
||||
teamNameTablet: {
|
||||
marginLeft: -12,
|
||||
paddingLeft: 0,
|
||||
|
|
@ -109,7 +116,7 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
}));
|
||||
|
||||
export const textStyle = StyleSheet.create({
|
||||
bright: typography('Body', 200, 'SemiBold'),
|
||||
bold: typography('Body', 200, 'SemiBold'),
|
||||
regular: typography('Body', 200, 'Regular'),
|
||||
});
|
||||
|
||||
|
|
@ -122,8 +129,8 @@ const ChannelListItem = ({
|
|||
const isTablet = useIsTablet();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
// Make it brighter if it's not muted, and highlighted or has unreads
|
||||
const isBright = isUnread || mentionsCount > 0;
|
||||
// Make it bolded if it has unreads or mentions
|
||||
const isBolded = isUnread || mentionsCount > 0;
|
||||
|
||||
const height = useMemo(() => {
|
||||
let h = 40;
|
||||
|
|
@ -138,13 +145,14 @@ const ChannelListItem = ({
|
|||
}, [channel.id]);
|
||||
|
||||
const textStyles = useMemo(() => [
|
||||
isBright ? textStyle.bright : textStyle.regular,
|
||||
isBolded ? textStyle.bold : textStyle.regular,
|
||||
styles.text,
|
||||
isBright && styles.highlight,
|
||||
isMuted && styles.muted,
|
||||
isBolded && styles.highlight,
|
||||
isActive && isTablet && !isInfo ? styles.textActive : null,
|
||||
isInfo ? styles.textInfo : null,
|
||||
], [isBright, styles, isMuted, isActive, isInfo, isTablet]);
|
||||
isMuted && styles.muted,
|
||||
isMuted && isInfo && styles.infoMuted,
|
||||
], [isBolded, styles, isMuted, isActive, isInfo, isTablet]);
|
||||
|
||||
const containerStyle = useMemo(() => [
|
||||
styles.container,
|
||||
|
|
@ -178,7 +186,7 @@ const ChannelListItem = ({
|
|||
hasDraft={hasDraft}
|
||||
isActive={isInfo ? false : isTablet && isActive}
|
||||
isInfo={isInfo}
|
||||
isUnread={isBright}
|
||||
isUnread={isBolded}
|
||||
isArchived={channel.deleteAt > 0}
|
||||
membersCount={membersCount}
|
||||
name={channel.name}
|
||||
|
|
@ -201,7 +209,7 @@ const ChannelListItem = ({
|
|||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
testID={`${testID}.${teamDisplayName}.display_name`}
|
||||
style={styles.teamName}
|
||||
style={[styles.teamName, isMuted && styles.teamNameMuted]}
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
|
|
@ -218,7 +226,7 @@ const ChannelListItem = ({
|
|||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
testID={`${testID}.${teamDisplayName}.display_name`}
|
||||
style={[styles.teamName, styles.teamNameTablet]}
|
||||
style={[styles.teamName, styles.teamNameTablet, isMuted && styles.teamNameMuted]}
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Text>
|
||||
|
|
|
|||
58
app/components/markdown/error_boundary.tsx
Normal file
58
app/components/markdown/error_boundary.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type State = {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: JSX.Element;
|
||||
error: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
error: {
|
||||
color: theme.errorTextColor,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
fontStyle: 'italic',
|
||||
...typography('Body', 100),
|
||||
},
|
||||
}));
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props, State, any> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {hasError: false};
|
||||
}
|
||||
|
||||
componentDidCatch() {
|
||||
this.setState({hasError: true});
|
||||
}
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const {children, error, theme} = this.props;
|
||||
const {hasError} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<Text style={style.error}>
|
||||
{error}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
|
@ -10,6 +10,7 @@ import MathView from 'react-native-math-view';
|
|||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ErrorBoundary from '@components/markdown/error_boundary';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Screens} from '@constants';
|
||||
|
|
@ -194,21 +195,26 @@ const LatexCodeBlock = ({content, theme}: Props) => {
|
|||
type={'opacity'}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.rightColumn}>
|
||||
{split.lines?.map((latexCode) => (
|
||||
<View
|
||||
style={styles.code}
|
||||
key={latexCode}
|
||||
>
|
||||
<MathView
|
||||
math={latexCode}
|
||||
renderError={onRenderErrorMessage}
|
||||
resizeMode={'cover'}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{plusMoreLines}
|
||||
</View>
|
||||
<ErrorBoundary
|
||||
error={intl.formatMessage({id: 'markdown.latex.error', defaultMessage: 'Latex render error'})}
|
||||
theme={theme}
|
||||
>
|
||||
<View style={styles.rightColumn}>
|
||||
{split.lines?.map((latexCode) => (
|
||||
<View
|
||||
style={styles.code}
|
||||
key={latexCode}
|
||||
>
|
||||
<MathView
|
||||
math={latexCode}
|
||||
renderError={onRenderErrorMessage}
|
||||
resizeMode={'cover'}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{plusMoreLines}
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
<View style={styles.language}>
|
||||
<Text style={styles.languageText}>
|
||||
{languageDisplayName}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform, Text, View} from 'react-native';
|
||||
import MathView from 'react-native-math-view';
|
||||
|
||||
import ErrorBoundary from '@components/markdown/error_boundary';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
|
|
@ -27,32 +30,41 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
flexWrap: 'wrap',
|
||||
},
|
||||
errorText: {
|
||||
flexDirection: 'row',
|
||||
color: theme.errorTextColor,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
fontStyle: 'italic',
|
||||
...typography('Body', 100),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const LatexInline = ({content, maxMathWidth, theme}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const onRenderErrorMessage = (errorMsg: MathViewErrorProps) => {
|
||||
return <Text style={style.errorText}>{'Latex render error: ' + errorMsg.error.message}</Text>;
|
||||
const error = formatMessage({id: 'markdown.latex.error', defaultMessage: 'Latex render error'});
|
||||
return <Text style={style.errorText}>{`${error}: ${errorMsg.error.message}`}</Text>;
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style.viewStyle}
|
||||
key={content}
|
||||
<ErrorBoundary
|
||||
error={formatMessage({id: 'markdown.latex.error', defaultMessage: 'Latex render error'})}
|
||||
theme={theme}
|
||||
>
|
||||
<MathView
|
||||
style={[style.mathStyle, {maxWidth: maxMathWidth || '100%'}]}
|
||||
math={content}
|
||||
renderError={onRenderErrorMessage}
|
||||
resizeMode='contain'
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={style.viewStyle}
|
||||
key={content}
|
||||
>
|
||||
<MathView
|
||||
style={[style.mathStyle, {maxWidth: maxMathWidth || '100%'}]}
|
||||
math={content}
|
||||
renderError={onRenderErrorMessage}
|
||||
resizeMode='contain'
|
||||
/>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
|
|||
switchMap((id) => observeChannel(database, id!)),
|
||||
);
|
||||
|
||||
const canPost = combineLatest([channel, currentUser]).pipe(switchMap(([c, u]) => (c && u ? observePermissionForChannel(c, u, Permissions.CREATE_POST, false) : of$(false))));
|
||||
const canPost = combineLatest([channel, currentUser]).pipe(switchMap(([c, u]) => (c && u ? observePermissionForChannel(c, u, Permissions.CREATE_POST, true) : of$(true))));
|
||||
const channelIsArchived = channel.pipe(switchMap((c) => (ownProps.channelIsArchived ? of$(true) : of$(c?.deleteAt !== 0))));
|
||||
|
||||
const experimentalTownSquareIsReadOnly = observeConfigBooleanValue(database, 'ExperimentalTownSquareIsReadOnly');
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
filesForGallery.value[idx] = file;
|
||||
};
|
||||
|
||||
const isSingleImage = () => (filesInfo.length === 1 && isImage(filesInfo[0]));
|
||||
const isSingleImage = () => (filesInfo.length === 1 && (isImage(filesInfo[0]) || isVideo(filesInfo[0])));
|
||||
|
||||
const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false) => {
|
||||
const singleImage = isSingleImage();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ type Props = {
|
|||
inViewPort?: boolean;
|
||||
isSingleImage?: boolean;
|
||||
resizeMode?: ResizeMode;
|
||||
wrapperWidth?: number;
|
||||
wrapperWidth: number;
|
||||
updateFileForGallery: (idx: number, file: FileInfo) => void;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ const VideoFile = ({
|
|||
const imageDimensions = useMemo(() => {
|
||||
if (isSingleImage) {
|
||||
const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45;
|
||||
return calculateDimensions(video.height, video.width, wrapperWidth, viewPortHeight);
|
||||
return calculateDimensions(video.height || wrapperWidth, video.width || wrapperWidth, wrapperWidth, viewPortHeight);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
@ -106,6 +106,10 @@ const VideoFile = ({
|
|||
setVideo(data);
|
||||
}
|
||||
} finally {
|
||||
if (!data.width) {
|
||||
data.height = wrapperWidth;
|
||||
data.width = wrapperWidth;
|
||||
}
|
||||
const {width: tw, height: th} = calculateDimensions(
|
||||
data.height,
|
||||
data.width,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class NetworkManager {
|
|||
},
|
||||
retryPolicyConfiguration: {
|
||||
type: RetryTypes.EXPONENTIAL_RETRY,
|
||||
retryLimit: 2,
|
||||
retryLimit: 3,
|
||||
exponentialBackoffBase: 2,
|
||||
exponentialBackoffScale: 0.5,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {queryPreferencesByCategoryAndName} from './preference';
|
|||
import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
||||
import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread';
|
||||
|
||||
const {SERVER: {POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD}} = MM_TABLES;
|
||||
const {SERVER: {CHANNEL, POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD}} = MM_TABLES;
|
||||
|
||||
export const prepareDeletePost = async (post: PostModel): Promise<Model[]> => {
|
||||
const preparedModels: Model[] = [post.prepareDestroyPermanently()];
|
||||
|
|
@ -178,3 +178,30 @@ export const queryPostsBetween = (database: Database, earliest: number, latest:
|
|||
}
|
||||
return database.get<PostModel>(POST).query(...clauses);
|
||||
};
|
||||
|
||||
export const observeLastPostAtPerChannelByTeam = (database: Database, teamId: string) => {
|
||||
return database.get<PostInChannelModel>(POSTS_IN_CHANNEL).query(
|
||||
Q.on(CHANNEL,
|
||||
Q.and(
|
||||
Q.or(
|
||||
Q.where('team_id', Q.eq(teamId)),
|
||||
Q.where('team_id', Q.eq('')),
|
||||
),
|
||||
Q.where('delete_at', Q.eq(0)),
|
||||
),
|
||||
),
|
||||
Q.sortBy('latest', Q.desc),
|
||||
).observeWithColumns(['latest']).pipe(
|
||||
switchMap((pics) => {
|
||||
return of$(pics.reduce<Record<string, number>>((result, pic) => {
|
||||
const id = pic.channelId;
|
||||
if (result[id]) {
|
||||
result[id] = Math.max(result[id], pic.latest);
|
||||
} else {
|
||||
result[id] = pic.latest;
|
||||
}
|
||||
return result;
|
||||
}, {}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import {combineLatestWith, concatAll, map, switchMap} from 'rxjs/operators';
|
|||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {getChannelById, observeAllMyChannelNotifyProps, queryMyChannelUnreads} from '@queries/servers/channel';
|
||||
import {observeLastPostAtPerChannelByTeam} from '@queries/servers/post';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeLastUnreadChannelId} from '@queries/servers/system';
|
||||
import {observeIsCRTEnabled} from '@queries/servers/thread';
|
||||
|
||||
import {getChannelsFromRelation} from '../body';
|
||||
|
||||
|
|
@ -40,7 +42,10 @@ type NotifyProps = {
|
|||
[key: string]: Partial<ChannelNotifyProps>;
|
||||
}
|
||||
|
||||
const mostRecentFirst = (a: MyChannelModel, b: MyChannelModel) => {
|
||||
const mostRecentFirst = (lastPostAtPerChannel: Record<string, number> | undefined, a: MyChannelModel, b: MyChannelModel) => {
|
||||
if (lastPostAtPerChannel && lastPostAtPerChannel[a.id] && lastPostAtPerChannel[b.id]) {
|
||||
return lastPostAtPerChannel[b.id] - lastPostAtPerChannel[a.id];
|
||||
}
|
||||
return b.lastPostAt - a.lastPostAt;
|
||||
};
|
||||
|
||||
|
|
@ -50,10 +55,16 @@ const mostRecentFirst = (a: MyChannelModel, b: MyChannelModel) => {
|
|||
* Unreads, Mentions, and Muted Mentions Only
|
||||
*
|
||||
* Mentions on top, then unreads, then muted channels with mentions.
|
||||
* Secondary sorting within each of those is by recent posting.
|
||||
* Secondary sorting within each of those is by recent posting or by recent root post if CRT is enabled.
|
||||
*/
|
||||
|
||||
const filterAndSortMyChannels = ([myChannels, notifyProps]: [MyChannelModel[], NotifyProps]): MyChannelModel[] => {
|
||||
type FilterAndSortMyChannelsArgs = [
|
||||
MyChannelModel[],
|
||||
NotifyProps,
|
||||
Record<string, number> | undefined
|
||||
]
|
||||
|
||||
const filterAndSortMyChannels = ([myChannels, notifyProps, lastPostAtPerChannel]: FilterAndSortMyChannelsArgs): MyChannelModel[] => {
|
||||
const mentions: MyChannelModel[] = [];
|
||||
const unreads: MyChannelModel[] = [];
|
||||
const mutedMentions: MyChannelModel[] = [];
|
||||
|
|
@ -63,8 +74,10 @@ const filterAndSortMyChannels = ([myChannels, notifyProps]: [MyChannelModel[], N
|
|||
};
|
||||
|
||||
for (const myChannel of myChannels) {
|
||||
const id = myChannel.id;
|
||||
|
||||
// is it a mention?
|
||||
if (!isMuted(myChannel.id) && myChannel.mentionsCount > 0) {
|
||||
if (!isMuted(id) && myChannel.mentionsCount > 0) {
|
||||
mentions.push(myChannel);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -83,9 +96,9 @@ const filterAndSortMyChannels = ([myChannels, notifyProps]: [MyChannelModel[], N
|
|||
}
|
||||
|
||||
// Sort
|
||||
mentions.sort(mostRecentFirst);
|
||||
unreads.sort(mostRecentFirst);
|
||||
mutedMentions.sort(mostRecentFirst);
|
||||
mentions.sort(mostRecentFirst.bind(null, lastPostAtPerChannel));
|
||||
unreads.sort(mostRecentFirst.bind(null, lastPostAtPerChannel));
|
||||
mutedMentions.sort(mostRecentFirst.bind(null, lastPostAtPerChannel));
|
||||
|
||||
return [...mentions, ...unreads, ...mutedMentions];
|
||||
};
|
||||
|
|
@ -105,9 +118,13 @@ const enhanced = withObservables(['currentTeamId', 'isTablet', 'onlyUnreads'], (
|
|||
switchMap(getC),
|
||||
) : of$('');
|
||||
const notifyProps = observeAllMyChannelNotifyProps(database);
|
||||
const crt = observeIsCRTEnabled(database);
|
||||
const lastPostInChannel = crt.pipe(
|
||||
switchMap((enabled) => (enabled && onlyUnreads ? observeLastPostAtPerChannelByTeam(database, currentTeamId) : of$(undefined))),
|
||||
);
|
||||
|
||||
const unreads = queryMyChannelUnreads(database, currentTeamId).observe().pipe(
|
||||
combineLatestWith(notifyProps),
|
||||
const unreads = queryMyChannelUnreads(database, currentTeamId).observeWithColumns(['last_post_at']).pipe(
|
||||
combineLatestWith(notifyProps, lastPostInChannel),
|
||||
map(filterAndSortMyChannels),
|
||||
map(getChannelsFromRelation),
|
||||
concatAll(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Threads Component should match snapshot 1`] = `
|
||||
exports[`Thread item in the channel list Threads Component should match snapshot 1`] = `
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
|
|
@ -67,7 +67,8 @@ exports[`Threads Component should match snapshot 1`] = `
|
|||
Object {
|
||||
"color": "rgba(255,255,255,0.72)",
|
||||
"marginTop": -1,
|
||||
"paddingHorizontal": 12,
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 20,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
|
|
@ -80,3 +81,5 @@ exports[`Threads Component should match snapshot 1`] = `
|
|||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`Thread item in the channel list Threads Component should match snapshot with only unreads filter 1`] = `null`;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
|||
import withObservables from '@nozbe/with-observables';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeCurrentChannelId, observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {observeCurrentChannelId, observeCurrentTeamId, observeOnlyUnreads} from '@queries/servers/system';
|
||||
import {observeUnreadsAndMentionsInTeam} from '@queries/servers/thread';
|
||||
|
||||
import ThreadsButton from './threads_button';
|
||||
|
|
@ -17,6 +17,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
|||
|
||||
return {
|
||||
currentChannelId: observeCurrentChannelId(database),
|
||||
onlyUnreads: observeOnlyUnreads(database),
|
||||
unreadsAndMentions: currentTeamId.pipe(
|
||||
switchMap(
|
||||
(teamId) => observeUnreadsAndMentionsInTeam(database, teamId),
|
||||
|
|
|
|||
|
|
@ -7,16 +7,34 @@ import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
|||
|
||||
import Threads from './threads_button';
|
||||
|
||||
test('Threads Component should match snapshot', () => {
|
||||
const {toJSON} = renderWithIntlAndTheme(
|
||||
<Threads
|
||||
currentChannelId='someChannelId'
|
||||
unreadsAndMentions={{
|
||||
unreads: 0,
|
||||
mentions: 0,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
describe('Thread item in the channel list', () => {
|
||||
test('Threads Component should match snapshot', () => {
|
||||
const {toJSON} = renderWithIntlAndTheme(
|
||||
<Threads
|
||||
currentChannelId='someChannelId'
|
||||
onlyUnreads={false}
|
||||
unreadsAndMentions={{
|
||||
unreads: 0,
|
||||
mentions: 0,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Threads Component should match snapshot with only unreads filter', () => {
|
||||
const {toJSON} = renderWithIntlAndTheme(
|
||||
<Threads
|
||||
currentChannelId='someChannelId'
|
||||
onlyUnreads={true}
|
||||
unreadsAndMentions={{
|
||||
unreads: 0,
|
||||
mentions: 0,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,13 +37,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
|
||||
type Props = {
|
||||
currentChannelId: string;
|
||||
onlyUnreads: boolean;
|
||||
unreadsAndMentions: {
|
||||
unreads: number;
|
||||
mentions: number;
|
||||
};
|
||||
};
|
||||
|
||||
const ThreadsButton = ({currentChannelId, unreadsAndMentions}: Props) => {
|
||||
const ThreadsButton = ({currentChannelId, onlyUnreads, unreadsAndMentions}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ const ThreadsButton = ({currentChannelId, unreadsAndMentions}: Props) => {
|
|||
|
||||
const text = [
|
||||
customStyles.text,
|
||||
unreads ? channelItemTextStyle.bright : channelItemTextStyle.regular,
|
||||
unreads ? channelItemTextStyle.bold : channelItemTextStyle.regular,
|
||||
styles.text,
|
||||
unreads ? styles.highlight : undefined,
|
||||
isActive ? styles.textActive : undefined,
|
||||
|
|
@ -80,6 +81,10 @@ const ThreadsButton = ({currentChannelId, unreadsAndMentions}: Props) => {
|
|||
return [container, icon, text];
|
||||
}, [customStyles, isActive, styles, unreads]);
|
||||
|
||||
if (onlyUnreads && !unreads && !mentions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@
|
|||
"login.signIn": "Log In",
|
||||
"login.signingIn": "Logging In",
|
||||
"login.username": "Username",
|
||||
"markdown.latex.error": "Latex render error",
|
||||
"mentions.empty.paragraph": "You'll see messages here when someone mentions you or uses terms you're monitoring.",
|
||||
"mentions.empty.title": "No Mentions yet",
|
||||
"mobile.about.appVersion": "App Version: {version} (Build {number})",
|
||||
|
|
@ -286,6 +287,7 @@
|
|||
"mobile.action_menu.select": "Select an option",
|
||||
"mobile.add_team.create_team": "Create a new team",
|
||||
"mobile.add_team.join_team": "Join Another Team",
|
||||
"mobile.android.back_handler_exit": "Press back again to exit",
|
||||
"mobile.android.photos_permission_denied_description": "Upload photos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo library.",
|
||||
"mobile.android.photos_permission_denied_title": "{applicationName} would like to access your photos",
|
||||
"mobile.camera_photo_permission_denied_description": "Take photos and upload them to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your camera.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue