1. 日记操作组件

2. 设置页面:启动密码,意见反馈,关于,退出登录
3. 设置页面:用户头像,名称,简介
This commit is contained in:
xuwenyang 2019-05-31 20:42:16 +08:00
parent 3c8d338345
commit d6331e6447
27 changed files with 1518 additions and 115 deletions

5
App.js
View file

@ -13,7 +13,10 @@ import {
Animated,
LayoutAnimation,
InteractionManager,
Alert, StatusBar, DeviceEventEmitter, Linking
Alert,
StatusBar,
DeviceEventEmitter,
Linking
} from 'react-native';
import {Input} from "react-native-elements";
import {Navigation} from 'react-native-navigation';

View file

@ -19,6 +19,60 @@ for(let pageName in PageList) {
Navigation.registerComponent(pageName, () => PageList[pageName]);
}
function loginByAccount() {
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Timepill',
options: {
topBar: {
visible: false,
// hide top bar for android
drawBehind: true,
animate: true
}
}
}
}]
}
}
});
}
function loginByPassword() {
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Password',
options: {
topBar: {
title: {
text: '请输入密码'
}
},
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
},
passProps: {
type: 'login'
}
}
}]
}
}
});
}
Navigation.events().registerAppLaunchedListener(async () => {
try {
@ -30,29 +84,16 @@ Navigation.events().registerAppLaunchedListener(async () => {
let token = await Token.getUserToken();
// let token;
if(!token) {
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Timepill',
options: {
topBar: {
visible: false,
// hide top bar for android
drawBehind: true,
animate: true
}
}
}
}]
}
}
});
loginByAccount();
} else {
Navigation.setRoot(BottomNav.config());
const password = await Token.getLoginPassword();
if(password) {
loginByPassword();
} else {
Navigation.setRoot(BottomNav.config());
}
}
});

BIN
src/Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,61 @@
import React, {Component} from 'react';
import {DeviceEventEmitter, Alert} from 'react-native';
import {Navigation} from 'react-native-navigation';
import Api from '../../util/api';
import Msg from '../../util/msg';
import Event from '../../util/event';
function action(componentId, diary, callbacks) {
ActionSheet.showActionSheetWithOptions({
options:['修改','删除', '取消'],
cancelButtonIndex: 2,
destructiveButtonIndex: 1
}, (index) => {
if(index === 0) {
Navigation.push(componentId, {
component: {
name: 'Write',
options: {
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
},
passProps: {
diary: diary
}
}
});
} else if (index === 1) {
Alert.alert('提示', '确认删除日记?', [
{text: '删除', style: 'destructive', onPress: () => {
Api.deleteDiary(diary.id)
.then(() => {
DeviceEventEmitter.emit(Event.updateDiarys, 'del');
Msg.showMsg('日记已删除');
if(callbacks && callbacks.onDelete){
callbacks.onDelete();
}
})
.catch(e => {
Msg.showMsg('日记删除失败' + e.message);
})
.done();
}},
{text: '取消', onPress: () => {}},
]);
}
});
}
export default {
action
}

View file

