...
 
Commits (2)
module.exports = {
"prettier.printWidth": 100,
bracketSpacing: false,
jsxBracketSameLine: true,
singleQuote: true,
semi: true,
bracketSpacing: true,
trailingComma: 'all',
};
......@@ -38,6 +38,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -98,6 +99,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -174,6 +176,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -279,6 +282,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -350,6 +354,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -417,6 +422,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -490,6 +496,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -556,6 +563,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......
......@@ -37,6 +37,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -88,6 +89,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......
......@@ -362,10 +362,12 @@ exports[`blog card component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......
......@@ -66,10 +66,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -259,10 +261,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -435,10 +439,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -590,10 +596,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -736,10 +744,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......
......@@ -81,10 +81,12 @@ exports[`Channel screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -346,10 +348,12 @@ exports[`Channel screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -606,10 +610,12 @@ exports[`Channel screen component should show closed channel message 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -892,10 +898,12 @@ exports[`Channel screen component should show closed channel message 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......
import service from '../../../src/common/services/deeplinks-router.service';
import { MINDS_DEEPLINK } from '../../../src/config/Config';
import navigationService from '../../../src/navigation/NavigationService';
import service from '../../../src/common/services/deeplinks-router.service';
/**
* Tests
......@@ -10,19 +7,43 @@ import navigationService from '../../../src/navigation/NavigationService';
describe('Deeplinks router service', () => {
navigationService.navigate = jest.fn();
beforeEach(() => {
navigationService.navigate.mockClear();
});
it('should add route', async () => {
expect(service.routes.length).toBe(12)
service.clearRoutes();
expect(service.routes.length).toBe(0);
service.add('crypto/:someparam', 'screen1');
service.add('myurl/:someparam1', 'screen2');
expect(service.routes.length).toBe(14);
service.add('twoparams/:someparam1/:someparam2', 'screen2');
expect(service.routes.length).toBe(3);
service.navigate('http://www.minds.com/crypto/somevalue');
expect(navigationService.navigate).toHaveBeenCalledWith('screen1', {someparam: 'somevalue'});
expect(navigationService.navigate).toHaveBeenCalledWith('screen1', {
someparam: 'somevalue',
});
service.navigate('http://www.minds.com/myurl/somevalue');
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {someparam1: 'somevalue'});
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
});
service.navigate('http://www.minds.com/twoparams/somevalue/somevalue1');
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
someparam2: 'somevalue1',
});
service.navigate(
'http://www.minds.com/twoparams/somevalue/somevalue1?other=1&other2=2',
);
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
someparam2: 'somevalue1',
other: '1',
other2: '2',
});
service.navigate('http://www.minds.com/myurl/somevalue?other=1');
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
other: '1',
});
});
});
\ No newline at end of file
});
......@@ -50,12 +50,13 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="mobile.minds.com" android:path="/"/>
<data android:pathPattern="/channels/..*"/>
android:host="www.minds.com" android:path="/"/>
<data android:pathPattern="/email-confirmation"/>
<!-- <data android:pathPattern="/channels/..*"/>
<data android:pathPattern="/newsfeed/..*"/>
<data android:pathPattern="/groups/profile/..*"/>
<data android:pathPattern="/wallet/..*"/>
<data android:pathPattern="/..*"/>
<data android:pathPattern="/..*"/> -->
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
......
......@@ -6,8 +6,8 @@
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:mobile.minds.com</string>
<string>activitycontinuation:mobile.minds.com</string>
<string>applinks:www.minds.com</string>
<string>activitycontinuation:www.minds.com</string>
</array>
</dict>
</plist>
......@@ -602,6 +602,13 @@
"email":"Invalid email",
"number":"Invalid number"
},
"emailConfirm": {
"confirmed":"Email address confirmed!",
"confirm":"Please confirm your email address. Didn't get it?",
"sendAgain": "Click here to send again.",
"confirmNote": "Note: If you change your email address, it will need to be confirmed again.",
"sent":"Email sent"
},
"imagePicker":{
"gallery":"Choose from gallery",
"camera":"Take new photo",
......
......@@ -14,6 +14,12 @@ import { Alert } from 'react-native';
*/
class UserStore {
@observable me = {};
@observable emailConfirmMessageDismiss = false;
@action
setDissmis(value) {
this.emailConfirmMessageDismiss = value;
}
@action
setUser(user) {
......
......@@ -51,6 +51,26 @@ export default class UserModel extends BaseModel {
*/
@observable mode = 0;
/**
* @var {boolean}
*/
@observable email_confirmed = false;
/**
* Confirm email
* @param {Object} params
*/
confirmEmail = async params => {
// call any api endpoint with the param
try {
await apiService.get('api/v2/entities/', { urn: this.urn, ...params });
this.setEmailConfirmed(true);
return true;
} catch (error) {
return false;
}
};
getOwnerIcontime() {
if (sessionService.getUser().guid === this.guid) {
return sessionService.getUser().icontime;
......@@ -98,6 +118,11 @@ export default class UserModel extends BaseModel {
this.mode = value;
}
@action
setEmailConfirmed(value) {
this.email_confirmed = value;
}
/**
* Is admin
*/
......
......@@ -5,7 +5,9 @@ import navigationService from '../../navigation/NavigationService';
* Deeplinks router
*/
class DeeplinksRouter {
/**
* Routes
*/
routes = [];
/**
......@@ -15,6 +17,28 @@ class DeeplinksRouter {
MINDS_DEEPLINK.forEach(r => this.add(r[0], r[1]));
}
/**
* Clear routes
*/
clearRoutes() {
this.routes = [];
}
/**
* Parse params
* @param {string} url
*/
parseQueryParams(url) {
let regex = /[?&]([^=#]+)=([^&#]*)/g,
params = {},
match;
while ((match = regex.exec(url))) {
params[match[1]] = match[2];
}
return params;
}
/**
* Add a new route
* @param {string} url ex: newsfeed/:guid
......@@ -28,7 +52,7 @@ class DeeplinksRouter {
this.routes.push({
screen,
params,
re: new RegExp('^' + url.replace(re, '([^\/]+?)') + '\/?$')
re: new RegExp('^' + url.replace(re, '([^\/]+?)') + '(\/?$|\/?\\?)')
});
}
......@@ -63,8 +87,9 @@ class DeeplinksRouter {
if (match) {
const params = {};
route.params.forEach((v, i) => params[v] = match[i + 1]);
const urlParams = this.parseQueryParams(url);
return { screen: route.screen, params }
return { screen: route.screen, params: {...params, ...urlParams}}
}
}
return null;
......
import apiService from './api.service';
import logService from './log.service';
class EmailConfirmationService {
async send() {
try {
const response = await apiService.post(
'api/v2/email/confirmation/resend',
{},
);
return Boolean(response && response.sent);
} catch (err) {
logService.exception('[EmailConfirmationService] send', err);
return false;
}
}
}
export default new EmailConfirmationService();
......@@ -23,7 +23,7 @@ export const MINDS_URI_SETTINGS = {
export const MINDS_MAX_VIDEO_LENGTH = 5; // in minutes
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030'
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030';
export const MINDS_CDN_URI = 'https://cdn.minds.com/';
export const MINDS_ASSETS_CDN_URI = 'https://cdn-assets.minds.com/';
......@@ -45,6 +45,7 @@ export const MINDS_FEATURES = {
* Deeplink to screen/params maping
*/
export const MINDS_DEEPLINK = [
['email-confirmation', 'EmailConfirmation'],
['groups/profile/:guid/feed', 'GroupView'],
['groups/profile/:guid', 'GroupView'],
['notifications', 'Notifications'],
......@@ -59,7 +60,7 @@ export const MINDS_DEEPLINK = [
['wallet/tokens/:section', 'Wallet'],
];
export const DISABLE_PASSWORD_INPUTS = true;
export const DISABLE_PASSWORD_INPUTS = false;
// IF TRUE COMMENT THE SMS PERMISSIONS IN ANDROID MANIFEST TOO!!!
export const GOOGLE_PLAY_STORE = DeviceInfo.getBuildNumber() < 1050000000 && Platform.OS == 'android';
......@@ -23,7 +23,7 @@ export const MINDS_URI_SETTINGS = {
export const MINDS_MAX_VIDEO_LENGTH = 5; // in minutes
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030'
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030';
export const MINDS_CDN_URI = 'https://cdn.minds.com/';
export const MINDS_ASSETS_CDN_URI = 'https://cdn-assets.minds.com/';
......@@ -45,6 +45,7 @@ export const MINDS_FEATURES = {
* Deeplink to screen/params maping
*/
export const MINDS_DEEPLINK = [
['email-confirmation', 'EmailConfirmation'],
['groups/profile/:guid/feed', 'GroupView'],
['groups/profile/:guid', 'GroupView'],
['notifications', 'Notifications'],
......
......@@ -49,6 +49,7 @@ import {withErrorBoundaryScreen} from '../common/components/ErrorBoundary';
import DeleteChannelScreen from '../settings/screens/DeleteChannelScreen';
import DiscoveryFeedScreen from '../discovery/DiscoveryFeedScreen';
import Gathering from '../gathering/Gathering';
import EmailConfirmationScreen from '../onboarding/EmailConfirmationScreen';
/**
* Main stack navigator
......@@ -57,6 +58,9 @@ const Stack = createStackNavigator({
Tabs: {
screen: withErrorBoundaryScreen(TabsScreen),
},
EmailConfirmation: {
screen: withErrorBoundaryScreen(EmailConfirmationScreen)
},
// Logs: {
// screen: LogsScreen
// },
......
......@@ -74,7 +74,6 @@ export default class Activity extends Component {
if (this.props.entity.listRef) {
const offsetToScrollTo = this.props.entity._list.scrollOffset + e.nativeEvent.layout.height;
setTimeout(() => {
this.props.entity.listRef.scrollToOffset({
offset: offsetToScrollTo,
......
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { CommonStyle as CS } from '../styles/Common';
import CenteredLoading from '../common/components/CenteredLoading';
import i18n from '../common/services/i18n.service';
import sessionService from '../common/services/session.service';
/**
* Email confirmation screen
*/
export default class EmailConfirmationScreen extends Component {
static navigationOptions = {
title: 'Email confirm',
};
/**
* State
*/
state = {
confirmed: false,
error: false,
};
/**
* Component did mount
*/
componentDidMount() {
this.confirm();
}
/**
* Confirm
*/
confirm = async () => {
this.setState({ error: false });
const result = await sessionService
.getUser()
.confirmEmail(this.props.navigation.state.params);
if (!result) {
this.setState({ error: true });
} else {
this.setState({ confirmed: true });
}
};
/**
* Render body
*/
renderBody() {
if (this.state.error) {
return (
<Text
style={[CS.fontL, CS.textCenter, CS.colorDarkGreyed]}
onPress={this.confirm}>
{i18n.t('errorMessage') + '\n'}
<Text style={[CS.colorPrimary]}>{i18n.t('tryAgain')}</Text>
</Text>
);
}
if (this.state.confirmed) {
return (
<Text
style={[CS.fontXL, CS.textCenter, CS.colorDarkGreyed]}
onPress={() => this.props.navigation.goBack()}>
{i18n.t('emailConfirm.confirmed') + '\n'}
<Text style={[CS.colorPrimary]}>{i18n.t('goback')}</Text>
</Text>
);
}
return <CenteredLoading />;
}
/**
* Render
*/
render() {
return (
<View style={[CS.flexContainer, CS.centered]}>{this.renderBody()}</View>
);
}
}
......@@ -17,10 +17,12 @@ import CenteredLoading from '../../common/components/CenteredLoading';
import Button from '../../common/components/Button';
import { CommonStyle } from '../../styles/Common';
import ModalConfirmPassword from '../../auth/ModalConfirmPassword';
import { inject } from 'mobx-react/native'
/**
* Email settings screen
*/
@inject('user')
export default class EmailScreen extends Component {
static navigationOptions = {
......@@ -31,7 +33,8 @@ export default class EmailScreen extends Component {
email: null,
saving: false,
isVisible: false,
loaded: false
loaded: false,
showConfirmNote: false,
}
constructor(){
......@@ -45,7 +48,7 @@ export default class EmailScreen extends Component {
* Set email value
*/
setEmail = (email) => {
this.setState({email});
this.setState({email, showConfirmNote: true});
}
/**
......@@ -61,9 +64,9 @@ export default class EmailScreen extends Component {
this.props.navigation.goBack();
})
.finally(() => {
this.setState({isVisible:false});
this.setState({saving: false});
this.props.user.me.setEmailConfirmed(false);
})
.catch(() => {
Alert.alert(i18n.t('error'), i18n.t('settings.errorSaving'));
......@@ -83,19 +86,19 @@ export default class EmailScreen extends Component {
}
const email = this.state.email;
const showConfirmNote = this.state.showConfirmNote;
// validate
const error = validator.emailMessage(email);
const message = error ? <FormValidationMessage>{error}</FormValidationMessage> : null;
const confirmNote = showConfirmNote ? <FormValidationMessage>{i18n.t('emailConfirm.confirmNote')}</FormValidationMessage> : null;
return (
<View style={[CommonStyle.flexContainer, CommonStyle.backgroundWhite, CommonStyle.marginTop2x]}>
<Input
label={i18n.t('settings.currentEmail')}
onChangeText={this.setEmail}
value={email}
errorMessage={error}
leftIcon={{ type: 'font-awesome', name: 'envelope-o' }}
/>
<View style={[CommonStyle.flexContainer, CommonStyle.backgroundWhite]}>
<FormLabel labelStyle={CommonStyle.fieldLabel}>{i18n.t('settings.currentEmail')}</FormLabel>
<FormInput onChangeText={this.setEmail} value={email} inputStyle={CommonStyle.fieldTextInput}/>
{message}
{confirmNote}
<Button
text={i18n.t('save').toUpperCase()}
loading={this.state.saving}
......
import React, { Component } from 'react';
import { Text, StyleSheet, View, Alert } from 'react-native';
import i18n from '../common/services/i18n.service';
import emailConfirmationService from '../common/services/email-confirmation.service';
import IonIcon from 'react-native-vector-icons/Ionicons';
import { CommonStyle as CS } from '../styles/Common';
import { observer, inject } from 'mobx-react/native';
/**
* Email Confirmation Message
*/
export default
@inject('user')
@observer
class EmailConfirmation extends Component {
/**
* Send confirmation email
*/
send = async () => {
if (await emailConfirmationService.send()) {
Alert.alert(i18n.t('emailConfirm.sent'));
} else {
Alert.alert(i18n.t('pleaseTryAgain'));
}
};
/**
* Dismiss message
*/
dismiss = () => {
this.props.user.setDissmis(true);
};
/**
* Render
*/
render() {
const show =
!this.props.user.emailConfirmMessageDismiss &&
this.props.user.me.email_confirmed === false;
if (!show) {
return null;
}
return (
<View style={styles.container}>
<Text style={[CS.fontM, CS.colorWhite]}>
{i18n.t('emailConfirm.confirm')}
</Text>
<Text style={[CS.bold, CS.colorWhite]} onPress={this.send}>
{i18n.t('emailConfirm.sendAgain')}
</Text>
<IonIcon
style={[styles.modalCloseIcon, CS.colorWhite]}
size={28}
name="ios-close"
onPress={this.dismiss}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#4690df',
height: 40,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
},
modalCloseIcon: {
position: 'absolute',
alignSelf: 'flex-end',
paddingRight: 15,
},
});
......@@ -19,13 +19,15 @@ import featuresService from '../common/services/features.service';
import { SafeAreaView } from 'react-navigation';
import isIphoneX from '../common/helpers/isIphoneX';
import testID from '../common/helpers/testID';
import EmailConfirmation from './EmailConfirmation';
const forceInset = isIphoneX ? {top: 32} : null
export default
@inject('user')
@inject('wallet')
@observer
export default class Topbar extends Component {
class Topbar extends Component {
componentDidMount() {
this.props.wallet.refresh();
......@@ -36,9 +38,9 @@ export default class Topbar extends Component {
<SafeAreaView style={styles.container} forceInset={forceInset}>
<View style={styles.topbar}>
{ featuresService.has('crypto') &&
<TouchableOpacity
onPress={() => this.props.navigation.navigate('BoostConsole', { navigation: this.props.navigation })}
{ featuresService.has('crypto') &&
<TouchableOpacity
onPress={() => this.props.navigation.navigate('BoostConsole', { navigation: this.props.navigation })}
{...testID('boost-console button')} >
<View style={styles.topbarLeft}>
<Icon name="trending-up" size={22} color='#444' style={ styles.button }/>
......@@ -57,7 +59,6 @@ export default class Topbar extends Component {
testID="AvatarButton"
/> }
</View>
<TouchableOpacity onPress={() => this.props.navigation.navigate('More', { navigation: this.props.navigation })} {...testID('Main menu button')}>
<View style={styles.topbarRight}>
<Icon name="menu" size={22} color='#444' style={ styles.button }/>
......@@ -65,6 +66,7 @@ export default class Topbar extends Component {
</TouchableOpacity>
</View>
<EmailConfirmation user={this.props.user} />
</SafeAreaView>
);
}
......@@ -81,7 +83,7 @@ const styles = StyleSheet.create({
container: {
height: topbarHeight,
display: 'flex',
flexDirection: 'row',
flexDirection: 'column',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#EEE',
backgroundColor: '#FFFFFF',
......