Unverified Commit 4e38d4c5 authored by Diego Mello's avatar Diego Mello Committed by GitHub
Browse files

Merge 4.16.0 into master (#3053)



* [CHORE] Remove Google Services files from repo (#2405)

* Android

* iOS

* [FIX] Fix broken StatusView on tablet (#2407)
Signed-off-by: default avatarEzequiel De Oliveira <ezequiel1de1oliveira@gmail.com>

* [FIX] REST for method calls not raising errors (#2408)

* [FIX] REST for Method calls not raising erorrs

* Remove unnecessary lint disable

* [NEW] Encrypt user credentials and preferences (#2247)

* install react-native-mmkv-storage

* wip ios migration

* change all js rn-user-defaults -> react-native-mmkv-storage

* remove all rn-user-defaults native references (iOS)

* android migration from rn-user-defaults to react-native-mmkv-storage

* ios app group accessible mmkv

* handle get errors

* remove access of credentials from legacy native apps

* remove data of user defaults

* remove no longer necessary import

* js mmkv encryption

* run migration only once

* reply from notification android

* fix app group key access at native...
parent f5b013f4
......@@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.15.0"
versionName "4.16.0"
vectorDrawables.useSupportLibrary = true
if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
......
......@@ -2,7 +2,8 @@ export const STATUS_COLORS = {
online: '#2de0a5',
busy: '#f5455c',
away: '#ffd21f',
offline: '#cbced1'
offline: '#cbced1',
loading: '#9ea2a8'
};
export const SWITCH_TRACK_COLOR = {
......
import React from 'react';
import {
ImageBackground, StyleSheet, Text, View
ImageBackground, StyleSheet, Text, View, ActivityIndicator
} from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme';
import sharedStyles from '../Styles';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
const styles = StyleSheet.create({
......@@ -29,15 +29,17 @@ const styles = StyleSheet.create({
}
});
const EmptyRoom = ({ theme, text }) => (
const BackgroundContainer = ({ theme, text, loading }) => (
<View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${ theme }` }} style={styles.image} />
<Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text>
{text ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={[styles.text, { color: themes[theme].auxiliaryTintColor }]} /> : null}
</View>
);
EmptyRoom.propTypes = {
BackgroundContainer.propTypes = {
text: PropTypes.string,
theme: PropTypes.string
theme: PropTypes.string,
loading: PropTypes.bool
};
export default withTheme(EmptyRoom);
export default withTheme(BackgroundContainer);
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types */
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import BackgroundContainer from '.';
import { ThemeContext } from '../../theme';
import { longText } from '../../../storybook/utils';
const stories = storiesOf('BackgroundContainer', module);
stories.add('basic', () => (
<BackgroundContainer />
));
stories.add('loading', () => (
<BackgroundContainer loading />
));
stories.add('text', () => (
<BackgroundContainer text='Text here' />
));
stories.add('long text', () => (
<BackgroundContainer text={longText} />
));
const ThemeStory = ({ theme, ...props }) => (
<ThemeContext.Provider
value={{ theme }}
>
<BackgroundContainer {...props} />
</ThemeContext.Provider>
);
stories.add('dark theme - loading', () => (
<ThemeStory theme='dark' loading />
));
stories.add('dark theme - text', () => (
<ThemeStory theme='dark' text={longText} />
));
stories.add('black theme - loading', () => (
<ThemeStory theme='black' loading />
));
stories.add('black theme - text', () => (
<ThemeStory theme='black' text={longText} />
));
......@@ -23,7 +23,7 @@ export const getHeaderHeight = (isLandscape) => {
export const getHeaderTitlePosition = ({ insets, numIconsRight }) => ({
left: insets.left + 60,
right: insets.right + (45 * numIconsRight)
right: insets.right + Math.max(45 * numIconsRight, 15)
});
const styles = StyleSheet.create({
......
......@@ -18,7 +18,7 @@ const Mentions = React.memo(({ mentions, trackingType, theme }) => {
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={item => item.id || item.username || item.command || item}
keyExtractor={item => item.rid || item.name || item.command || item}
keyboardShouldPersistTaps='always'
/>
</View>
......
export const MENTIONS_TRACKING_TYPE_USERS = '@';
export const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
export const MENTIONS_COUNT_TO_DISPLAY = 4;
......@@ -41,7 +41,8 @@ import {
MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_COMMANDS,
MENTIONS_COUNT_TO_DISPLAY,
MENTIONS_TRACKING_TYPE_USERS
MENTIONS_TRACKING_TYPE_USERS,
MENTIONS_TRACKING_TYPE_ROOMS
} from './constants';
import CommandsPreview from './CommandsPreview';
import { getUserSelector } from '../../selectors/login';
......@@ -354,58 +355,48 @@ class MessageBox extends Component {
// eslint-disable-next-line react/sort-comp
debouncedOnChangeText = debounce(async(text) => {
const { sharing } = this.props;
const db = database.active;
const isTextEmpty = text.length === 0;
// this.setShowSend(!isTextEmpty);
if (isTextEmpty) {
this.stopTrackingMention();
return;
}
this.handleTyping(!isTextEmpty);
if (!sharing) {
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
if (slashCommand) {
const [, name, params] = slashCommand;
const { start, end } = this.selection;
const cursor = Math.max(start, end);
const txt = cursor < text.length ? text.substr(0, cursor).split(' ') : text.split(' ');
const lastWord = txt[txt.length - 1];
const result = lastWord.substring(1);
const commandMention = text.match(/^\//); // match only if message begins with /
const channelMention = lastWord.match(/^#/);
const userMention = lastWord.match(/^@/);
const emojiMention = lastWord.match(/^:/);
if (commandMention && !sharing) {
const command = text.substr(1);
const commandParameter = text.match(/^\/([a-z0-9._-]+) (.+)/im);
if (commandParameter) {
const db = database.active;
const [, name, params] = commandParameter;
const commandsCollection = db.get('slash_commands');
try {
const command = await commandsCollection.find(name);
if (command.providesPreview) {
return this.setCommandPreview(command, name, params);
const commandRecord = await commandsCollection.find(name);
if (commandRecord.providesPreview) {
return this.setCommandPreview(commandRecord, name, params);
}
} catch (e) {
console.log('Slash command not found');
}
}
}
if (!isTextEmpty) {
try {
const { start, end } = this.selection;
const cursor = Math.max(start, end);
const lastNativeText = this.text;
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
let regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
// if sharing, track #|@|:
if (sharing) {
regexp = /(#|@|:)([a-z0-9._-]+)$/im;
}
const result = lastNativeText.substr(0, cursor).match(regexp);
if (!result) {
if (!sharing) {
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
if (slash) {
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
}
}
return this.stopTrackingMention();
// do nothing
}
const [, lastChar, name] = result;
this.identifyMentionKeyword(name, lastChar);
} catch (e) {
log(e);
}
return this.identifyMentionKeyword(command, MENTIONS_TRACKING_TYPE_COMMANDS);
} else if (channelMention) {
return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_ROOMS);
} else if (userMention) {
return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_USERS);
} else if (emojiMention) {
return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_EMOJIS);
} else {
this.stopTrackingMention();
return this.stopTrackingMention();
}
}, 100)
......@@ -483,10 +474,10 @@ class MessageBox extends Component {
getFixedMentions = (keyword) => {
let result = [];
if ('all'.indexOf(keyword) !== -1) {
result = [{ id: -1, username: 'all' }];
result = [{ rid: -1, username: 'all' }];
}
if ('here'.indexOf(keyword) !== -1) {
result = [{ id: -2, username: 'here' }, ...result];
result = [{ rid: -2, username: 'here' }, ...result];
}
return result;
}
......@@ -504,17 +495,17 @@ class MessageBox extends Component {
getEmojis = debounce(async(keyword) => {
const db = database.active;
if (keyword) {
const customEmojisCollection = db.get('custom_emojis');
const likeString = sanitizeLikeString(keyword);
let customEmojis = await customEmojisCollection.query(
Q.where('name', Q.like(`${ likeString }%`))
).fetch();
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [] });
const customEmojisCollection = db.get('custom_emojis');
const likeString = sanitizeLikeString(keyword);
const whereClause = [];
if (likeString) {
whereClause.push(Q.where('name', Q.like(`${ likeString }%`)));
}
let customEmojis = await customEmojisCollection.query(...whereClause).fetch();
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [] });
}, 300)
getSlashCommands = debounce(async(keyword) => {
......
......@@ -4,16 +4,21 @@ import {
View, Text, StyleSheet, TouchableOpacity
} from 'react-native';
import I18n from '../../../i18n';
import sharedStyles from '../../Styles';
import Icon from './Icon';
import { themes } from '../../../constants/colors';
import Markdown from '../../../containers/markdown';
import I18n from '../../i18n';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import Markdown from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon';
import { withTheme } from '../../theme';
const HIT_SLOP = {
top: 5, right: 5, bottom: 5, left: 5
};
const TITLE_SIZE = 16;
const SUBTITLE_SIZE = 12;
const getSubTitleSize = scale => SUBTITLE_SIZE * scale;
const styles = StyleSheet.create({
container: {
flex: 1,
......@@ -24,12 +29,12 @@ const styles = StyleSheet.create({
flexDirection: 'row'
},
title: {
...sharedStyles.textSemibold,
fontSize: TITLE_SIZE
flexShrink: 1,
...sharedStyles.textSemibold
},
subtitle: {
...sharedStyles.textRegular,
fontSize: 12
flexShrink: 1,
...sharedStyles.textRegular
},
typingUsers: {
...sharedStyles.textSemibold
......@@ -37,8 +42,9 @@ const styles = StyleSheet.create({
});
const SubTitle = React.memo(({
usersTyping, subtitle, renderFunc, theme
usersTyping, subtitle, renderFunc, theme, scale
}) => {
const fontSize = getSubTitleSize(scale);
// typing
if (usersTyping.length) {
let usersText;
......@@ -48,7 +54,7 @@ const SubTitle = React.memo(({
usersText = usersTyping.join(', ');
}
return (
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
<Text style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]} numberOfLines={1}>
<Text style={styles.typingUsers}>{usersText} </Text>
{ usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
</Text>
......@@ -66,7 +72,7 @@ const SubTitle = React.memo(({
<Markdown
preview
msg={subtitle}
style={[styles.subtitle, { color: themes[theme].auxiliaryText }]}
style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]}
numberOfLines={1}
theme={theme}
/>
......@@ -80,18 +86,20 @@ SubTitle.propTypes = {
usersTyping: PropTypes.array,
theme: PropTypes.string,
subtitle: PropTypes.string,
renderFunc: PropTypes.func
renderFunc: PropTypes.func,
scale: PropTypes.number
};
const HeaderTitle = React.memo(({
title, tmid, prid, scale, theme
title, tmid, prid, scale, theme, testID
}) => {
const titleStyle = { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor };
if (!tmid && !prid) {
return (
<Text
style={[styles.title, { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor }]}
style={[styles.title, titleStyle]}
numberOfLines={1}
testID={`room-view-title-${ title }`}
testID={testID}
>
{title}
</Text>
......@@ -102,10 +110,10 @@ const HeaderTitle = React.memo(({
<Markdown
preview
msg={title}
style={[styles.title, { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor }]}
style={[styles.title, titleStyle]}
numberOfLines={1}
theme={theme}
testID={`room-view-title-${ title }`}
testID={testID}
/>
);
});
......@@ -115,11 +123,12 @@ HeaderTitle.propTypes = {
tmid: PropTypes.string,
prid: PropTypes.string,
scale: PropTypes.number,
theme: PropTypes.string
theme: PropTypes.string,
testID: PropTypes.string
};
const Header = React.memo(({
title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, roomUserId, theme
title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, onPress, theme, isGroupChat, teamMain, testID
}) => {
const portrait = height > width;
let scale = 1;
......@@ -130,19 +139,11 @@ const Header = React.memo(({
}
}
const onPress = () => goRoomActionsView();
let renderFunc;
if (tmid) {
renderFunc = () => (
<View style={styles.titleContainer}>
<Icon
type={prid ? 'discussion' : type}
tmid={tmid}
status={status}
roomUserId={roomUserId}
theme={theme}
/>
<RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} />
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{parentTitle}</Text>
</View>
);
......@@ -150,7 +151,7 @@ const Header = React.memo(({
return (
<TouchableOpacity
testID='room-view-header-actions'
testID='room-header'
accessibilityLabel={title}
onPress={onPress}
style={styles.container}
......@@ -158,17 +159,23 @@ const Header = React.memo(({
hitSlop={HIT_SLOP}
>
<View style={styles.titleContainer}>
{tmid ? null : <Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} />}
{tmid ? null : <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} />}
<HeaderTitle
title={title}
tmid={tmid}
prid={prid}
scale={scale}
connecting={connecting}
theme={theme}
testID={testID}
/>
</View>
<SubTitle usersTyping={tmid ? [] : usersTyping} subtitle={subtitle} theme={theme} renderFunc={renderFunc} />
<SubTitle
usersTyping={tmid ? [] : usersTyping}
subtitle={subtitle}
theme={theme}
renderFunc={renderFunc}
scale={scale}
/>
</TouchableOpacity>
);
});
......@@ -181,17 +188,18 @@ Header.propTypes = {
height: PropTypes.number.isRequired,
prid: PropTypes.string,
tmid: PropTypes.string,
teamMain: PropTypes.bool,
status: PropTypes.string,
theme: PropTypes.string,
usersTyping: PropTypes.array,
connecting: PropTypes.bool,
roomUserId: PropTypes.string,
isGroupChat: PropTypes.bool,
parentTitle: PropTypes.string,
goRoomActionsView: PropTypes.func
onPress: PropTypes.func,
testID: PropTypes.string
};
Header.defaultProps = {
usersTyping: []
};
export default Header;
export default withTheme(Header);
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */
import React from 'react';
import { View, Dimensions } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import RoomHeaderComponent from './RoomHeader';
import Header from '../Header';
import { longText } from '../../../storybook/utils';
import { ThemeContext } from '../../theme';
const stories = storiesOf('RoomHeader', module);
// TODO: refactor after react-navigation v6
const HeaderExample = ({ title }) => (
<Header
headerTitle={() => (
<View style={{ flex: 1, paddingHorizontal: 12 }}>
{title()}
</View>
)}
/>
);
const { width, height } = Dimensions.get('window');
const RoomHeader = ({ ...props }) => (
<RoomHeaderComponent width={width} height={height} title='title' type='p' testID={props.title} onPress={() => alert('header pressed!')} {...props} />
);
stories.add('title and subtitle', () => (
<>
<HeaderExample title={() => <RoomHeader title='title' type='p' />} />
<HeaderExample title={() => <RoomHeader title={longText} type='p' />} />
<HeaderExample title={() => <RoomHeader subtitle='subtitle' />} />
<HeaderExample title={() => <RoomHeader subtitle={longText} />} />
<HeaderExample title={() => <RoomHeader title={longText} subtitle={longText} />} />
</>
));
stories.add('icons', () => (
<>
<HeaderExample title={() => <RoomHeader title='private channel' type='p' />} />
<HeaderExample title={() => <RoomHeader title='public channel' type='c' />} />
<HeaderExample title={() => <RoomHeader title='discussion' prid='asd' />} />
<HeaderExample title={() => <RoomHeader title='omnichannel' type='l' />} />
<HeaderExample title={() => <RoomHeader title='private team' type='p' teamMain />} />
<HeaderExample title={() => <RoomHeader title='public team' type='c' teamMain />} />
<HeaderExample title={() => <RoomHeader title='group dm' type='d' isGroupChat />} />
<HeaderExample title={() => <RoomHeader title='online dm' type='d' status='online' />} />
<HeaderExample title={() => <RoomHeader title='away dm' type='d' status='away' />} />
<HeaderExample title={() => <RoomHeader title='busy dm' type='d' status='busy' />} />
<HeaderExample title={() => <RoomHeader title='loading dm' type='d' status='loading' />} />
<HeaderExample title={() => <RoomHeader title='offline dm' type='d' />} />
</>
));
stories.add('typing', () => (
<>
<HeaderExample title={() => <RoomHeader usersTyping={['user 1']} />} />
<HeaderExample title={() => <RoomHeader usersTyping={['user 1', 'user 2']} />} />
<HeaderExample title={() => <RoomHeader usersTyping={['user 1', 'user 2', 'user 3', 'user 4', 'user 5']} />} />
</>
));
stories.add('landscape', () => (
<>
<HeaderExample title={() => <RoomHeader width={height} height={width} />} />
<HeaderExample title={() => <RoomHeader width={height} height={width} subtitle='subtitle' />} />
<HeaderExample title={() => <RoomHeader width={height} height={width} title={longText} subtitle={longText} />} />
</>
));
stories.add('thread', () => (
<>
<HeaderExample title={() => <RoomHeader tmid='123' parentTitle='parent title' />} />
<HeaderExample title={() => <RoomHeader tmid='123' title={'markdown\npreview\n#3\n4\n5'} parentTitle={longText} />} />
</>
));
const ThemeStory = ({ theme }) => (
<ThemeContext.Provider
value={{ theme }}
>
<HeaderExample title={() => <RoomHeader subtitle='subtitle' />} />
</ThemeContext.Provider>
);
stories.add('themes', () => (
<>
<ThemeStory theme='light' />
<ThemeStory theme='dark' />
<ThemeStory theme='black' />
</>
));