@ -7,6 +7,8 @@ import Color from '../../style/color';
import UserIcon from '../userIcon';
import Photo from '../photo';
import DiaryAction from './diaryAction';
export default class DiaryBrief extends Component {
@ -26,6 +28,10 @@ export default class DiaryBrief extends Component {
return this.showField.indexOf(field) >= 0;
}
onDiaryAction() {
DiaryAction.action(this.props.componentId, this.diary);
}
render() {
let diary = this.diary;
if(!diary) {
@ -79,7 +85,7 @@ export default class DiaryBrief extends Component {
<View style={{flex: 1}} />
{
this.editable
? <TouchableOpacity onPress={this.props.onDiaryAction}>
? <TouchableOpacity onPress={this.onDiaryAction.bind(this)}>
<Ionicons name="ios-more" size={24}
color={Color.inactiveText}
style={localStyle.moreIcon} />

View file

@ -90,62 +90,12 @@ export default class DiaryList extends Component {
diary: diary,
user: diary.user,
editable: this.editable,
onDiaryAction: this._onDiaryAction.bind(this)
editable: this.editable
}
}
});
}
_onDiaryAction(diary) {
ActionSheet.showActionSheetWithOptions({
options:['修改','删除', '取消'],
cancelButtonIndex: 2,
destructiveButtonIndex: 1
}, (index) => {
if(index === 0) {
Navigation.push(this.props.componentId, {
component: {
name: 'Write',
options: {
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
},
passProps: {
diary: diary
}
}
});
} else if (index === 1) {
Alert.alert('提示', '确认删除日记?', [
{text: '删除', style: 'destructive', onPress: () => {
Api.deleteDiary(diary.id)
.then(() => {
let filterDiaries = this.state.diaries.filter((it) => it.id !== diary.id);
this.setState({
diaries: filterDiaries
});
Msg.showMsg('日记已删除');
})
.catch(e => {
Msg.showMsg('日记删除失败');
})
.done();
}},
{text: '取消', onPress: () => {}},
]);
}
});
}
_onPhotoPress(photoUrl) {
Navigation.push(this.props.componentId, {
component: {
@ -253,12 +203,12 @@ export default class DiaryList extends Component {
renderItem={({item}) => {
return (
<Touchable onPress={() => this._onDiaryPress(item)}>
<DiaryBrief diary={item}
<DiaryBrief {...this.props}
diary={item}
showField={this.props.showField}
editable={this.editable}
onUserIconPress={() => this._onUserIconPress(item)}
onDiaryAction={() => this._onDiaryAction(item)}
onPhotoPress={() => this._onPhotoPress(item.photoUrl)}
>

View file

@ -13,7 +13,7 @@ async function resize(uri, oWidth, oHeight, maxPixel) {
height = Math.sqrt(oHeight * maxPixel / oWidth);
}
const newUri = await ImageResizer.createResizedImage(uri, width, height);
const newUri = await ImageResizer.createResizedImage(uri, width, height, 'JPEG', 75);
return 'file://' + newUri.uri;
}

View file

@ -169,7 +169,8 @@ export default class NotebookDiaryList extends Component {
},
passProps: {
diary: diary,
editable: this.editable
editable: this.editable,
expired: this.notebook.isExpired
}
}
});

View file

@ -0,0 +1,142 @@
import React, {Component} from 'react';
import {
StyleSheet,
View,
TextInput,
TouchableHighlight,
InteractionManager,
Keyboard,
Alert
} from 'react-native';
import Api from '../util/api'
export default class PasswordInput extends Component {
constructor(props) {
super(props);
this.state = {
text: ''
};
}
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this._onPress();
});
}
componentWillUnMount() {
Keyboard.dismiss();
}
clear() {
this.setState({
text: ''
})
}
_onPress() {
setTimeout(() => {
this.inputText.focus();
}, 500);
}
render() {
return (
<TouchableHighlight activeOpacity={1} underlayColor='transparent'
onPress={this._onPress.bind(this)}
>
<View style={[localStyle.container, this.props.style]}>
<TextInput ref={(r) => this.inputText = r}
style={localStyle.textInput}
maxLength={this.props.maxLength}
autoFocus={false}
keyboardType={Api.IS_IOS ? "number-pad" : 'numeric'}
blurOnSubmit={false}
value={this.state.text}
onChangeText={
(text) => {
this.setState({text});
if(text.length === this.props.maxLength) {
this.props.onEnd(text);
}
}
}
/>
{
this._getInputItem()
}
</View>
</TouchableHighlight>
)
}
_getInputItem() {
let inputItem = [];
let text = this.state.text;
for(let i=0; i<parseInt(this.props.maxLength); i++) {
if(i == 0) {
inputItem.push(
<View key={i} style={[localStyle.inputItem, this.props.inputItemStyle]}>
{i < text.length ? <View style={[localStyle.iconStyle, this.props.iconStyle]}/> : null}
</View>
);
} else {
inputItem.push(
<View key={i} style={[localStyle.inputItem, localStyle.inputItemBorderLeftWidth, this.props.inputItemStyle]}>
{i < text.length ? <View style={[localStyle.iconStyle, this.props.iconStyle]}/> : null}
</View>)
}
}
return inputItem;
}
}
const localStyle = StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
borderWidth: 1,
borderColor: '#ccc',
backgroundColor: '#fff',
borderRadius: 5
},
textInput: {
position: 'absolute',
top: 0,
left: 0,
width: 1,
height:1,
padding: 0,
margin: 0
},
inputItem: {
height: 45,
width: 45,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white'
},
inputItemBorderLeftWidth: {
borderLeftWidth: 1,
borderColor: '#ccc'
},
iconStyle: {
width: 16,
height: 16,
backgroundColor: '#222',
borderRadius: 8
}
});

View file

@ -5,6 +5,13 @@ import {Avatar} from "react-native-elements";
export default class UserIcon extends Component {
constructor(props) {
super(props);
this.state = {
iconUrl : props.iconUrl
};
}
_defaultOnPress() {
// empty
}
@ -15,7 +22,7 @@ export default class UserIcon extends Component {
containerStyle={localStyle.container}
width={this.props.width || 40}
height={this.props.height || 40}
source={{uri: this.props.iconUrl}}
source={{uri: this.state.iconUrl}}
onPress={this.props.onPress ? this.props.onPress : this._defaultOnPress.bind(this)}
activeOpacity={0.7}
/>

View file

@ -23,14 +23,19 @@ export default class UserIntro extends Component {
}
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.loadUser();
});
Api.getSelfInfoByStore()
.then(user => {
this.selfInfo = user;
InteractionManager.runAfterInteractions(() => {
this.refresh();
});
});
}
loadUser() {
let user = this.state.user;
(user ? Api.getUserInfo(user.id) : Api.getSelfInfoByStore())
refresh() {
let userId = this.state.user ? this.state.user.id : this.selfInfo.id;
Api.getUserInfo(userId)
.then(user => {
this.setState({
user: user

60
src/page/AboutPage.js Normal file
View file

@ -0,0 +1,60 @@
import React, { Component } from 'react';
import {
StyleSheet,
View,
Text,
Image,
DeviceEventEmitter
} from 'react-native';
import Api from '../util/api';
import TokenManager from '../util/token';
import Event from '../util/event';
import Color from '../style/color';
import UpdateInfo from '../updateInfo';
export default class AboutPage extends Component {
constructor(props) {
super(props);
this.state = {
info: null,
news: UpdateInfo
};
}
static options(passProps) {
return {
topBar: {
title: {
text: '关于'
}
}
};
}
componentDidMount() {
TokenManager.setUpdateVersion(UpdateInfo.version)
.then(() => {
DeviceEventEmitter.emit(Event.updateNewsRead);
});
}
render() {
const label = this.state.info ? ` (${this.state.info.label})` : null;
return (
<View style={{flex: 1, backgroundColor: 'white'}}>
<View style={{flex: 1, padding: 15, alignItems: 'center', paddingTop: 80}}>
<Image source={require('../Icon.png')}
style={{width: 128, height: 128, borderRadius: 28, borderWidth: 1, borderColor:"#d9d9d9"}} />
<Text style={{paddingTop: 20, paddingBottom: 60}}>版本: {Api.VERSION}{label}</Text>
<Text style={{color: Color.inactiveText}}>{this.state.news.date} 更新日志</Text>
<Text style={{lineHeight: 20}}>{this.state.news.info}</Text>
</View>
</View>
);
}
}

View file

@ -18,7 +18,8 @@ import Api from '../util/api'
import Event from '../util/event';
import DiaryFull from '../component/diary/diaryFull';
import CommentInput from '../component/comment/commentInput'
import DiaryAction from '../component/diary/diaryAction';
import CommentInput from '../component/comment/commentInput';
export default class DiaryDetailPage extends Component {
@ -35,31 +36,40 @@ export default class DiaryDetailPage extends Component {
user: props.user,
editable: props.editable || false,
expired: props.expired || false,
needScrollToBottom: false
}
this.onDiaryAction = props.onDiaryAction || (() => {});
}
static options(passProps) {
return {
topBar: {
title: {
text: '日记详情'
},
rightButtons: [{
id: 'navButtonMore',
icon: Icon.navButtonMore,
color: Color.primary // android
}]
let topBar = {
title: {
text: '日记详情'
}
}
if(!passProps.expired) {
topBar.rightButtons = [{
id: 'navButtonMore',
icon: Icon.navButtonMore,
color: Color.primary // android
}]
}
return {
topBar
};
}
navigationButtonPressed({buttonId}) {
if(this.state.editable) {
this.onDiaryAction(this.state.diary);
if(this.state.editable || this.state.diary.user_id == this.state.selfInfo.id) {
let componentId = this.props.componentId;
DiaryAction.action(componentId, this.state.diary, {
onDelete: () => {
Navigation.pop(componentId);
}
});
} else {
ActionSheet.showActionSheetWithOptions({
@ -90,7 +100,9 @@ export default class DiaryDetailPage extends Component {
}).done();
this.diaryListener = DeviceEventEmitter.addListener(Event.updateDiarys, (param) => {
this.refreshDiary();
if(param != 'del') {
this.refreshDiary();
}
});
this.commentListener = DeviceEventEmitter.addListener(Event.updateComments, (param) => {
@ -131,6 +143,15 @@ export default class DiaryDetailPage extends Component {
}
render() {
if(!this.state.selfInfo || !this.state.diary) {
return null;
}
let isMine = false;
if(this.state.selfInfo.id == this.state.diary.user_id) {
isMine = true;
}
return (
<View style={localStyle.wrap}>
<ScrollView ref={(r)=>this.scroll = r}
@ -144,16 +165,16 @@ export default class DiaryDetailPage extends Component {
>
<DiaryFull ref={(r) => this.diaryFull = r}
{...this.props}
diary={this.state.diary}
refreshData={() => this.state.diary}
editable={this.state.editable}
{...this.props}
editable={this.state.editable || isMine}
></DiaryFull>
</ScrollView>
{
this.state.selfInfo && this.state.diary ? (
!this.state.expired ? (
<CommentInput ref={(r) => this.commentInput = r}
diary={this.state.diary}
selfInfo={this.state.selfInfo}

70
src/page/FeedbackPage.js Normal file
View file

@ -0,0 +1,70 @@
import React, { Component } from 'react';
import {
StyleSheet,
View,
Text,
DeviceEventEmitter,
TextInput
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {Button} from "react-native-elements";
import Color from '../style/color';
import Api from '../util/api';
import Msg from '../util/msg';
export default class FeedbackPage extends Component {
constructor(props) {
super(props);
this.state = {
content: ''
}
}
static options(passProps) {
return {
topBar: {
title: {
text: '意见反馈'
}
}
};
}
send() {
Api.feedback(this.state.content)
.then(() => {
Msg.showMsg('感谢反馈 ');
Navigation.pop(this.props.componentId);
})
.catch(e => {
Msg.showMsg('反馈失败');
})
.done();
}
render() {
return (
<View style={{flex: 1, backgroundColor: Color.spaceBackground}}>
<TextInput
style={{padding: 10, height: 240, margin: 10, backgroundColor: '#fff', textAlignVertical:'top'}}
underlineColorAndroid='transparent'
selectionColor={Color.light}
autoCorrect={false}
multiline={true}
maxLength={1000}
value={this.state.content}
onChangeText={(text) => this.setState({content: text})}
/>
<Button borderRadius={999} title={'发送'} backgroundColor={Color.primary}
onPress={this.send.bind(this)}
/>
</View>
);
}
}

View file

@ -49,7 +49,6 @@ export default class FollowPage extends Component {
}
}
});
}
render() {

View file

@ -1,9 +1,10 @@
import React, {Component} from 'react';
import {StyleSheet, Text, View, ScrollView} from 'react-native';
import {StyleSheet, Text, View, DeviceEventEmitter} from 'react-native';
import {Navigation} from 'react-native-navigation';
import Color from '../style/color';
import {Icon} from '../style/icon';
import Event from '../util/event';
import NotebookDiaryList from '../component/notebook/notebookDiaryList';
@ -46,10 +47,21 @@ export default class NotebookDetailPage extends Component {
});
}
componentDidMount() {
this.diaryListener = DeviceEventEmitter.addListener(Event.updateDiarys, (param) => {
this.diaryList.refresh();
});
}
componentWillUnmount() {
this.diaryListener.remove();
}
render() {
return (
<View style={{flex: 1}}>
<NotebookDiaryList notebook={this.props.notebook}
<NotebookDiaryList ref={(r) => this.diaryList = r}
notebook={this.props.notebook}
{...this.props}>
</NotebookDiaryList>
</View>

208
src/page/PasswordPage.js Normal file
View file

@ -0,0 +1,208 @@
import React, { Component } from 'react';
import {
View,
Text,
Alert,
DeviceEventEmitter,
TouchableOpacity,
Keyboard
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import Color from '../style/color';
import Api from '../util/api'
import TokenManager from '../util/token';
import Event from '../util/event';
import Msg from '../util/msg';
import BottomNav from '../nav/bottomNav';
import PasswordInput from "../component/passwordInput";
export default class PasswordPage extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
oldPassword: null,
step: 1,
password: null
};
}
componentDidMount() {
TokenManager.getLoginPassword()
.then(pwd => {
if(this.props.type == 'setting') {
if(this.props.operation == 'setnew') {
this.setState({
title: '请输入新密码'
});
} else if(this.props.operation == 'cancel'){
this.setState({
title: '请输入密码',
oldPassword: pwd
});
}
} else if(this.props.type == 'login') {
this.setState({
oldPassword: pwd
});
}
});
}
_onEnd(password) {
if(this.props.type == 'setting') {
if(this.props.operation == 'setnew') {
this._setting(password);
} else if(this.props.operation == 'cancel') {
this._clearPassword(password);
}
} else if(this.props.type == 'login') {
this._login(password);
}
}
_setting(password) {
if (this.state.step == 1) {
if(!password.match(/^\d+$/)) {
Alert.alert('提示', '只能设置数字密码');
} else {
this.setState({
title: '请再次输入密码',
password: password,
step: 2
});
}
this.refs.input.clear();
} else if (this.state.step == 2) {
if(this.state.password !== password) {
Alert.alert('设置失败', '两次输入的密码不相同,请重新输入');
this.refs.input.clear();
this.setState({
title: '请输入新密码',
password: null,
step: 1
});
} else {
TokenManager.setLoginPassword(password)
.then(() => {
Keyboard.dismiss();
Msg.showMsg('密码已设置');
DeviceEventEmitter.emit(Event.passwordUpdated);
Navigation.pop(this.props.componentId);
})
.catch(e => {
Alert.alert('错误', '设置密码失败:' + e.message);
})
}
}
}
_login(password) {
if(!this.state.oldPassword) {
Alert.alert('错误', '密码加载失败');
return;
}
if(this.state.oldPassword === password) {
Navigation.setRoot(BottomNav.config());
} else {
Alert.alert('失败', '密码错误');
}
this.refs.input.clear();
}
_clearPassword(password) {
if(!this.state.oldPassword) {
Alert.alert('错误', '密码加载失败');
return;
}
if(this.state.oldPassword !== password) {
Alert.alert('提示', '密码不正确');
this.refs.input.clear();
return;
}
TokenManager.setLoginPassword('')
.then(() => {
Keyboard.dismiss();
Msg.showMsg('密码已清除');
DeviceEventEmitter.emit(Event.passwordUpdated);
Navigation.pop(this.props.componentId);
}).catch(() => {
Alert.alert('错误', '清除密码失败');
})
}
toLogin() {
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Timepill',
options: {
topBar: {
visible: false,
// hide top bar for android
drawBehind: true,
animate: true
}
}
}
}]
}
}
});
};
render() {
return (
<View style={{flex: 1, backgroundColor: '#EFEFF4'}}>
<View style={{flex: 1, alignItems: 'center', marginTop: 60}}>
<Text style={{fontSize: 24}}>{this.state.title}</Text>
<PasswordInput ref='input' style={{marginTop: 50}} maxLength={4} onEnd={this._onEnd.bind(this)}/>
{
this.props.type == 'setting' && this.props.operation != 'cancel'
? (
<Text style={{marginTop: 50, fontSize: 11, color: Color.inactiveText}}>提示: 从后台切切换前台时不需要输入密码</Text>
) : null
}
{
this.props.type == 'setting' ? null : (
<View style={{flex: 1, alignItems: 'center', paddingTop: 22}}>
<TouchableOpacity onPress={this.toLogin}>
<Text style={{fontSize: 14, color: Color.primary, padding: 10}}>
忘记密码通过登录重设
</Text>
</TouchableOpacity>
</View>
)
}
</View>
</View>
);
}
}

272
src/page/SettingPage.js Normal file
View file

@ -0,0 +1,272 @@
import React, {Component} from 'react';
import {
StyleSheet,
Text,
View,
Switch,
Alert,
Linking,
TouchableOpacity,
DeviceEventEmitter
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import Ionicons from 'react-native-vector-icons/Ionicons';
import Api from '../util/api';
import TokenManager from '../util/token';
import Color from '../style/color';
export default class SettingPage extends Component {
constructor(props) {
super(props);
this.state = {
hasPassword: false,
hasUpdateNews: false,
settings: {}
}
}
static options(passProps) {
return {
topBar: {
title: {
text: '设置'
}
}
};
}
componentDidMount() {
this.refreshPasswordState();
this.passwordListener = DeviceEventEmitter.addListener('passwordUpdated', this.refreshPasswordState.bind(this));
}
componentWillUnmount() {
this.passwordListener.remove();
}
refreshPasswordState() {
TokenManager.getLoginPassword()
.then((pwd) => this.setState({
hasPassword: pwd != null && pwd.length > 0
}));
};
changePassword = () => {
let titleText = '';
if(!this.state.hasPassword) {
titleText = '设置启动密码';
} else {
titleText = '取消启动密码';
}
Navigation.push(this.props.componentId, {
component: {
name: 'Password',
options: {
topBar: {
title: {
text: titleText
}
},
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
},
passProps: {
type: 'setting',
operation: !this.state.hasPassword ? 'setnew' : 'cancel'
}
}
});
};
changePush = (val) => {
//
}
jumpTo(pageName) {
Navigation.push(this.props.componentId, {
component: {
name: pageName,
options: {
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
}
}
});
}
logout() {
Alert.alert('提示','确认退出登录?',[
{text: '退出', style: 'destructive', onPress: () => {
Api.logout();
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Timepill',
options: {
topBar: {
visible: false,
// hide top bar for android
drawBehind: true,
animate: true
}
}
}
}]
}
}
});
}},
{text: '取消', onPress: () => {}}
]);
}
render() {
return (
<View style={localStyle.wrap}>
<View style={localStyle.group}>
<TouchableOpacity style={localStyle.item} onPress={() => this.jumpTo('UserInfoEdit')}>
<Text style={localStyle.title}>修改个人信息</Text>
<Ionicons name="ios-arrow-forward" style={localStyle.arrow} size={18} color='#0076FF'/>
</TouchableOpacity>
<View style={localStyle.line} />
<View style={localStyle.item}>
<Text style={localStyle.title}>启动密码</Text>
<Switch value={this.state.hasPassword}
trackColor={Api.IS_ANDROID ? Color.textSelect : null}
thumbColor={Api.IS_ANDROID && this.state.hasPassword ? Color.light : null}
onValueChange={this.changePassword} />
</View>
<View style={localStyle.line} />
<View style={localStyle.item}>
<Text style={localStyle.title}>提醒推送</Text>
<Switch value={this.state.settings['pushMessage']}
trackColor={Api.IS_ANDROID ? Color.textSelect : null}
thumbColor={Api.IS_ANDROID && this.state.settings['pushMessage'] ? Color.light : null}
onValueChange={this.changePush} />
</View>
</View>
<View style={localStyle.group}>
{
Api.IS_IOS ? (
<View>
<TouchableOpacity style={localStyle.item}
onPress={() =>
Linking.openURL("https://itunes.apple.com/us/app/jiao-nang-ri-ji/id1142102323?l=zh&ls=1&mt=8")}
>
<Text style={localStyle.title}> App Store 评价</Text>
<Ionicons name="ios-arrow-forward" style={localStyle.arrow} size={18}/>
</TouchableOpacity>
<View style={localStyle.line} />
</View>
) : null
}
<TouchableOpacity style={localStyle.item}
onPress={() => this.jumpTo('Feedback')}>
<Text style={localStyle.title}>意见反馈</Text>
<Ionicons name="ios-arrow-forward" style={localStyle.arrow} size={18}/>
</TouchableOpacity>
<View style={localStyle.line} />
<TouchableOpacity style={localStyle.item}
onPress={() => this.jumpTo('About')}>
<Text style={localStyle.title}>关于</Text>
{
this.state.hasUpdateNews
? (
<View style={localStyle.badge}>
<Text style={localStyle.badgeText}>1</Text>
</View>
)
: null
}
<Ionicons name="ios-arrow-forward" style={localStyle.arrow} size={18}/>
</TouchableOpacity>
</View>
<View style={[localStyle.group, { marginTop: 45 }]}>
<TouchableOpacity style={localStyle.item}
onPress={this.logout.bind(this)}
>
<Text style={localStyle.button}>退出登录</Text>
</TouchableOpacity>
</View>
</View>
);
}
}
const localStyle = StyleSheet.create({
wrap: {
flex: 1,
backgroundColor: '#EFEFF4'
},
group: {
marginTop: 30,
backgroundColor: 'white',
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#c8c7cc'
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 15,
height: 45
},
title: {
fontSize: 16,
color: '#222',
flex: 1
},
line: {
marginLeft: 15,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#c8c7cc'
},
arrow: {
paddingTop: 1,
color: Color.inactiveText
},
button: {
flex: 1,
textAlign: 'center',
color: '#d9534f',
fontSize: 16
},
badge: {
backgroundColor: 'red',
paddingHorizontal:8,
paddingVertical: 2,
borderRadius: 12,
marginRight: 10
},
badgeText: {
color: 'white',
fontSize: 12,
fontFamily: 'Arial'
}
});

View file

@ -0,0 +1,187 @@
import React, { Component } from 'react';
import {
StyleSheet,
View,
Text,
TouchableOpacity,
Image,
DeviceEventEmitter,
Keyboard
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import Ionicons from 'react-native-vector-icons/Ionicons';
import Color from '../style/color';
import Token from '../util/token'
import Api from '../util/api';
import Event from '../util/event';
import Msg from '../util/msg';
import Loading from '../component/loading'
import ImageAction from '../component/image/imageAction'
export default class UserInfoEditPage extends Component {
constructor(props) {
super(props);
this.state = {
user: null,
uploading: false
};
}
componentDidMount() {
this.loadUser();
this.userInfoListener = DeviceEventEmitter.addListener(Event.userInfoUpdated, this.loadUser.bind(this));
}
componentWillUnmount() {
this.userInfoListener.remove();
}
loadUser() {
Api.getSelfInfoByStore()
.then(user => {
this.setState({user});
});
}
editIcon() {
ImageAction.action({
width: 640,
height: 640,
cropping: true
}, -1, 640 * 640, (e, imageUri) => {
if(e) {
Msg.showMsg('操作失败:' + e.message);
} else {
this.uploadIcon(imageUri);
}
});
}
uploadIcon(uri) {
this.setState({uploading: true});
Api.updateUserIcon(uri)
.then(async user => {
await Token.setUserInfo(user);
this.loadUser();
Msg.showMsg('头像保存成功');
DeviceEventEmitter.emit(Event.userInfoUpdated);
})
.catch(e => {
Msg.showMsg('头像更新失败:' + e.message);
})
.done(() => {
this.setState({uploading: false});
});
}
jumpTo(pageName) {
Navigation.push(this.props.componentId, {
component: {
name: pageName,
options: {
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
},
passProps: {
user: this.state.user
}
}
});
}
render() {
return (
<View style={{flex: 1, backgroundColor: '#EFEFF4'}}>
<Loading visible={this.state.uploading}></Loading>
{
this.state.user ? (
<View style={localStyle.group}>
<TouchableOpacity style={localStyle.item} onPress={this.editIcon.bind(this)}>
<Text style={localStyle.title}>头像</Text>
<View style={localStyle.right}>
<Image source={{uri: this.state.user.iconUrl}} style={{width: 28, height: 28, borderRadius: 14}} />
<Ionicons name="ios-arrow-forward" style={localStyle.arrow} size={18}/>
</View>
</TouchableOpacity>
<View style={localStyle.line} />
<TouchableOpacity style={localStyle.item} onPress={() => this.jumpTo('UserNameEdit')}>
<Text style={localStyle.title}>名字</Text>
<View style={localStyle.right}>
<Text style={localStyle.value}>{this.state.user.name}</Text>
<Ionicons name="ios-arrow-forward" style={localStyle.arrow} size={18}/>
</View>
</TouchableOpacity>
<View style={localStyle.line} />
<TouchableOpacity style={localStyle.item} onPress={() => this.jumpTo('UserIntroEdit')}>
<Text style={localStyle.title}>个人简介</Text>
<Ionicons name="ios-arrow-forward" style={localStyle.arrow} size={18}/>
</TouchableOpacity>
</View>
) : null
}
</View>
);
}
}
const localStyle = StyleSheet.create({
group: {
marginTop: 30,
backgroundColor: 'white',
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#c8c7cc'
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 15,
height: 45
},
title: {
fontSize: 16,
color: '#222'
},
line: {
marginLeft: 15,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#c8c7cc'
},
right: {
flexDirection:'row',
alignItems: 'center'
},
arrow: {
paddingTop: 1,
color: Color.inactiveText,
paddingLeft: 15
},
value: {
fontSize: 16,
color: Color.inactiveText
},
button: {
flex: 1,
textAlign: 'center',
color: '#d9534f',
fontSize: 16
}
});

View file

@ -0,0 +1,126 @@
import React, { Component } from 'react';
import {
StyleSheet,
View,
Alert,
Text,
TextInput,
DeviceEventEmitter,
Keyboard
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import Color from '../style/color';
import {Icon} from '../style/icon';
import Msg from '../util/msg';
import Api from '../util/api';
import Token from '../util/token';
import Event from '../util/event';
export default class EditIntroPage extends Component {
constructor(props) {
super(props);
Navigation.events().bindComponent(this);
this.state = {
intro: props.user.intro
}
}
static options(passProps) {
return {
topBar: {
title: {
text: '修改简介'
},
rightButtons: [{
id: 'save',
icon: Icon.navButtonSave
}]
},
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
};
}
componentDidMount() {
setTimeout(() => {
this.refs.input.focus();
}, 500);
}
navigationButtonPressed({buttonId}) {
this.saveUserIntro();
}
saveUserIntro() {
const len = this.state.intro.length;
if(len === 0) {
Alert.alert('提示', '简介不能为空');
return;
} else if (len > 500) {
Alert.alert('提示', '简介不能超过500个字');
return;
}
Api.updateUserInfo(this.props.user.name, this.state.intro)
.then(async user => {
Keyboard.dismiss();
Msg.showMsg('修改成功');
await Token.setUserInfo(user);
DeviceEventEmitter.emit(Event.userInfoUpdated);
Navigation.pop(this.props.componentId);
})
.catch(e => {
Msg.showMsg('修改失败');
})
.done();
}
render() {
return (
<View style={{flex: 1, backgroundColor: '#EFEFF4'}}>
<View style={localStyle.group}>
<TextInput ref='input'
underlineColorAndroid='transparent'
style={localStyle.textInput}
selectionColor={Color.primary}
value={this.state.intro}
onChangeText={(text) => this.setState({intro: text})}
multiline={true}
/>
</View>
</View>
);
}
}
const localStyle = StyleSheet.create({
group: {
marginTop: 30,
backgroundColor: 'white',
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#c8c7cc'
},
textInput: {
flexGrow: 1,
fontSize: 16,
margin: 15,
color: Color.text,
height: 200,
textAlignVertical: 'top'
}
});

View file

@ -0,0 +1,137 @@
import React, { Component } from 'react';
import {
StyleSheet,
View,
Alert,
Text,
TextInput,
DeviceEventEmitter,
Keyboard
} from 'react-native';
import {Navigation} from 'react-native-navigation';
import Color from '../style/color';
import {Icon} from '../style/icon';
import Msg from '../util/msg';
import Api from '../util/api';
import Token from '../util/token';
import Event from '../util/event';
export default class UserNameEditPage extends Component {
constructor(props) {
super(props);
Navigation.events().bindComponent(this);
this.state = {
name: props.user.name
}
}
static options(passProps) {
return {
topBar: {
title: {
text: '修改名字'
},
rightButtons: [{
id: 'save',
icon: Icon.navButtonSave
}]
},
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
};
}
componentDidMount() {
setTimeout(() => {
this.refs.input.focus();
}, 500);
}
navigationButtonPressed({buttonId}) {
this.saveUserName();
}
saveUserName() {
const len = this.state.name.length;
if (len === 0) {
Alert.alert('提示', '名字不能为空');
return;
} else if (len > 10) {
Alert.alert('提示', '名字不能超过10个字');
return;
}
Api.updateUserInfo(this.state.name, this.props.user.intro)
.then(async user => {
Keyboard.dismiss();
Msg.showMsg('修改成功');
await Token.setUserInfo(user);
DeviceEventEmitter.emit(Event.userInfoUpdated);
Navigation.pop(this.props.componentId);
})
.catch(e => {
Msg.showMsg('修改失败');
})
.done();
}
render() {
return (
<View style={{flex: 1, backgroundColor: '#EFEFF4'}}>
<View style={localStyle.group}>
<View style={localStyle.item}>
<Text style={localStyle.title}>名字</Text>
<TextInput ref='input'
style={localStyle.textInput}
underlineColorAndroid='transparent'
selectionColor={Color.primary}
value={this.state.name}
onChangeText={(text) => this.setState({name: text})}
/>
</View>
</View>
</View>
);
}
}
const localStyle = StyleSheet.create({
group: {
marginTop: 30,
backgroundColor: 'white',
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#c8c7cc'
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 15,
height: 45
},
title: {
fontSize: 16,
color: '#222'
},
textInput: {
flex: 1,
fontSize: 16,
marginLeft: 15,
padding: 0,
height: 24,
color: Color.content
}
});

View file

@ -9,7 +9,7 @@ import {
import {Navigation} from 'react-native-navigation';
import Api from '../util/api';
import {Icon} from '../style/icon'
import {Icon} from '../style/icon';
import Event from "../util/event";
import Color from '../style/color';
@ -101,6 +101,22 @@ export default class UserPage extends Component {
.catch(e => {
Alert.alert('取消关注失败');
}).done();
} else if(buttonId == 'setting') {
Navigation.push(this.props.componentId, {
component: {
name: 'Setting',
options: {
bottomTabs: {
visible: false,
// hide bottom tab for android
drawBehind: true,
animate: true
}
}
}
});
}
}
@ -130,11 +146,16 @@ export default class UserPage extends Component {
this.diaryListener = DeviceEventEmitter.addListener(Event.updateDiarys, (param) => {
this.diaryList.refresh();
});
this.userInfoListener = DeviceEventEmitter.addListener(Event.userInfoUpdated, (param) => {
this.userIntro.refresh();
});
}
componentWillUnmount() {
this.notebookListener.remove();
this.diaryListener.remove();
this.userInfoListener.remove();
}
_renderLabel = props => ({route}) => {
@ -166,6 +187,7 @@ export default class UserPage extends Component {
_renderScene = SceneMap({
userIntro: () => <UserIntro
ref={(r) => this.userIntro = r}
user={this.user}
/>,
diary: () => <DiaryList

View file

@ -23,7 +23,6 @@ import {Icon} from '../style/icon';
import Color from '../style/color';
import Api from '../util/api';
import Msg from '../util/msg';
import Token from '../util/token'
import Event from "../util/event";
import NotebookLine from '../component/notebook/notebookLine';

View file

@ -1,6 +1,8 @@
export default {
About: require("./AboutPage.js").default,
DiaryDetail: require("./DiaryDetailPage.js").default,
Empty: require("./EmptyPage.js").default,
Feedback: require("./FeedbackPage.js").default,
Follow: require("./FollowPage.js").default,
FollowUser: require("./FollowUserPage.js").default,
Home: require("./HomePage.js").default,
@ -8,7 +10,12 @@ NotebookDetail: require("./NotebookDetailPage.js").default,
NotebookEdit: require("./NotebookEditPage.js").default,
NotificationHistory: require("./NotificationHistoryPage.js").default,
Notification: require("./NotificationPage.js").default,
Password: require("./PasswordPage.js").default,
Photo: require("./PhotoPage.js").default,
Setting: require("./SettingPage.js").default,
UserInfoEdit: require("./UserInfoEditPage.js").default,
UserIntroEdit: require("./UserIntroEditPage.js").default,
UserNameEdit: require("./UserNameEditPage.js").default,
User: require("./UserPage.js").default,
Write: require("./WritePage.js").default
}

11
src/updateInfo.js Normal file
View file

@ -0,0 +1,11 @@
export default {
version: 5,
date: '4月9日',
info: `
1.增加草稿本
2.修复桌面图标提示点不掉的问题
3.优化话题UI
4.优化 iPhone X 兼容
5.设置页增加关注用户入口
6.点击 Tab 图标回到顶部再次点击刷新列表
`}

View file

@ -18,6 +18,7 @@ const IS_IPHONEX = isIphoneX();
const baseUrl = 'http://open.timepill.net/api';
const v2Url = 'http://v2.timepill.net/api';
async function login(username, password) {
@ -42,6 +43,12 @@ async function login(username, password) {
}
}
async function logout() {
TokenManager.setUserToken('');
TokenManager.setUserInfo(false);
TokenManager.setLoginPassword('');
}
async function getSelfInfo() {
return call('GET', '/users/my');
}
@ -54,6 +61,20 @@ async function getUserInfo(id) {
return call('GET', '/users/' + id)
}
async function updateUserIcon(photoUri) {
return upload('POST', '/users/icon', {
icon: {uri: photoUri, name: 'image.jpg', type: 'image/jpg'}
});
}
async function updateUserInfo(name, intro) {
return call('PUT', '/users', {
name: name,
intro: intro
});
}
async function getTodayDiaries(page = 1, page_size = 20, first_id = '') {
return call('GET', '/diaries/today?page=' + page + '&page_size=' + page_size + `&first_id=${first_id}`)
.then((json) => {
@ -216,6 +237,10 @@ async function report(user_id, diary_id) {
});
}
async function feedback(content) {
return callV2('POST', '/feedback', {content});
}
async function upload(method, api, body) {
let token = await TokenManager.getUserToken();
@ -245,9 +270,8 @@ async function upload(method, api, body) {
, 60000);
}
async function call(method, api, body, _timeout = 10000) {
async function call(method, api, body = null, _timeout = 10000) {
let token = await TokenManager.getUserToken();
return timeout(fetch(baseUrl + api, {
method: method,
headers: {
@ -268,6 +292,31 @@ async function call(method, api, body, _timeout = 10000) {
, _timeout);
}
async function callV2(method, api, body = null, _timeout = 10000) {
let token = await TokenManager.getToken();
return timeout(fetch(v2Url + api, {
method: method,
headers: {
'Authorization': token,
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-TP-OS': OS,
'X-TP-OS-Version': OS_VERSION,
'X-TP-Version': VERSION,
'X-Device-ID': DEVICE_ID,
},
body: body ? JSON.stringify(body) : null
})
.then((response) => {
return response;
})
.then(checkStatus)
.then(parseJSON)
.catch(handleCatch)
,_timeout);
}
async function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
@ -332,9 +381,13 @@ export default {
IS_IPHONEX,
login,
logout,
getSelfInfoByStore,
getUserInfo,
updateUserIcon,
updateUserInfo,
getTodayDiaries,
getFollowDiaries,
getNotebookDiaries,
@ -367,5 +420,6 @@ export default {
addDiary,
updateDiary,
report
report,
feedback
}

View file

@ -4,6 +4,8 @@ export default {
updateDiarys: 'updateDiarys',
updateComments: 'updateComments',
commentPressed: 'commentPressed'
commentPressed: 'commentPressed',
passwordUpdated: 'passwordUpdated',
updateNewsRead: 'updateNewsRead',
userInfoUpdated: 'userInfoUpdated'
}