├── .buckconfig ├── .eslintrc.json ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── LICENSE ├── README.md ├── android ├── app │ ├── BUCK │ ├── build.gradle │ ├── my-release-key.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ └── fonts │ │ │ ├── Entypo.ttf │ │ │ ├── EvilIcons.ttf │ │ │ ├── FontAwesome.ttf │ │ │ ├── Foundation.ttf │ │ │ ├── Ionicons.ttf │ │ │ ├── MaterialIcons.ttf │ │ │ ├── Octicons.ttf │ │ │ └── Zocial.ttf │ │ ├── java │ │ └── com │ │ │ └── reactnativecnblogs │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ └── debug.keystore.properties └── settings.gradle ├── app.json ├── index.android.js ├── index.ios.js ├── ios ├── bundle │ ├── main.jsbundle │ └── main.jsbundle.meta ├── reactNativeCnblogs-tvOS │ └── Info.plist ├── reactNativeCnblogs-tvOSTests │ └── Info.plist ├── reactNativeCnblogs.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── reactNativeCnblogs-tvOS.xcscheme │ │ └── reactNativeCnblogs.xcscheme ├── reactNativeCnblogs │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-40.png │ │ │ ├── Icon-40@2x-1.png │ │ │ ├── Icon-40@2x.png │ │ │ ├── Icon-40@3x.png │ │ │ ├── Icon-60@2x.png │ │ │ ├── Icon-60@3x.png │ │ │ ├── Icon-72.png │ │ │ ├── Icon-72@2x.png │ │ │ ├── Icon-76.png │ │ │ ├── Icon-76@2x.png │ │ │ ├── Icon-Small.png │ │ │ ├── Icon-Small@2x-1.png │ │ │ ├── Icon-Small@3x-1.png │ │ │ └── cnblogs_icon.png │ │ ├── Contents.json │ │ └── LaunchImage.launchimage │ │ │ ├── Contents.json │ │ │ ├── Default1242x2208.png │ │ │ ├── Default640x1136.png │ │ │ ├── Default640x960.png │ │ │ └── Default750x1334.png │ ├── Info.plist │ ├── main.jsbundle │ └── main.m └── reactNativeCnblogsTests │ ├── Info.plist │ └── reactNativeCnblogsTests.m ├── package.json ├── screenshot ├── 1.png ├── 2.png └── 3.png ├── source ├── action │ ├── author.js │ ├── comment.js │ ├── common.js │ ├── config.js │ ├── offline.js │ ├── post.js │ ├── search.js │ ├── update.js │ └── user.js ├── common │ ├── base64.js │ ├── index.js │ ├── jsencrypt.js │ └── updater.js ├── component │ ├── button │ │ ├── home.js │ │ ├── post.js │ │ ├── search.js │ │ └── single.js │ ├── drawerPanel.js │ ├── endtag.js │ ├── header │ │ ├── author.js │ │ ├── favorite.js │ │ ├── home.js │ │ ├── news.js │ │ ├── offline.js │ │ ├── offlinePost.js │ │ ├── post.js │ │ ├── search.js │ │ └── user.js │ ├── hintMessage.js │ ├── htmlConvertor.js │ ├── imageBox.js │ ├── listview │ │ ├── authorPostList.js │ │ ├── authorPostRow.js │ │ ├── blinkList.js │ │ ├── blinkRow.js │ │ ├── newsCommentList.js │ │ ├── newsCommentRow.js │ │ ├── newsList.js │ │ ├── newsRow.js │ │ ├── offlineList.js │ │ ├── offlineRow.js │ │ ├── postCommentList.js │ │ ├── postCommentRow.js │ │ ├── postList.js │ │ ├── postRow.js │ │ ├── questionList.js │ │ ├── questionRow.js │ │ ├── searchList.js │ │ ├── searchRow.js │ │ ├── userBlinkList.js │ │ ├── userBlinkRow.js │ │ ├── userFavoriteList.js │ │ ├── userFavoriteRow.js │ │ ├── userPostList.js │ │ ├── userPostRow.js │ │ ├── userQuestionList.js │ │ └── userQuestionRow.js │ ├── logo.js │ ├── messager.js │ ├── navbar.js │ ├── navigation.js │ ├── panel.js │ ├── plugin.js │ ├── router.js │ ├── searchBar.js │ ├── spinner.js │ ├── updater.js │ └── view.js ├── config │ ├── api.js │ ├── drawer.js │ ├── index.js │ ├── refreshControl.js │ └── routerScene.js ├── constant │ └── actiontype.js ├── image │ ├── author.png │ ├── avatar.jpg │ ├── header │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ └── logo.png ├── index.js ├── middleware │ ├── callback.js │ ├── common.js │ ├── index.js │ ├── logger.js │ ├── pending.js │ └── promise.js ├── reducer │ ├── author.js │ ├── authorUI.js │ ├── comment.js │ ├── commentListUI.js │ ├── common.js │ ├── config.js │ ├── index.js │ ├── offline.js │ ├── post.js │ ├── postDetailUI.js │ ├── postListUI.js │ ├── search.js │ ├── searchUI.js │ ├── store.js │ ├── update.js │ ├── user.js │ └── userListUI.js ├── service │ ├── authorService.js │ ├── commentService.js │ ├── postService.js │ ├── request.js │ ├── searchService.js │ ├── storage.js │ ├── updateService.js │ └── userService.js ├── style │ └── index.js └── view │ ├── about.js │ ├── author.js │ ├── blink.js │ ├── blinkAdd.js │ ├── commentAdd.js │ ├── favorite.js │ ├── feedback.js │ ├── home.android.js │ ├── home.ios.js │ ├── index.js │ ├── login.js │ ├── offline.js │ ├── offlinePost.js │ ├── post.js │ ├── postComment.js │ ├── question.js │ ├── questionAdd.js │ ├── questionAnswerComment.js │ ├── search.js │ ├── searchDetail.js │ ├── setting.js │ ├── startup.js │ ├── tailSetting.js │ ├── update.js │ ├── user.js │ ├── userAsset.js │ └── web.js └── tsconfig.json /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true 5 | }, 6 | "extends": ["eslint:recommended","plugin:react/recommended"], 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "experimentalObjectRestSpread": true, 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "react", 16 | "react-native" 17 | ], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | "tab" 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "windows" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "single" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | emoji=true 26 | 27 | module.system=haste 28 | 29 | experimental.strict_type_args=true 30 | 31 | munge_underscores=true 32 | 33 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 34 | 35 | suppress_type=$FlowIssue 36 | suppress_type=$FlowFixMe 37 | suppress_type=$FixMe 38 | 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-0]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 40 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-0]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 41 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 42 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 43 | 44 | unsafe.enable_getters_and_setters=true 45 | 46 | [version] 47 | ^0.40.0 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | .vscode 31 | local.properties 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | assets 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | android/app/libs 42 | android/keystores/debug.keystore 43 | android/gradle.properties 44 | source/config/index.bak.js 45 | 46 | __tests__ -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 togayther 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 由于博客园官方近期正在对其授权接口进行安全验证方面的重构,以防范openapi被暴力攻击的风险,所以暂时关闭了登录接口。目前本项目暂时无法登录(接口返回:{"error":"unauthorized_client"})。请大家谅解。代码仅供参考。 3 | 4 | ## introduction 简介 5 | 6 | This is based on the react-native implementation of the cnblogs.com's mobile client for both android and ios. if you have any comments or suggestions, welcome feedback. 7 | 8 | 基于 react-native 实现的博客园移动客户端,兼容android和ios。如果您有任何问题或者建议,欢迎留言反馈,作者会第一时间进行回复,谢谢! 9 | 10 | ## screenshot 截图 11 | 12 | ![home page ](https://github.com/togayther/react-native-cnblogs/raw/master/screenshot/1.png) 13 | 14 | ![index page ](https://github.com/togayther/react-native-cnblogs/raw/master/screenshot/2.png) 15 | 16 | ![profile page ](https://github.com/togayther/react-native-cnblogs/raw/master/screenshot/3.png) 17 | 18 | ## download 下载 19 | ### android 20 | #### download link: 21 | http://fir.im/togayther 22 | 23 | 24 | ### ios 25 | #### appstore link: 26 | https://itunes.apple.com/cn/app/bo-ke-yuan-she-qu/id1176047767?l=zh&ls=1&mt=8 27 | 28 | ## how to run 本地运行 29 | note: if you behind GFW, strongly recommend that you work with vpn. 30 | 31 | 提示:如果你处于全球最大的局域网,强烈建议你购买一个vpn。 32 | 33 | * config your react-native environment: https://facebook.github.io/react-native/docs/getting-started.html 34 | * git clone https://github.com/togayther/react-native-cnblogs.git 35 | * npm install 36 | * react-native link 37 | * connect physical device or turn on the emulator 38 | * react-native run-android/run-ios 39 | * good luck and enjoy 40 | 41 | 注意: 42 | 因为本软件涉及到基于oauth的登录授权,故本地运行还需要向博客园申请 clientId、clientSecret、rsa加密公钥等授权信息。否则运行后无法登录进入首页。 43 | 44 | 应博客园官方团队要求,该软件开源时未公开已取得的授权信息。非常抱歉。 45 | 46 | 授权信息申请方式: 47 | 48 | 对于个人开发者,需要提供以下信息: 49 | 真实姓名、手机号码、常用邮箱、相关app介绍。 50 | 然后邮件发送至: contact@cnblogs.com 51 | 52 | 授权信息配置文件:source/config/index.js => authData 53 | 54 | ## License 授权协议 55 | This project is available under the MIT license. 56 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | lib_deps = [] 12 | 13 | for jarfile in glob(['libs/*.jar']): 14 | name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')] 15 | lib_deps.append(':' + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | 21 | for aarfile in glob(['libs/*.aar']): 22 | name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')] 23 | lib_deps.append(':' + name) 24 | android_prebuilt_aar( 25 | name = name, 26 | aar = aarfile, 27 | ) 28 | 29 | android_library( 30 | name = "all-libs", 31 | exported_deps = lib_deps, 32 | ) 33 | 34 | android_library( 35 | name = "app-code", 36 | srcs = glob([ 37 | "src/main/java/**/*.java", 38 | ]), 39 | deps = [ 40 | ":all-libs", 41 | ":build_config", 42 | ":res", 43 | ], 44 | ) 45 | 46 | android_build_config( 47 | name = "build_config", 48 | package = "com.reactnativecnblogs", 49 | ) 50 | 51 | android_resource( 52 | name = "res", 53 | package = "com.reactnativecnblogs", 54 | res = "src/main/res", 55 | ) 56 | 57 | android_binary( 58 | name = "app", 59 | keystore = "//android/keystores:debug", 60 | manifest = "src/main/AndroidManifest.xml", 61 | package_type = "debug", 62 | deps = [ 63 | ":app-code", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /android/app/my-release-key.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/my-release-key.keystore -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # okhttp 54 | 55 | -keepattributes Signature 56 | -keepattributes *Annotation* 57 | -keep class okhttp3.** { *; } 58 | -keep interface okhttp3.** { *; } 59 | -dontwarn okhttp3.** 60 | 61 | # okio 62 | 63 | -keep class sun.misc.Unsafe { *; } 64 | -dontwarn java.nio.file.* 65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 66 | -dontwarn okio.** 67 | -------------------------------------------------------------------------------- /android/app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 博客苑 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /android/app/src/main/java/com/reactnativecnblogs/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.reactnativecnblogs; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "reactNativeCnblogs"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/reactnativecnblogs/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.reactnativecnblogs; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.facebook.react.ReactNativeHost; 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.shell.MainReactPackage; 9 | import com.facebook.soloader.SoLoader; 10 | import com.oblador.vectoricons.VectorIconsPackage; 11 | import com.remobile.toast.RCTToastPackage; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | public class MainApplication extends Application implements ReactApplication { 17 | 18 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 19 | @Override 20 | public boolean getUseDeveloperSupport() { 21 | return BuildConfig.DEBUG; 22 | } 23 | 24 | @Override 25 | protected List getPackages() { 26 | return Arrays.asList( 27 | new MainReactPackage(), 28 | new VectorIconsPackage(), 29 | new RCTToastPackage() 30 | ); 31 | } 32 | }; 33 | 34 | @Override 35 | public ReactNativeHost getReactNativeHost() { 36 | return mReactNativeHost; 37 | } 38 | 39 | @Override 40 | public void onCreate() { 41 | super.onCreate(); 42 | SoLoader.init(this, /* native exopackage */ false); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 博客园 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | mavenLocal() 18 | jcenter() 19 | maven { 20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 21 | url "$rootDir/../node_modules/react-native/android" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | MYAPP_RELEASE_STORE_FILE=my-release-key.keystore 22 | MYAPP_RELEASE_KEY_ALIAS=my-key-alias 23 | MYAPP_RELEASE_STORE_PASSWORD=password 24 | MYAPP_RELEASE_KEY_PASSWORD=password 25 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 6 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = "debug", 3 | properties = "debug.keystore.properties", 4 | store = "debug.keystore", 5 | visibility = [ 6 | "PUBLIC", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'reactNativeCnblogs' 2 | 3 | include ':app' 4 | include ':react-native-vector-icons' 5 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') 6 | include ':react-native-toast' 7 | project(':react-native-toast').projectDir = new File(settingsDir, '../node_modules/@remobile/react-native-toast/android') -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactNativeCnblogs", 3 | "displayName": "博客园" 4 | } -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | import { 2 | AppRegistry, 3 | } from 'react-native'; 4 | import reactNativeCnblogs from './source'; 5 | 6 | AppRegistry.registerComponent('reactNativeCnblogs', () => reactNativeCnblogs); -------------------------------------------------------------------------------- /index.ios.js: -------------------------------------------------------------------------------- 1 | import { 2 | AppRegistry, 3 | } from 'react-native'; 4 | import reactNativeCnblogs from './source'; 5 | 6 | AppRegistry.registerComponent('reactNativeCnblogs', () => reactNativeCnblogs); -------------------------------------------------------------------------------- /ios/bundle/main.jsbundle.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/bundle/main.jsbundle.meta -------------------------------------------------------------------------------- /ios/reactNativeCnblogs-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ios/reactNativeCnblogs-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "AppDelegate.h" 11 | 12 | #import 13 | #import 14 | 15 | @implementation AppDelegate 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 18 | { 19 | NSURL *jsCodeLocation; 20 | 21 | jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; 22 | 23 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 24 | moduleName:@"reactNativeCnblogs" 25 | initialProperties:nil 26 | launchOptions:launchOptions]; 27 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 28 | 29 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 30 | UIViewController *rootViewController = [UIViewController new]; 31 | rootViewController.view = rootView; 32 | self.window.rootViewController = rootViewController; 33 | [self.window makeKeyAndVisible]; 34 | return YES; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40@2x-1.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-72.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-Small@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/Icon-Small@3x-1.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/cnblogs_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/AppIcon.appiconset/cnblogs_icon.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "736h", 7 | "filename" : "Default1242x2208.png", 8 | "minimum-system-version" : "8.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "extent" : "full-screen", 14 | "idiom" : "iphone", 15 | "subtype" : "667h", 16 | "filename" : "Default750x1334.png", 17 | "minimum-system-version" : "8.0", 18 | "orientation" : "portrait", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "orientation" : "portrait", 23 | "idiom" : "iphone", 24 | "filename" : "Default640x960.png", 25 | "extent" : "full-screen", 26 | "minimum-system-version" : "7.0", 27 | "scale" : "2x" 28 | }, 29 | { 30 | "extent" : "full-screen", 31 | "idiom" : "iphone", 32 | "subtype" : "retina4", 33 | "filename" : "Default640x1136.png", 34 | "minimum-system-version" : "7.0", 35 | "orientation" : "portrait", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "orientation" : "portrait", 40 | "idiom" : "iphone", 41 | "extent" : "full-screen", 42 | "scale" : "1x" 43 | }, 44 | { 45 | "orientation" : "portrait", 46 | "idiom" : "iphone", 47 | "extent" : "full-screen", 48 | "scale" : "2x" 49 | }, 50 | { 51 | "orientation" : "portrait", 52 | "idiom" : "iphone", 53 | "extent" : "full-screen", 54 | "subtype" : "retina4", 55 | "scale" : "2x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default1242x2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default1242x2208.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default640x1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default640x1136.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default640x960.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default640x960.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default750x1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/ios/reactNativeCnblogs/Images.xcassets/LaunchImage.launchimage/Default750x1334.png -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | zh_CN 7 | CFBundleDisplayName 8 | reactNativeCnblogs 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | 博客园 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | CodePushDeploymentKey 26 | Rmyzlv-ezw9bCaJyV-60CsN_LMxzNyj6RPzEb 27 | LSRequiresIPhoneOS 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSLocationWhenInUseUsageDescription 35 | LaunchScreen 36 | UIAppFonts 37 | 38 | Entypo.ttf 39 | EvilIcons.ttf 40 | FontAwesome.ttf 41 | Foundation.ttf 42 | Ionicons.ttf 43 | MaterialIcons.ttf 44 | Octicons.ttf 45 | Zocial.ttf 46 | 47 | UIRequiredDeviceCapabilities 48 | 49 | armv7 50 | 51 | UISupportedInterfaceOrientations 52 | 53 | UIInterfaceOrientationPortrait 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | UIViewControllerBasedStatusBarAppearance 58 | 59 | NSLocationWhenInUseUsageDescription 60 | 61 | NSAppTransportSecurity 62 | 63 | 64 | NSExceptionDomains 65 | 66 | localhost 67 | 68 | NSExceptionAllowsInsecureHTTPLoads 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /ios/reactNativeCnblogs/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "AppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/reactNativeCnblogsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/reactNativeCnblogsTests/reactNativeCnblogsTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import 14 | #import 15 | 16 | #define TIMEOUT_SECONDS 600 17 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 18 | 19 | @interface reactNativeCnblogsTests : XCTestCase 20 | 21 | @end 22 | 23 | @implementation reactNativeCnblogsTests 24 | 25 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 26 | { 27 | if (test(view)) { 28 | return YES; 29 | } 30 | for (UIView *subview in [view subviews]) { 31 | if ([self findSubviewInView:subview matching:test]) { 32 | return YES; 33 | } 34 | } 35 | return NO; 36 | } 37 | 38 | - (void)testRendersWelcomeScreen 39 | { 40 | UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 41 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 42 | BOOL foundElement = NO; 43 | 44 | __block NSString *redboxError = nil; 45 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 46 | if (level >= RCTLogLevelError) { 47 | redboxError = message; 48 | } 49 | }); 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | RCTSetLogFunction(RCTDefaultLogFunction); 64 | 65 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 66 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 67 | } 68 | 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactNativeCnblogs", 3 | "version": "3.6.0", 4 | "author": { 5 | "name": "togayther", 6 | "email": "sleepsleepsleep@foxmail.com" 7 | }, 8 | "license": "MIT", 9 | "keywords": [ 10 | "react-native", 11 | "react-component", 12 | "ios", 13 | "android", 14 | "cnblog", 15 | "cnblogs", 16 | "mobile" 17 | ], 18 | "homepage": "https://github.com/togayther/react-native-cnblogs", 19 | "bugs": { 20 | "url": "https://github.com/togayther/react-native-cnblogs/issues" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/togayther/react-native-cnblogs.git" 25 | }, 26 | "scripts": { 27 | "log": "adb logcat *:S ReactNative:V ReactNativeJS:V", 28 | "reverse": "adb reverse tcp:8081 tcp:8081", 29 | "start": "node node_modules/react-native/local-cli/cli.js start", 30 | "test-android": "cd android && gradlew installRelease", 31 | "release-android": "cd android && gradlew assembleRelease", 32 | "release-ios": "react-native bundle --entry-file index.ios.js --platform ios --dev false --bundle-output ./ios/bundle/main.jsbundle --assets-dest ./ios/bundle", 33 | "update-android": "code-push release-react cnblogs android", 34 | "update-android-production": "code-push promote cnblogs Staging Production" 35 | }, 36 | "dependencies": { 37 | "@remobile/react-native-toast": "^1.0.5", 38 | "entities": "^1.1.1", 39 | "flux-standard-action": "^1.2.0", 40 | "lodash": "^4.17.4", 41 | "moment": "^2.18.1", 42 | "react": "^16.0.0-alpha.6", 43 | "react-addons-pure-render-mixin": "^15.5.2", 44 | "react-native": "^0.43.3", 45 | "react-native-action-button": "^2.6.8", 46 | "react-native-animatable": "^1.1.1", 47 | "react-native-drawer": "^2.3.0", 48 | "react-native-html-converter": "^1.0.4", 49 | "react-native-parallax-scroll-view": "^0.19.0", 50 | "react-native-scrollable-tab-view": "^0.7.4", 51 | "react-native-vector-icons": "^4.0.1", 52 | "react-redux": "^4.4.8", 53 | "react-timer-mixin": "^0.13.3", 54 | "redux": "^3.6.0", 55 | "redux-actions": "^2.0.1", 56 | "redux-thunk": "^2.2.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /screenshot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/screenshot/1.png -------------------------------------------------------------------------------- /screenshot/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/screenshot/2.png -------------------------------------------------------------------------------- /screenshot/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/screenshot/3.png -------------------------------------------------------------------------------- /source/action/author.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | import * as types from '../constant/actiontype'; 3 | import * as authorService from '../service/authorService'; 4 | 5 | export const getAuthorByRank = createAction( 6 | types.FETCH_AUTHORS_BY_RANK, 7 | async(params)=> { 8 | return await authorService.getAuthorsByRank({ 9 | pageIndex: 1, 10 | ...params 11 | }); 12 | } 13 | ); 14 | 15 | export const getAuthorsByKey = createAction( 16 | types.FETCH_AUTHORS_BY_KEY, 17 | async(key)=> { 18 | return await authorService.getAuthorsByKey(key); 19 | }, 20 | (key)=> { 21 | return { 22 | pending: true, 23 | key 24 | } 25 | } 26 | ); 27 | 28 | export const getAuthorDetail = createAction( 29 | types.FETCH_AUTHOR_DETAIL, 30 | async(blogger)=> { 31 | return await authorService.getAuthorDetail(blogger); 32 | }, 33 | (blogger)=> { 34 | return { 35 | pending: true, 36 | blogger 37 | } 38 | } 39 | ); 40 | 41 | export const getAuthorPosts = createAction( 42 | types.FETCH_AUTHOR_POSTS, 43 | async(blogger)=> { 44 | return await authorService.getAuthorPosts(blogger, { 45 | pageIndex: 1 46 | }); 47 | }, 48 | (blogger)=> { 49 | return { 50 | pending: true, 51 | blogger 52 | } 53 | } 54 | ); 55 | 56 | export const getAuthorPostsWithPage = createAction( 57 | types.FETCH_AUTHOR_POSTS_WITHPAGE, 58 | async(blogger, params)=> { 59 | return await authorService.getAuthorPosts(blogger, params); 60 | }, 61 | (blogger)=> { 62 | return { 63 | pending: true, 64 | blogger 65 | } 66 | } 67 | ); 68 | 69 | export const clearAuthorSearchResult = createAction( 70 | types.CLEAR_AUTHOR_SEARCH_RESULT 71 | ); -------------------------------------------------------------------------------- /source/action/comment.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | import * as types from '../constant/actiontype'; 3 | import * as commentService from '../service/commentService'; 4 | 5 | export const getCommentsByPost = createAction( 6 | types.FETCH_COMMENTS_BY_POST, 7 | async(category, id, params)=> { 8 | return await commentService.getCommentsByPost(category, id, { 9 | pageIndex: 1, 10 | ...params 11 | }); 12 | }, 13 | (category, id)=>{ 14 | return { 15 | pending: true, 16 | category, 17 | id 18 | } 19 | } 20 | ); 21 | 22 | export const getCommentsByPostWithPage = createAction( 23 | types.FETCH_COMMENTS_BY_POST_WITHPAGE, 24 | async(category, id, params)=> { 25 | return await commentService.getCommentsByPost(category, id, params); 26 | }, 27 | (category, id)=> { 28 | return { 29 | pending: true, 30 | category, 31 | id 32 | } 33 | } 34 | ); 35 | 36 | export const addComment = createAction(types.ADD_COMMENT, 37 | async({category, params, data})=>{ 38 | return await commentService.addComment(category, params, data); 39 | }, 40 | ({category, params, resolved, rejected})=> { 41 | return { 42 | pending: true, 43 | category, 44 | id: params.id, 45 | resolved, 46 | rejected 47 | } 48 | } 49 | ); -------------------------------------------------------------------------------- /source/action/common.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions'; 3 | import _ from 'lodash'; 4 | import * as types from '../constant/actiontype'; 5 | 6 | export const message = createAction(types.SHOW_MESSAGE, (text)=> { 7 | let id = _.uniqueId(); 8 | return { 9 | id: id, 10 | text 11 | } 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /source/action/config.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions'; 3 | import * as types from '../constant/actiontype'; 4 | import * as storageService from '../service/storage'; 5 | 6 | export const updateConfig = createAction(types.UPDATE_CONFIG, async({key, value})=> { 7 | return storageService.mergeItem(key, value); 8 | }, ({key, value, resolved, rejected})=>{ 9 | return { 10 | key, 11 | value, 12 | resolved, 13 | rejected 14 | } 15 | }); 16 | 17 | export const removeConfig = createAction(types.REMOVE_CONFIG, async({key})=>{ 18 | return storageService.removeItem(key); 19 | }, ({key})=>{ 20 | return { 21 | key 22 | } 23 | }); 24 | 25 | export const getConfig = createAction(types.GET_CONFIG, async({key})=> { 26 | return await storageService.getItem(key); 27 | }, ({key, resolved, rejected})=>{ 28 | return { 29 | key, 30 | resolved, 31 | rejected 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /source/action/offline.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions'; 3 | import _ from 'lodash'; 4 | import { storageKey } from '../config'; 5 | import * as types from '../constant/actiontype'; 6 | import * as storageService from '../service/storage'; 7 | 8 | export const savePost = createAction(types.OFFLINE_POST_TO_STORAGE, async(postData)=> { 9 | return storageService.mergeItem(storageKey.OFFLINE_POSTS, postData); 10 | }); 11 | 12 | export const getPost = createAction(types.GET_POST_FROM_STORAGE, async(id)=> { 13 | return storageService.getItem(storageKey.OFFLINE_POSTS).then((posts)=>{ 14 | if (posts && posts[id]) { 15 | return posts[id].postContent; 16 | } 17 | return null; 18 | }); 19 | }); 20 | 21 | export const getPosts = createAction(types.GET_POSTS_FROM_STORAGE, async()=> { 22 | return storageService.getItem(storageKey.OFFLINE_POSTS); 23 | }); 24 | 25 | export const removePosts = createAction(types.REMOVE_POSTS_IN_STORAGE, async()=> { 26 | return storageService.removeItem(storageKey.OFFLINE_POSTS); 27 | }); 28 | 29 | export const removePost = createAction(types.REMOVE_POST_IN_STORAGE, async(id)=> { 30 | return storageService.getItem(storageKey.OFFLINE_POSTS).then((posts)=>{ 31 | delete posts[id]; 32 | storageService.setItem(storageKey.OFFLINE_POSTS, posts); 33 | }); 34 | }, (id)=> { 35 | return { 36 | id 37 | } 38 | }); -------------------------------------------------------------------------------- /source/action/post.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | import * as types from '../constant/actiontype'; 3 | import * as postService from '../service/postService'; 4 | 5 | export const getPostByCategory = createAction( 6 | types.FETCH_POSTS_BY_CATEGORY, 7 | async(category)=> { 8 | return await postService.getPostByCategory(category, { 9 | pageIndex: 1 10 | }); 11 | }, 12 | (category)=> { 13 | return { 14 | pending: true, 15 | category 16 | } 17 | } 18 | ); 19 | 20 | export const getPostByCategoryWithPage = createAction( 21 | types.FETCH_POSTS_BY_CATEGORY_WITHPAGE, 22 | async(category, params)=> { 23 | return await postService.getPostByCategory(category, params); 24 | }, 25 | (category)=> { 26 | return { 27 | pending: true, 28 | category 29 | } 30 | } 31 | ); 32 | 33 | export const getPostById = createAction(types.FETCH_POST_BY_ID, 34 | async(category, id)=>{ 35 | return await postService.getPostById(category, id); 36 | }, 37 | (category, id)=> { 38 | return { 39 | pending: true, 40 | category, 41 | id 42 | } 43 | } 44 | ); 45 | 46 | export const addPost = createAction(types.ADD_POST, 47 | async({category, data})=>{ 48 | return await postService.addPost(category, data); 49 | }, 50 | ({category, data, resolved, rejected})=> { 51 | return { 52 | pending: true, 53 | url: data.LinkUrl, 54 | category, 55 | resolved, 56 | rejected 57 | } 58 | } 59 | ); 60 | 61 | export const removePost = createAction(types.REMOVE_POST, 62 | async({category, params})=>{ 63 | return await postService.removePost(category, params); 64 | }, 65 | ({category, params, resolved, rejected})=> { 66 | return { 67 | pending: true, 68 | id: params.id, 69 | url: params.url, 70 | category, 71 | resolved, 72 | rejected 73 | } 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /source/action/search.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | import * as types from '../constant/actiontype'; 3 | import * as searchService from '../service/searchService'; 4 | 5 | export const searchByKey = createAction( 6 | types.SEARCH_BY_KEY, 7 | async(category, key)=> { 8 | return await searchService.searchByKey(category, key, { 9 | pageIndex: 1 10 | }); 11 | }, 12 | (category, key)=> { 13 | return { 14 | pending: true, 15 | category, 16 | key 17 | } 18 | } 19 | ); 20 | 21 | export const searchByKeyWithPage = createAction( 22 | types.SEARCH_BY_KEY_WITHPAGE, 23 | async(category, key, params)=> { 24 | return await searchService.searchByKey(category, key, params); 25 | }, 26 | (category, key)=> { 27 | return { 28 | pending: true, 29 | category, 30 | key 31 | } 32 | } 33 | ); 34 | 35 | export const clearSearchResult = createAction( 36 | types.CLEAR_SEARCH_RESULT, 37 | null, 38 | (category)=> { 39 | return { 40 | category: category 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /source/action/update.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | import * as types from '../constant/actiontype'; 3 | import * as updateService from '../service/updateService'; 4 | 5 | export const getUpdateInfo = createAction( 6 | types.FETCH_UPDATE_INFO, 7 | async(version)=> { 8 | return await updateService.getUpdateInfo(version); 9 | } 10 | ) -------------------------------------------------------------------------------- /source/action/user.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | import * as types from '../constant/actiontype'; 3 | import * as userService from '../service/userService'; 4 | 5 | export const login = createAction( 6 | types.LOGIN, 7 | async({username, password})=> { 8 | return await userService.login(username, password); 9 | }, 10 | ({username, resolved, rejected})=> { 11 | return { 12 | username, 13 | resolved, 14 | rejected 15 | } 16 | } 17 | ); 18 | 19 | export const refreshToken = createAction( 20 | types.REFRESH_TOKEN, 21 | async({token})=> { 22 | return await userService.refreshToken(token); 23 | }, 24 | ({token, resolved, rejected})=> { 25 | return { 26 | token, 27 | resolved, 28 | rejected 29 | } 30 | } 31 | ); 32 | 33 | export const getUserInfo = createAction( 34 | types.FETCH_USER_INFO, 35 | async()=> { 36 | return await userService.getUserInfo(); 37 | }, 38 | ({resolved, rejected} = {})=> { 39 | return { 40 | resolved, 41 | rejected 42 | } 43 | } 44 | ) 45 | 46 | export const getUserAssetByCategory = createAction( 47 | types.FETCH_USER_ASSET, 48 | async(category, params = {})=> { 49 | params.pageIndex = 1; 50 | return await userService.getUserAsset(category, params); 51 | }, 52 | (category)=> { 53 | return { 54 | pending: true, 55 | category 56 | } 57 | } 58 | ) 59 | 60 | export const getUserAssetByCategoryWithPage = createAction( 61 | types.FETCH_USER_ASSET_WITHPAGE, 62 | async(category, params)=> { 63 | return await userService.getUserAsset(category, params); 64 | }, 65 | (category)=> { 66 | return { 67 | pending: true, 68 | category 69 | } 70 | } 71 | ); -------------------------------------------------------------------------------- /source/common/updater.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | NetInfo 3 | } from 'react-native'; 4 | 5 | import codePush from 'react-native-code-push'; 6 | 7 | // check the code-push doc: 8 | // https://github.com/Microsoft/react-native-code-push#getting-started 9 | 10 | export function getNetStatus() { 11 | return NetInfo.fetch().then((netinfo=> { 12 | return netinfo.toUpperCase() == 'WIFI'; 13 | })); 14 | } 15 | 16 | export function callCodePush(){ 17 | 18 | //隐式更新 19 | codePush.sync(); 20 | 21 | /* 22 | let updateDialogOptions = { 23 | title: "更新提示", 24 | optionalUpdateMessage: "检测到可用的新版本,是否升级?", 25 | optionalIgnoreButtonLabel: "无视", 26 | optionalInstallButtonLabel: "升级", 27 | }; 28 | //显式更新 29 | codePush.sync( 30 | { 31 | updateDialog: updateDialogOptions 32 | }, 33 | (status)=>{ 34 | if (status == codePush.SyncStatus.INSTALLING_UPDATE) { 35 | 36 | } 37 | } 38 | ); 39 | */ 40 | } 41 | 42 | export function update() { 43 | getNetStatus().done((status)=>status && callCodePush()); 44 | } -------------------------------------------------------------------------------- /source/component/button/home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | StyleSheet, 4 | } from 'react-native'; 5 | import TimerMixin from 'react-timer-mixin'; 6 | import ActionButton from 'react-native-action-button'; 7 | import Icon from 'react-native-vector-icons/Ionicons'; 8 | import ViewPage from '../view'; 9 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 10 | 11 | const buttons = [{ 12 | title:'闪存', 13 | icon: 'ios-color-palette-outline', 14 | action:'push', 15 | view: 'blinkAdd', 16 | color: StyleConfig.color_primary 17 | },{ 18 | title:'博问', 19 | icon: 'ios-document-outline', 20 | action:'push', 21 | view: 'questionAdd', 22 | color: StyleConfig.color_primary 23 | }]; 24 | 25 | class HomeButton extends Component { 26 | 27 | constructor(props) { 28 | super(props); 29 | } 30 | 31 | componentWillUnmount() { 32 | this.timer && TimerMixin.clearTimeout(this.timer); 33 | } 34 | 35 | onButtonPress(item){ 36 | const { router } = this.props; 37 | if(router && router[item.action] && ViewPage[item.view]){ 38 | this.timer = TimerMixin.setTimeout(() => { 39 | router[item.action](ViewPage[item.view]()); 40 | }, 500); 41 | } 42 | } 43 | 44 | renderButtonItem(item, index){ 45 | return ( 46 | this.onButtonPress(item) } 51 | buttonColor = { item.color } 52 | titleColor = { StyleConfig.color_white } 53 | textContainerStyle = { ComponentStyles.action_button_container } 54 | textStyle = { ComponentStyles.action_button_text } 55 | titleBgColor = { item.color }> 56 | 57 | 58 | ) 59 | } 60 | 61 | renderButtonIcon(){ 62 | return ( 63 | 64 | ) 65 | } 66 | 67 | render() { 68 | return ( 69 | 78 | { 79 | buttons && buttons.map((button, index)=>{ 80 | return this.renderButtonItem(button, index) 81 | }) 82 | } 83 | 84 | ) 85 | } 86 | } 87 | 88 | export default HomeButton; -------------------------------------------------------------------------------- /source/component/button/post.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | StyleSheet, 4 | } from 'react-native'; 5 | import TimerMixin from 'react-timer-mixin'; 6 | import ActionButton from 'react-native-action-button'; 7 | import Icon from 'react-native-vector-icons/Ionicons'; 8 | import { ComponentStyles, StyleConfig } from '../../style'; 9 | 10 | const buttons = [{ 11 | title:'评论', 12 | icon: 'ios-text-outline', 13 | action:'onCommentPress', 14 | color: StyleConfig.color_primary 15 | },{ 16 | title:'离线', 17 | icon: 'ios-download-outline', 18 | action:'onOfflinePress', 19 | color: StyleConfig.color_primary 20 | },{ 21 | title:'收藏', 22 | icon: 'ios-filing-outline', 23 | action:'onFavoritePress', 24 | color: StyleConfig.color_primary 25 | }]; 26 | 27 | class PostButton extends Component { 28 | 29 | constructor(props) { 30 | super(props); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.timer && TimerMixin.clearTimeout(this.timer); 35 | } 36 | 37 | onButtonPress(item){ 38 | if(this.props[item.action]){ 39 | this.timer = TimerMixin.setTimeout(() => { 40 | this.props[item.action](item); 41 | }, 500); 42 | } 43 | } 44 | 45 | renderButtonItem(item, index){ 46 | return ( 47 | this.onButtonPress(item) }> 56 | 57 | 58 | ) 59 | } 60 | 61 | renderButtonIcon(){ 62 | return ( 63 | 64 | ) 65 | } 66 | 67 | render() { 68 | return ( 69 | 78 | { 79 | buttons && buttons.map((button, index)=>{ 80 | return this.renderButtonItem(button, index) 81 | }) 82 | } 83 | 84 | ) 85 | } 86 | } 87 | 88 | export default PostButton; -------------------------------------------------------------------------------- /source/component/button/search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | StyleSheet, 4 | } from 'react-native'; 5 | import TimerMixin from 'react-timer-mixin'; 6 | import ActionButton from 'react-native-action-button'; 7 | import Icon from 'react-native-vector-icons/Ionicons'; 8 | import { ComponentStyles, StyleConfig } from '../../style'; 9 | 10 | const buttons = [{ 11 | title:'离线', 12 | icon: 'ios-download-outline', 13 | action:'onOfflinePress', 14 | color: StyleConfig.color_primary 15 | },{ 16 | title:'收藏', 17 | icon: 'ios-filing-outline', 18 | action:'onFavoritePress', 19 | color: StyleConfig.color_primary 20 | }]; 21 | 22 | class SearchButton extends Component { 23 | 24 | constructor(props) { 25 | super(props); 26 | } 27 | 28 | componentWillUnmount() { 29 | this.timer && TimerMixin.clearTimeout(this.timer); 30 | } 31 | 32 | onButtonPress(item){ 33 | if(this.props[item.action]){ 34 | this.timer = TimerMixin.setTimeout(() => { 35 | this.props[item.action](item); 36 | }, 500); 37 | } 38 | } 39 | 40 | renderButtonItem(item, index){ 41 | return ( 42 | this.onButtonPress(item) }> 51 | 52 | 53 | ) 54 | } 55 | 56 | renderButtonIcon(){ 57 | return ( 58 | 59 | ) 60 | } 61 | 62 | render() { 63 | return ( 64 | 73 | { 74 | buttons && buttons.map((button, index)=>{ 75 | return this.renderButtonItem(button, index) 76 | }) 77 | } 78 | 79 | ) 80 | } 81 | } 82 | 83 | export default SearchButton; -------------------------------------------------------------------------------- /source/component/button/single.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Icon from 'react-native-vector-icons/Ionicons'; 3 | import ActionButton from 'react-native-action-button'; 4 | import { ComponentStyles, StyleConfig } from '../../style'; 5 | 6 | class SingleButton extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | renderButtonIcon(){ 13 | const { icon = 'ios-arrow-round-back' } = this.props; 14 | return ( 15 | 19 | ) 20 | } 21 | 22 | render() { 23 | const { 24 | onPress = ()=>null, 25 | color = StyleConfig.action_color_primary, 26 | position ='left', 27 | offsetX = StyleConfig.action_offset_x, 28 | offsetY = StyleConfig.action_offset_y 29 | } = this.props; 30 | 31 | return ( 32 | onPress() } 39 | hideShadow = { true } 40 | icon = { this.renderButtonIcon() }> 41 | 42 | ) 43 | } 44 | } 45 | 46 | 47 | export default SingleButton; 48 | 49 | 50 | -------------------------------------------------------------------------------- /source/component/endtag.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Text, 4 | View 5 | } from 'react-native'; 6 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 7 | import { CommonStyles } from '../style'; 8 | 9 | class EndTag extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 14 | } 15 | 16 | render() { 17 | const { text = "— 我是有底线的 —" } = this.props; 18 | return ( 19 | 20 | 21 | { text } 22 | 23 | 24 | ) 25 | } 26 | } 27 | 28 | export default EndTag; 29 | 30 | 31 | -------------------------------------------------------------------------------- /source/component/hintMessage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text 5 | } from 'react-native'; 6 | 7 | import Icon from 'react-native-vector-icons/Ionicons'; 8 | import { CommonStyles, ComponentStyles, StyleConfig } from '../style'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | 11 | class HintMessage extends Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 16 | } 17 | 18 | render() { 19 | const { message = '- 这里什么都没有 -' } = this.props; 20 | return ( 21 | 22 | 23 | { message } 24 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | export default HintMessage; 31 | 32 | 33 | -------------------------------------------------------------------------------- /source/component/imageBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Image, 5 | ActivityIndicator 6 | } from 'react-native'; 7 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 8 | import { CommonStyles, StyleConfig } from '../style'; 9 | 10 | const defaultMaxWidth = StyleConfig.screen_width - ( StyleConfig.space_3 * 2 ); 11 | 12 | class ImageBox extends Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | loading: true 18 | }; 19 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 20 | } 21 | 22 | onImageLoadEnd(){ 23 | const { uri, maxWidth = defaultMaxWidth } = this.props; 24 | this.setState({ 25 | loading: false 26 | }); 27 | Image.getSize && Image.getSize(uri, (width, height)=> { 28 | if (width >= maxWidth) { 29 | height = (maxWidth / width) * height; 30 | width = maxWidth; 31 | } 32 | this.image && this.image.setNativeProps({ 33 | style: { 34 | width: width, 35 | height: height 36 | } 37 | }); 38 | },() => null); 39 | } 40 | 41 | render() { 42 | const { uri, style } = this.props; 43 | return ( 44 | this.image = view } 46 | source={ {uri: uri} } 47 | style={ style } 48 | onLoadEnd={ ()=> this.onImageLoadEnd() }> 49 | { 50 | this.state.loading? 51 | 52 | 53 | 54 | : null 55 | } 56 | 57 | ) 58 | } 59 | } 60 | 61 | export default ImageBox; 62 | 63 | 64 | -------------------------------------------------------------------------------- /source/component/listview/authorPostList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import * as AuthorAction from '../../action/author'; 11 | import AuthorPostRow from './authorPostRow'; 12 | import EndTag from '../endtag'; 13 | import Spinner from '../spinner'; 14 | import ViewPage from '../view'; 15 | import { postCategory } from '../../config'; 16 | 17 | const category = postCategory.home; 18 | 19 | class AuthorPostList extends Component { 20 | constructor(props) { 21 | super(props); 22 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 23 | this.state = { 24 | dataSource: dataSource.cloneWithRows(props.posts), 25 | scrollButtonVisiable: false 26 | }; 27 | 28 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 29 | } 30 | 31 | componentWillReceiveProps(nextProps) { 32 | if (nextProps.posts && nextProps.posts.length && nextProps.posts !== this.props.posts) { 33 | this.setState({ 34 | dataSource: this.state.dataSource.cloneWithRows(nextProps.posts) 35 | }); 36 | } 37 | } 38 | 39 | renderListFooter() { 40 | const { ui } = this.props; 41 | if (ui.postPagePending) { 42 | return ; 43 | } 44 | if(ui.postPageEnabled!==true){ 45 | return ; 46 | } 47 | } 48 | 49 | formatAuthorPostDate(post){ 50 | if(post.Avatar){ 51 | post.Avatar = this.props.avatar; 52 | } 53 | post.AuthorEnabled = false; 54 | return post; 55 | } 56 | 57 | onListRowClick(post){ 58 | const postInfo = this.formatAuthorPostDate(post); 59 | this.props.router.push(ViewPage.post(), { 60 | id: postInfo.Id, 61 | post: postInfo, 62 | category 63 | }); 64 | } 65 | 66 | renderListRow(post) { 67 | if(post && post.Id){ 68 | return ( 69 | 73 | ) 74 | } 75 | } 76 | 77 | 78 | render() { 79 | return ( 80 | this.renderListRow(e) } 90 | renderFooter={ (e)=>this.renderListFooter(e) }> 91 | 92 | ); 93 | } 94 | } 95 | 96 | export default connect((state, props) => ({ 97 | posts : state.author[props.blogger].posts, 98 | ui: state.authorUI[props.blogger] 99 | }), dispatch => ({ 100 | authorAction : bindActionCreators(AuthorAction, dispatch) 101 | }))(AuthorPostList); -------------------------------------------------------------------------------- /source/component/listview/authorPostRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import _ from 'lodash'; 10 | import moment from 'moment'; 11 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 12 | import Config from '../../config'; 13 | import { decodeHTML, getBloggerAvatar } from '../../common'; 14 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 15 | 16 | class AuthorPostRow extends Component { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 21 | } 22 | 23 | getPostInfo(){ 24 | let { post } = this.props; 25 | let postInfo = {}; 26 | if (post && post.Id) { 27 | postInfo.Id = post.Id; 28 | postInfo.ViewCount = post.ViewCount; 29 | postInfo.CommentCount = post.CommentCount; 30 | postInfo.Title = decodeHTML(post.Title); 31 | if (post.Description) { 32 | postInfo.Description = _.truncate(decodeHTML(post.Description), { length : 70 }); 33 | } 34 | postInfo.DateAdded = moment(post.PostDate).startOf('minute').fromNow(); 35 | postInfo.Author = decodeHTML(post.Author); 36 | postInfo.blogger = post.BlogApp; 37 | postInfo.Avatar = getBloggerAvatar(post.Avatar); 38 | } 39 | return postInfo; 40 | } 41 | 42 | renderPostTitle(postInfo){ 43 | return ( 44 | 45 | 46 | { postInfo.Title } 47 | 48 | 49 | ) 50 | } 51 | 52 | renderPostDescr(postInfo){ 53 | return ( 54 | 55 | 56 | { postInfo.Description } 57 | 58 | 59 | ) 60 | } 61 | 62 | renderPostMeta(postInfo){ 63 | return ( 64 | 65 | 66 | { postInfo.DateAdded } 67 | 68 | 69 | 70 | 71 | { postInfo.CommentCount + ' / ' + postInfo.ViewCount } 72 | 73 | 74 | 75 | ) 76 | } 77 | 78 | render() { 79 | const postInfo = this.getPostInfo(); 80 | return ( 81 | { this.props.onRowPress(postInfo) }} 83 | underlayColor={ StyleConfig.touchable_press_color } 84 | key={ postInfo.Id }> 85 | 86 | 87 | { this.renderPostTitle(postInfo) } 88 | { this.renderPostDescr(postInfo) } 89 | { this.renderPostMeta(postInfo) } 90 | 91 | 92 | ) 93 | } 94 | } 95 | 96 | export default AuthorPostRow; 97 | -------------------------------------------------------------------------------- /source/component/listview/blinkList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import * as PostAction from '../../action/post'; 11 | import BlinkRow from './blinkRow'; 12 | import Spinner from '../spinner'; 13 | import EndTag from '../endtag'; 14 | import ViewPage from '../view'; 15 | import HintMessage from '../hintMessage'; 16 | import { postCategory } from '../../config'; 17 | 18 | const category = postCategory.blink; 19 | 20 | class BlinkList extends Component { 21 | constructor(props) { 22 | super(props); 23 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 24 | this.state = { 25 | dataSource: dataSource.cloneWithRows(props.blinks||{}), 26 | }; 27 | 28 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 29 | } 30 | 31 | componentWillReceiveProps(nextProps) { 32 | if (nextProps.blinks && nextProps.blinks.length && nextProps.blinks !== this.props.blinks) { 33 | this.setState({ 34 | dataSource: this.state.dataSource.cloneWithRows(nextProps.blinks) 35 | }); 36 | } 37 | } 38 | 39 | renderListFooter() { 40 | const { ui, blinks = {} } = this.props; 41 | if (ui.pagePending) { 42 | return ; 43 | } 44 | if(ui.refreshPending!==true && ui.pageEnabled!==true && blinks.length){ 45 | return ; 46 | } 47 | } 48 | 49 | onListRowPress(blink){ 50 | this.props.router.push(ViewPage.blink(), { 51 | id: blink.Id, 52 | category: category, 53 | blink 54 | }); 55 | } 56 | 57 | renderListRow(blink) { 58 | if(blink && blink.Id){ 59 | return ( 60 | this.onListRowPress(e) } /> 65 | ) 66 | } 67 | } 68 | 69 | render() { 70 | const { ui, blinks } = this.props; 71 | if(ui.refreshPending!==true && (!blinks || !blinks.length)){ 72 | return ; 73 | } 74 | 75 | return ( 76 | this.listView = view } 78 | removeClippedSubviews 79 | enableEmptySections = { true } 80 | onEndReachedThreshold={ 10 } 81 | initialListSize={ 10 } 82 | pageSize = { 10 } 83 | pagingEnabled={ false } 84 | scrollRenderAheadDistance={ 150 } 85 | dataSource={ this.state.dataSource } 86 | renderRow={ (e)=>this.renderListRow(e) } 87 | renderFooter={ (e)=>this.renderListFooter(e) }> 88 | 89 | ) 90 | } 91 | } 92 | 93 | export default connect((state, props) => ({ 94 | blinks: state.post[category], 95 | ui: state.postListUI[category] 96 | }), dispatch => ({ 97 | postAction : bindActionCreators(PostAction, dispatch) 98 | }))(BlinkList); -------------------------------------------------------------------------------- /source/component/listview/blinkRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import moment from 'moment'; 10 | import _ from 'lodash'; 11 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 12 | import Icon from 'react-native-vector-icons/Ionicons'; 13 | import { decodeHTML, getBloggerAvatar } from '../../common'; 14 | import { CommonStyles, ComponentStyles, StyleConfig } from '../../style'; 15 | 16 | class BlinkRow extends Component { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 21 | } 22 | 23 | getBlinkInfo(){ 24 | let { blink } = this.props; 25 | let blinkInfo = {}; 26 | if (blink && blink.Id) { 27 | blinkInfo.Id = blink.Id; 28 | blinkInfo.Content = decodeHTML(blink.Content); 29 | blinkInfo.CommentCount = blink.CommentCount; 30 | blinkInfo.Author= decodeHTML(blink.UserDisplayName); 31 | blinkInfo.Avatar = getBloggerAvatar(blink.UserIconUrl); 32 | blinkInfo.DateAdded = moment(blink.DateAdded).startOf('minute').fromNow(); 33 | } 34 | return blinkInfo; 35 | } 36 | 37 | renderBlinkHeader(blinkInfo){ 38 | return ( 39 | 40 | this.imgView=view} 41 | style={ [ ComponentStyles.avatar_mini, CommonStyles.m_r_2] } 42 | source={ blinkInfo.Avatar }> 43 | 44 | 45 | { blinkInfo.Author } 46 | 47 | 48 | ); 49 | } 50 | 51 | renderBlinkContent(blinkInfo){ 52 | return ( 53 | 54 | 55 | { blinkInfo.Content } 56 | 57 | 58 | ); 59 | } 60 | 61 | renderBlinkMeta(blinkInfo){ 62 | return ( 63 | 64 | 65 | { blinkInfo.DateAdded } 66 | 67 | 68 | 69 | 74 | 75 | { blinkInfo.CommentCount } 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | render() { 83 | const blinkInfo = this.getBlinkInfo(); 84 | return ( 85 | { this.props.onRowPress(blinkInfo) }} 87 | underlayColor={ StyleConfig.touchable_press_color } 88 | key={ blinkInfo.Id }> 89 | 90 | 91 | { this.renderBlinkHeader(blinkInfo) } 92 | { this.renderBlinkContent(blinkInfo) } 93 | { this.renderBlinkMeta(blinkInfo) } 94 | 95 | 96 | ) 97 | } 98 | } 99 | 100 | export default BlinkRow; 101 | -------------------------------------------------------------------------------- /source/component/listview/newsCommentList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | ListView 6 | } from 'react-native'; 7 | 8 | import { bindActionCreators } from 'redux'; 9 | import { connect } from 'react-redux'; 10 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 11 | import * as CommentAction from '../../action/comment'; 12 | import NewsCommentRow from './newsCommentRow'; 13 | import ViewPage from '../view'; 14 | import Spinner from '../spinner'; 15 | import EndTag from '../endtag'; 16 | 17 | class NewsCommentList extends Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 22 | this.state = { 23 | dataSource: dataSource.cloneWithRows(props.comments) 24 | }; 25 | 26 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 27 | } 28 | 29 | componentWillReceiveProps(nextProps) { 30 | if (nextProps.comments && nextProps.comments.length && nextProps.comments !== this.props.comments) { 31 | this.setState({ 32 | dataSource: this.state.dataSource.cloneWithRows(nextProps.comments) 33 | }); 34 | } 35 | } 36 | 37 | onListEndReached() { 38 | const { commentAction, comments, category, id, ui } = this.props; 39 | if (comments.length && ui.pageEnabled) { 40 | commentAction.getCommentsByPostWithPage(category, id, { 41 | pageIndex: ui.pageIndex + 1, 42 | pageSize: ui.pageSize 43 | }); 44 | } 45 | } 46 | 47 | renderListFooter() { 48 | const { ui } = this.props; 49 | if (ui.pagePending) { 50 | return 51 | } 52 | if(ui.pageEnabled!==true){ 53 | return 54 | } 55 | } 56 | 57 | renderListRow(comment) { 58 | const { category } = this.props; 59 | if(comment && comment.CommentID){ 60 | return ( 61 | 65 | ) 66 | } 67 | } 68 | 69 | render() { 70 | return ( 71 | this.listView = view } 73 | showsVerticalScrollIndicator 74 | removeClippedSubviews 75 | enableEmptySections 76 | onEndReachedThreshold={ 10 } 77 | initialListSize={ 10 } 78 | pagingEnabled={ false } 79 | scrollRenderAheadDistance={ 150 } 80 | dataSource={ this.state.dataSource } 81 | renderRow={ this.renderListRow.bind(this) } 82 | onEndReached={ this.onListEndReached.bind(this) } 83 | renderFooter={ this.renderListFooter.bind(this) }> 84 | 85 | ); 86 | } 87 | } 88 | 89 | export default connect((state, props) => ({ 90 | comments : state.comment[props.id], 91 | ui: state.commentListUI[props.id] 92 | }), dispatch => ({ 93 | commentAction : bindActionCreators(CommentAction, dispatch) 94 | }))(NewsCommentList); -------------------------------------------------------------------------------- /source/component/listview/newsCommentRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import moment from 'moment'; 10 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 11 | import HtmlConvertor from '../htmlConvertor'; 12 | import { filterCommentData, getBloggerAvatar, decodeHTML } from '../../common' 13 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 14 | 15 | class NewsCommentRow extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 20 | } 21 | 22 | getCommentInfo(){ 23 | let { comment } = this.props; 24 | let commentInfo = {}; 25 | if (comment && comment.CommentContent) { 26 | commentInfo.Id = comment.CommentID; 27 | commentInfo.DateAdded = moment(comment.DateAdded).startOf('minute').fromNow(); 28 | commentInfo.Author = decodeHTML(comment.UserName); 29 | commentInfo.Avatar = getBloggerAvatar(comment.FaceUrl); 30 | commentInfo.Body = filterCommentData(decodeHTML(comment.CommentContent)); 31 | } 32 | return commentInfo; 33 | } 34 | 35 | renderCommentHeader(commentInfo){ 36 | return ( 37 | 38 | 39 | 42 | 43 | { commentInfo.Author } 44 | 45 | 46 | 47 | { commentInfo.DateAdded } 48 | 49 | 50 | ) 51 | } 52 | 53 | renderCommentBody(commentInfo){ 54 | return ( 55 | 58 | 59 | ) 60 | } 61 | 62 | render() { 63 | const { onPress = ()=>null } = this.props; 64 | const commentInfo = this.getCommentInfo(); 65 | return ( 66 | 70 | 71 | { this.renderCommentHeader(commentInfo) } 72 | { this.renderCommentBody(commentInfo) } 73 | 74 | 75 | ) 76 | } 77 | } 78 | 79 | export default NewsCommentRow; 80 | -------------------------------------------------------------------------------- /source/component/listview/newsList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import * as PostAction from '../../action/post'; 11 | import NewsRow from './newsRow'; 12 | import Spinner from '../spinner'; 13 | import EndTag from '../endtag'; 14 | import ViewPage from '../view'; 15 | import HintMessage from '../hintMessage'; 16 | import { postCategory } from '../../config'; 17 | import { CommonStyles, ComponentStyles } from '../../style'; 18 | 19 | const category = postCategory.news; 20 | 21 | class NewsList extends Component { 22 | 23 | constructor(props) { 24 | super(props); 25 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 26 | this.state = { 27 | dataSource: dataSource.cloneWithRows(props.news||{}), 28 | }; 29 | 30 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | if (nextProps.news && nextProps.news.length && nextProps.news !== this.props.news) { 35 | this.setState({ 36 | dataSource: this.state.dataSource.cloneWithRows(nextProps.news) 37 | }); 38 | } 39 | } 40 | 41 | renderListFooter() { 42 | const { ui, news = {} } = this.props; 43 | if (ui.pagePending) { 44 | return ; 45 | } 46 | if(ui.refreshPending!==true && ui.pageEnabled!==true && news.length){ 47 | return ; 48 | } 49 | } 50 | 51 | onListRowPress(news){ 52 | this.props.router.push(ViewPage.post(), { 53 | id: news.Id, 54 | category: category, 55 | post: news 56 | }); 57 | } 58 | 59 | renderListRow(news) { 60 | if(news && news.Id){ 61 | return ( 62 | this.onListRowPress(e) } /> 66 | ) 67 | } 68 | } 69 | 70 | render() { 71 | 72 | const { ui, news } = this.props; 73 | if(ui.refreshPending!==true && (!news || !news.length)){ 74 | return ; 75 | } 76 | 77 | return ( 78 | this.listView = view } 80 | removeClippedSubviews 81 | enableEmptySections = { true } 82 | onEndReachedThreshold={ 10 } 83 | initialListSize={ 10 } 84 | pageSize = { 10 } 85 | pagingEnabled={ false } 86 | scrollRenderAheadDistance={ 150 } 87 | dataSource={ this.state.dataSource } 88 | renderRow={ (e)=>this.renderListRow(e) } 89 | renderFooter={ (e)=>this.renderListFooter(e) }> 90 | 91 | ); 92 | } 93 | } 94 | 95 | export default connect((state, props) => ({ 96 | news : state.post[category], 97 | ui: state.postListUI[category] 98 | }), dispatch => ({ 99 | postAction : bindActionCreators(PostAction, dispatch) 100 | }))(NewsList); -------------------------------------------------------------------------------- /source/component/listview/newsRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import _ from 'lodash'; 10 | import moment from 'moment'; 11 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 12 | import { decodeHTML, getBloggerAvatar, getNewsUrlFromID } from '../../common'; 13 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 14 | 15 | class NewsRow extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 20 | } 21 | 22 | getNewsInfo(){ 23 | let { news } = this.props; 24 | let newsInfo = {}; 25 | if (news && news.Id) { 26 | newsInfo.Id = news.Id; 27 | newsInfo.ViewCount = news.ViewCount; 28 | newsInfo.CommentCount = news.CommentCount; 29 | 30 | newsInfo.Title = decodeHTML(news.Title); 31 | if (news.Summary) { 32 | newsInfo.Description = _.truncate(decodeHTML(news.Summary), { length : 70 }); 33 | } 34 | newsInfo.Url = getNewsUrlFromID(news.Id); 35 | newsInfo.DateAdded = moment(news.DateAdded).startOf('minute').fromNow(); 36 | newsInfo.Avatar = getBloggerAvatar(news.TopicIcon); 37 | } 38 | return newsInfo; 39 | } 40 | 41 | renderNewsTitle(newsInfo){ 42 | return ( 43 | 44 | 45 | { newsInfo.Title } 46 | 47 | 48 | ) 49 | } 50 | 51 | renderNewsDescr(newsInfo){ 52 | return ( 53 | 54 | 55 | { newsInfo.Description } 56 | 57 | 58 | ) 59 | } 60 | 61 | renderNewsMeta(newsInfo){ 62 | return ( 63 | 64 | 65 | this.imgView=view } 66 | style={ [ ComponentStyles.avatar_mini, CommonStyles.m_r_2] } 67 | source={ newsInfo.Avatar }> 68 | 69 | 70 | { newsInfo.DateAdded } 71 | 72 | 73 | 74 | 75 | { newsInfo.CommentCount + ' / ' + newsInfo.ViewCount } 76 | 77 | 78 | 79 | ) 80 | } 81 | 82 | render() { 83 | const newsInfo = this.getNewsInfo(); 84 | 85 | return ( 86 | { this.props.onRowPress(newsInfo) }} 88 | underlayColor={ StyleConfig.touchable_press_color } 89 | key={ newsInfo.Id }> 90 | 91 | 92 | { this.renderNewsTitle(newsInfo) } 93 | { this.renderNewsDescr(newsInfo) } 94 | { this.renderNewsMeta(newsInfo) } 95 | 96 | 97 | ) 98 | } 99 | } 100 | 101 | export default NewsRow; 102 | -------------------------------------------------------------------------------- /source/component/listview/offlineList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import OfflineRow from './offlineRow'; 11 | import ViewPage from '../view'; 12 | 13 | class OfflineList extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 18 | this.state = { 19 | dataSource: dataSource.cloneWithRows(props.posts||{}), 20 | }; 21 | 22 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 23 | } 24 | 25 | componentWillReceiveProps(nextProps) { 26 | if (nextProps.posts && nextProps.posts.length && nextProps.posts !== this.props.posts) { 27 | this.setState({ 28 | dataSource: this.state.dataSource.cloneWithRows(nextProps.posts) 29 | }); 30 | } 31 | } 32 | 33 | onListRowPress(post){ 34 | this.props.router.push(ViewPage.offlinePost(), { 35 | id: post.Id, 36 | category: this.props.category, 37 | post 38 | }); 39 | } 40 | 41 | renderListRow(post) { 42 | if(post && post.Id){ 43 | const { onRemovePress = ()=>null } = this.props; 44 | return ( 45 | onRemovePress(e)} 49 | onRowPress={ (e)=>this.onListRowPress(e) } /> 50 | ) 51 | } 52 | } 53 | 54 | render() { 55 | return ( 56 | this.listView = view } 58 | removeClippedSubviews 59 | enableEmptySections = { true } 60 | onEndReachedThreshold={ 10 } 61 | initialListSize={ 10 } 62 | pageSize = { 10 } 63 | pagingEnabled={ false } 64 | scrollRenderAheadDistance={ 150 } 65 | dataSource={ this.state.dataSource } 66 | renderRow={ (e)=>this.renderListRow(e) }> 67 | 68 | ); 69 | } 70 | } 71 | 72 | export default connect((state, props) => ({ 73 | posts : state.offline.posts 74 | }), dispatch => ({ 75 | 76 | }))(OfflineList); -------------------------------------------------------------------------------- /source/component/listview/offlineRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import _ from 'lodash'; 10 | import moment from 'moment'; 11 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 12 | import { postCategory } from '../../config'; 13 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 14 | 15 | class OfflineRow extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 20 | } 21 | 22 | renderPostAuthor(postInfo){ 23 | if (postInfo.category != postCategory.news) { 24 | return ( 25 | 26 | this.imgView=view} 27 | style={ [ ComponentStyles.avatar_mini, CommonStyles.m_r_2] } 28 | source={ postInfo.Avatar }> 29 | 30 | 31 | { postInfo.Author } 32 | 33 | 34 | ) 35 | } 36 | } 37 | 38 | renderPostTitle(postInfo){ 39 | return ( 40 | 41 | 42 | { postInfo.Title } 43 | 44 | 45 | ) 46 | } 47 | 48 | renderPostDescr(postInfo){ 49 | return ( 50 | 51 | 52 | { postInfo.Description } 53 | 54 | 55 | ) 56 | } 57 | 58 | renderPostMeta(postInfo){ 59 | let offlineDate = moment(postInfo.offlineDate).startOf('minute').fromNow(); 60 | let postCategoryLabel, 61 | postCategoryColor; 62 | if (postInfo.category == postCategory.news) { 63 | postCategoryLabel = "#新闻"; 64 | postCategoryColor = StyleConfig.color_danger; 65 | }else{ 66 | postCategoryLabel = "#博文"; 67 | postCategoryColor = StyleConfig.color_primary; 68 | } 69 | return ( 70 | 71 | 72 | { offlineDate } 73 | 74 | 75 | 76 | { postCategoryLabel } 77 | 78 | 79 | 80 | ) 81 | } 82 | 83 | render() { 84 | const { post, onRowLongPress=()=>null } = this.props; 85 | return ( 86 | { this.props.onRowPress(post) }} 88 | onLongPress={(e)=>{ onRowLongPress(post) }} 89 | underlayColor={ StyleConfig.touchable_press_color } 90 | key={ post.Id }> 91 | 92 | { this.renderPostAuthor(post) } 93 | { this.renderPostTitle(post) } 94 | { this.renderPostDescr(post) } 95 | { this.renderPostMeta(post) } 96 | 97 | 98 | ) 99 | } 100 | } 101 | 102 | export default OfflineRow; 103 | -------------------------------------------------------------------------------- /source/component/listview/postCommentList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | ListView 6 | } from 'react-native'; 7 | 8 | import { bindActionCreators } from 'redux'; 9 | import { connect } from 'react-redux'; 10 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 11 | import * as CommentAction from '../../action/comment'; 12 | import PostCommentRow from './postCommentRow'; 13 | import EndTag from '../endtag'; 14 | import Spinner from '../spinner'; 15 | import ViewPage from '../view'; 16 | 17 | class PostCommentList extends Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 22 | this.state = { 23 | dataSource: dataSource.cloneWithRows(props.comments) 24 | }; 25 | 26 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 27 | } 28 | 29 | componentWillReceiveProps(nextProps) { 30 | if (nextProps.comments && nextProps.comments.length && nextProps.comments !== this.props.comments) { 31 | this.setState({ 32 | dataSource: this.state.dataSource.cloneWithRows(nextProps.comments) 33 | }); 34 | } 35 | } 36 | 37 | onListEndReached() { 38 | const { commentAction, comments, category, blogger, id, ui } = this.props; 39 | if (comments.length && ui.pageEnabled) { 40 | commentAction.getCommentsByPostWithPage(category, id, { 41 | blogger: blogger, 42 | pageIndex: ui.pageIndex + 1, 43 | pageSize: ui.pageSize 44 | }); 45 | } 46 | } 47 | 48 | renderListFooter() { 49 | const { ui } = this.props; 50 | if (ui.pagePending) { 51 | return 52 | } 53 | if(ui.pageEnabled!==true){ 54 | return 55 | } 56 | } 57 | 58 | renderListRow(comment) { 59 | const { category } = this.props; 60 | if(comment && comment.Id){ 61 | return ( 62 | 66 | ) 67 | } 68 | } 69 | 70 | render() { 71 | return ( 72 | this.listView = view } 74 | showsVerticalScrollIndicator 75 | removeClippedSubviews 76 | enableEmptySections 77 | onEndReachedThreshold={ 10 } 78 | initialListSize={ 10 } 79 | pagingEnabled={ false } 80 | scrollRenderAheadDistance={ 150 } 81 | dataSource={ this.state.dataSource } 82 | renderRow={ this.renderListRow.bind(this) } 83 | onEndReached={ this.onListEndReached.bind(this) } 84 | renderFooter={ this.renderListFooter.bind(this) }> 85 | 86 | ); 87 | } 88 | } 89 | 90 | export default connect((state, props) => ({ 91 | comments : state.comment[props.id], 92 | ui: state.commentListUI[props.id] 93 | }), dispatch => ({ 94 | commentAction : bindActionCreators(CommentAction, dispatch) 95 | }))(PostCommentList); -------------------------------------------------------------------------------- /source/component/listview/postCommentRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import moment from 'moment'; 10 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 11 | import HtmlConvertor from '../htmlConvertor'; 12 | import { filterCommentData, getBloggerAvatar, decodeHTML } from '../../common' 13 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 14 | 15 | class PostCommentRow extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 20 | } 21 | 22 | getCommentInfo(){ 23 | let { comment } = this.props; 24 | let commentInfo = {}; 25 | if (comment && comment.Body) { 26 | commentInfo.Id = comment.Id; 27 | commentInfo.DateAdded = moment(comment.DateAdded).startOf('minute').fromNow(); 28 | commentInfo.Author = decodeHTML(comment.Author); 29 | commentInfo.Avatar = getBloggerAvatar(comment.FaceUrl); 30 | commentInfo.Body = filterCommentData(decodeHTML(comment.Body)); 31 | } 32 | return commentInfo; 33 | } 34 | 35 | renderCommentHeader(commentInfo){ 36 | return ( 37 | 38 | 39 | 42 | 43 | { commentInfo.Author } 44 | 45 | 46 | 47 | { commentInfo.DateAdded } 48 | 49 | 50 | ) 51 | } 52 | 53 | renderCommentBody(commentInfo){ 54 | return ( 55 | 58 | 59 | ) 60 | } 61 | 62 | render() { 63 | const commentInfo = this.getCommentInfo(); 64 | return ( 65 | 69 | 70 | { this.renderCommentHeader(commentInfo) } 71 | { this.renderCommentBody(commentInfo) } 72 | 73 | 74 | ) 75 | } 76 | } 77 | 78 | export default PostCommentRow; 79 | -------------------------------------------------------------------------------- /source/component/listview/postList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import * as PostAction from '../../action/post'; 11 | import PostRow from './postRow'; 12 | import Spinner from '../spinner'; 13 | import EndTag from '../endtag'; 14 | import ViewPage from '../view'; 15 | import HintMessage from '../hintMessage'; 16 | 17 | class PostList extends Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 22 | this.state = { 23 | dataSource: dataSource.cloneWithRows(props.posts||{}), 24 | }; 25 | 26 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 27 | } 28 | 29 | componentWillReceiveProps(nextProps) { 30 | if (nextProps.posts && nextProps.posts.length && nextProps.posts !== this.props.posts) { 31 | this.setState({ 32 | dataSource: this.state.dataSource.cloneWithRows(nextProps.posts) 33 | }); 34 | } 35 | } 36 | 37 | renderListFooter() { 38 | const { ui, posts = {} } = this.props; 39 | if (ui.pagePending) { 40 | return ; 41 | } 42 | if(ui.refreshPending!==true && ui.pageEnabled!==true && posts.length){ 43 | return ; 44 | } 45 | } 46 | 47 | onListRowPress(post){ 48 | this.props.router.push(ViewPage.post(), { 49 | id: post.Id, 50 | category: this.props.category, 51 | post 52 | }); 53 | } 54 | 55 | renderListRow(post) { 56 | if(post && post.Id){ 57 | return ( 58 | this.onListRowPress(e) } /> 63 | ) 64 | } 65 | } 66 | 67 | render() { 68 | const { ui, posts } = this.props; 69 | if(ui.refreshPending!==true && (!posts || !posts.length)){ 70 | return ; 71 | } 72 | 73 | return ( 74 | this.listView = view } 76 | removeClippedSubviews 77 | enableEmptySections = { true } 78 | onEndReachedThreshold={ 10 } 79 | initialListSize={ 10 } 80 | pageSize = { 10 } 81 | pagingEnabled={ false } 82 | scrollRenderAheadDistance={ 150 } 83 | dataSource={ this.state.dataSource } 84 | renderRow={ (e)=>this.renderListRow(e) } 85 | renderFooter={ (e)=>this.renderListFooter(e) }> 86 | 87 | ); 88 | } 89 | } 90 | 91 | export default connect((state, props) => ({ 92 | posts: state.post[props.category], 93 | ui: state.postListUI[props.category] 94 | }), dispatch => ({ 95 | postAction : bindActionCreators(PostAction, dispatch) 96 | }))(PostList); -------------------------------------------------------------------------------- /source/component/listview/postRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import _ from 'lodash'; 10 | import moment from 'moment'; 11 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 12 | 13 | import Config from '../../config'; 14 | import { decodeHTML, getBloggerAvatar, getBloggerHdpiAvatar } from '../../common'; 15 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 16 | 17 | class PostRow extends Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 22 | } 23 | 24 | getPostInfo(){ 25 | let { post } = this.props; 26 | let postInfo = {}; 27 | if (post && post.Id) { 28 | postInfo.Id = post.Id; 29 | postInfo.ViewCount = post.ViewCount; 30 | postInfo.CommentCount = post.CommentCount; 31 | postInfo.Blogger = post.BlogApp; 32 | postInfo.Title = decodeHTML(post.Title); 33 | if (post.Description) { 34 | postInfo.Description = _.truncate(decodeHTML(post.Description), { length : 70 }); 35 | } 36 | postInfo.Url = post.Url; 37 | postInfo.DateAdded = moment(post.PostDate).startOf('minute').fromNow(); 38 | postInfo.Author = decodeHTML(post.Author); 39 | postInfo.Avatar = getBloggerAvatar(post.Avatar); 40 | postInfo.AvatarHdpi = getBloggerHdpiAvatar(post.Avatar); 41 | } 42 | return postInfo; 43 | } 44 | 45 | renderPostAuthor(postInfo){ 46 | return ( 47 | 48 | this.imgView=view} 49 | style={ [ ComponentStyles.avatar_mini, CommonStyles.m_r_2] } 50 | source={ postInfo.Avatar }> 51 | 52 | 53 | { postInfo.Author } 54 | 55 | 56 | ) 57 | } 58 | 59 | renderPostTitle(postInfo){ 60 | return ( 61 | 62 | 63 | { postInfo.Title } 64 | 65 | 66 | ) 67 | } 68 | 69 | renderPostDescr(postInfo){ 70 | return ( 71 | 72 | 73 | { postInfo.Description } 74 | 75 | 76 | ) 77 | } 78 | 79 | renderPostMeta(postInfo){ 80 | return ( 81 | 82 | 83 | { postInfo.DateAdded } 84 | 85 | 86 | 87 | 88 | { postInfo.CommentCount + ' / ' + postInfo.ViewCount } 89 | 90 | 91 | 92 | ) 93 | } 94 | 95 | render() { 96 | const postInfo = this.getPostInfo(); 97 | return ( 98 | { this.props.onRowPress(postInfo) }} 100 | underlayColor={ StyleConfig.touchable_press_color } 101 | key={ postInfo.Id }> 102 | 103 | 104 | { this.renderPostAuthor(postInfo) } 105 | { this.renderPostTitle(postInfo) } 106 | { this.renderPostDescr(postInfo) } 107 | { this.renderPostMeta(postInfo) } 108 | 109 | 110 | ) 111 | } 112 | } 113 | 114 | export default PostRow; 115 | -------------------------------------------------------------------------------- /source/component/listview/questionList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import * as PostAction from '../../action/post'; 11 | import Spinner from '../spinner'; 12 | import EndTag from '../endtag'; 13 | import ViewPage from '../view'; 14 | import QuestionRow from './questionRow'; 15 | import { postCategory } from '../../config'; 16 | 17 | const category = postCategory.question; 18 | 19 | class QuestionList extends Component { 20 | 21 | constructor(props) { 22 | super(props); 23 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 24 | this.state = { 25 | dataSource: dataSource.cloneWithRows(props.questions||{}), 26 | }; 27 | 28 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 29 | } 30 | 31 | componentWillReceiveProps(nextProps) { 32 | if (nextProps.questions && nextProps.questions.length && nextProps.questions !== this.props.questions) { 33 | this.setState({ 34 | dataSource: this.state.dataSource.cloneWithRows(nextProps.questions) 35 | }); 36 | } 37 | } 38 | 39 | renderListFooter() { 40 | const { ui, questions } = this.props; 41 | if (ui.pagePending) { 42 | return ; 43 | } 44 | if(ui.refreshPending!==true && ui.pageEnabled!==true && questions.length){ 45 | return ; 46 | } 47 | } 48 | 49 | onListRowPress(question){ 50 | this.props.router.push(ViewPage.question(), { 51 | id: question.Id, 52 | category: category, 53 | question 54 | }); 55 | } 56 | 57 | renderListRow(question) { 58 | if(question && question.Qid){ 59 | return ( 60 | this.onListRowPress(e) } /> 64 | ) 65 | } 66 | } 67 | 68 | render() { 69 | return ( 70 | this.listView = view } 72 | removeClippedSubviews 73 | enableEmptySections = { true } 74 | onEndReachedThreshold={ 10 } 75 | initialListSize={ 10 } 76 | pageSize = { 10 } 77 | pagingEnabled={ false } 78 | scrollRenderAheadDistance={ 150 } 79 | dataSource={ this.state.dataSource } 80 | renderRow={ (e)=>this.renderListRow(e) } 81 | renderFooter={ (e)=>this.renderListFooter(e) }> 82 | 83 | ); 84 | } 85 | } 86 | 87 | export default connect((state, props) => ({ 88 | questions: state.post[category], 89 | ui: state.postListUI[category] 90 | }), dispatch => ({ 91 | postAction : bindActionCreators(PostAction, dispatch) 92 | }))(QuestionList); -------------------------------------------------------------------------------- /source/component/listview/searchList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import Spinner from '../spinner'; 11 | import EndTag from '../endtag'; 12 | import ViewPage from '../view'; 13 | import SearchRow from './searchRow'; 14 | import { postCategory } from '../../config'; 15 | 16 | class SearchList extends Component { 17 | 18 | constructor(props) { 19 | super(props); 20 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 21 | this.state = { 22 | dataSource: dataSource.cloneWithRows(props.searchs||{}), 23 | }; 24 | 25 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 26 | } 27 | 28 | componentWillReceiveProps(nextProps) { 29 | if (nextProps.searchs && nextProps.searchs.length && nextProps.searchs !== this.props.searchs) { 30 | this.setState({ 31 | dataSource: this.state.dataSource.cloneWithRows(nextProps.searchs) 32 | }); 33 | } 34 | } 35 | 36 | renderListFooter() { 37 | const { ui } = this.props; 38 | if (ui.pagePending) { 39 | return ; 40 | } 41 | if(ui.pagePending!==true && ui.pageEnabled!==true){ 42 | return ; 43 | } 44 | } 45 | 46 | onListRowPress(search){ 47 | if(search.Id){ 48 | this.props.router.push(ViewPage.searchDetail(), { 49 | id: search.Id, 50 | post: search, 51 | category: postCategory.home 52 | }); 53 | } 54 | else{ 55 | openLink(search.LinkUri); 56 | } 57 | } 58 | 59 | renderListRow(search) { 60 | if(search && search.Id){ 61 | return ( 62 | this.onListRowPress(e) } /> 66 | ) 67 | } 68 | } 69 | 70 | render() { 71 | return ( 72 | this.listView = view } 74 | showsVerticalScrollIndicator 75 | removeClippedSubviews 76 | enableEmptySections = { true } 77 | onEndReachedThreshold={ 10 } 78 | initialListSize={ 15 } 79 | pageSize = { 15 } 80 | pagingEnabled={ false } 81 | scrollRenderAheadDistance={ 150 } 82 | onEndReached = {(e)=>this.props.onListEndReached()} 83 | dataSource={ this.state.dataSource } 84 | renderRow={ (e)=>this.renderListRow(e) } 85 | renderFooter={ (e)=>this.renderListFooter(e) }> 86 | 87 | ); 88 | } 89 | } 90 | 91 | export default connect((state, props) => ({ 92 | searchs : state.search[props.category], 93 | ui: state.searchUI[props.category] 94 | }), dispatch => ({ 95 | 96 | }))(SearchList); -------------------------------------------------------------------------------- /source/component/listview/searchRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TouchableHighlight 6 | } from 'react-native'; 7 | 8 | import moment from 'moment'; 9 | import _ from 'lodash'; 10 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 11 | import { decodeHTML } from '../../common'; 12 | import { CommonStyles, ComponentStyles, StyleConfig } from '../../style'; 13 | 14 | class SearchRow extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 19 | } 20 | 21 | getSearchInfo(){ 22 | let { search } = this.props; 23 | let searchInfo = {}; 24 | if (search && search.Title) { 25 | searchInfo.Title = decodeHTML(search.Title).replace(//g,'').replace(/<\/strong>/g,''); 26 | searchInfo.DateAdded = moment(search.PublishTime).startOf('minute').fromNow(); 27 | searchInfo.Author = search.UserName; 28 | searchInfo.ViewCount = search.ViewTimes; 29 | searchInfo.CommentCount = search.CommentTimes; 30 | searchInfo.Url = search.Uri; 31 | searchInfo.Id = search.Id; 32 | searchInfo.Description = decodeHTML(search.Content).replace(//g,'').replace(/<\/strong>/g,''); 33 | } 34 | return searchInfo; 35 | } 36 | 37 | renderSearchItemHeader(searchInfo){ 38 | return ( 39 | 40 | 41 | { searchInfo.Author } 42 | 43 | 44 | ) 45 | } 46 | 47 | renderSearchItemMeta(searchInfo){ 48 | return ( 49 | 50 | 51 | { searchInfo.DateAdded } 52 | 53 | 54 | { searchInfo.CommentCount + ' / ' + searchInfo.ViewCount } 55 | 56 | 57 | ) 58 | } 59 | 60 | renderSearchItemContent(searchInfo){ 61 | return ( 62 | 63 | 64 | { searchInfo.Title } 65 | 66 | 67 | { searchInfo.Description } 68 | 69 | 70 | ) 71 | } 72 | 73 | render() { 74 | const searchInfo = this.getSearchInfo(); 75 | return ( 76 | { this.props.onRowPress(searchInfo) }} 78 | underlayColor={ StyleConfig.touchable_press_color }> 79 | 80 | { this.renderSearchItemHeader(searchInfo) } 81 | { this.renderSearchItemContent(searchInfo) } 82 | { this.renderSearchItemMeta(searchInfo) } 83 | 84 | 85 | ) 86 | } 87 | } 88 | 89 | export default SearchRow; 90 | -------------------------------------------------------------------------------- /source/component/listview/userBlinkList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Alert, 6 | ListView 7 | } from 'react-native'; 8 | 9 | import { bindActionCreators } from 'redux'; 10 | import { connect } from 'react-redux'; 11 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 12 | import UserBlinkRow from './userBlinkRow'; 13 | import Spinner from '../spinner'; 14 | import EndTag from '../endtag'; 15 | import ViewPage from '../view'; 16 | import { postCategory } from '../../config'; 17 | 18 | const category = postCategory.blink; 19 | 20 | class UserBlinkList extends Component { 21 | 22 | constructor(props) { 23 | super(props); 24 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 25 | this.state = { 26 | dataSource: dataSource.cloneWithRows(props.blinks||{}), 27 | removeModalVisiable: false 28 | }; 29 | 30 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | if (nextProps.blinks && nextProps.blinks.length && nextProps.blinks !== this.props.blinks) { 35 | this.setState({ 36 | dataSource: this.state.dataSource.cloneWithRows(nextProps.blinks) 37 | }); 38 | } 39 | } 40 | 41 | renderListFooter() { 42 | const { ui } = this.props; 43 | if (ui.pagePending) { 44 | return ; 45 | } 46 | if(ui.refreshPending!==true && ui.pageEnabled!==true){ 47 | return ; 48 | } 49 | } 50 | 51 | onListRowPress(blink){ 52 | this.props.router.push(ViewPage.blink(), { 53 | id: blink.Id, 54 | category: category, 55 | blink 56 | }); 57 | } 58 | 59 | renderListRow(blink) { 60 | if(blink && blink.Id){ 61 | const { onRemovePress = ()=>null } = this.props; 62 | return ( 63 | onRemovePress(e)} 68 | onRowPress={(e)=>this.onListRowPress(e)} /> 69 | ) 70 | } 71 | } 72 | 73 | render() { 74 | return ( 75 | this.listView = view } 77 | removeClippedSubviews 78 | enableEmptySections = { true } 79 | onEndReachedThreshold={ 10 } 80 | initialListSize={ 10 } 81 | pageSize = { 10 } 82 | pagingEnabled={ false } 83 | scrollRenderAheadDistance={ 150 } 84 | dataSource={ this.state.dataSource } 85 | renderRow={ (e)=>this.renderListRow(e) } 86 | renderFooter={ (e)=>this.renderListFooter(e) }> 87 | 88 | ); 89 | } 90 | } 91 | 92 | export default connect((state, props) => ({ 93 | blinks: state.user[category], 94 | ui: state.userListUI[category] 95 | }), dispatch => ({ 96 | 97 | }))(UserBlinkList); -------------------------------------------------------------------------------- /source/component/listview/userFavoriteList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import UserFavoriteRow from './userFavoriteRow'; 11 | import Spinner from '../spinner'; 12 | import EndTag from '../endtag'; 13 | import ViewPage from '../view'; 14 | import { postCategory } from '../../config'; 15 | 16 | const category = postCategory.favorite; 17 | 18 | class UserFavoriteList extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 23 | this.state = { 24 | dataSource: dataSource.cloneWithRows(props.favorites||{}), 25 | }; 26 | 27 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | if (nextProps.favorites && nextProps.favorites.length && nextProps.favorites !== this.props.favorites) { 32 | this.setState({ 33 | dataSource: this.state.dataSource.cloneWithRows(nextProps.favorites) 34 | }); 35 | } 36 | } 37 | 38 | renderListFooter() { 39 | const { ui } = this.props; 40 | if (ui.pagePending) { 41 | return ; 42 | } 43 | if(ui.refreshPending!==true && ui.pageEnabled!==true){ 44 | return ; 45 | } 46 | } 47 | 48 | onListRowPress(favorite){ 49 | this.props.router.push(ViewPage.favorite(), { 50 | id: favorite.Id, 51 | post: favorite, 52 | category: favorite.Category, 53 | favorite 54 | }); 55 | } 56 | 57 | renderListRow(favorite) { 58 | if(favorite && favorite.WzLinkId){ 59 | const { onRemovePress = ()=>null } = this.props; 60 | return ( 61 | onRemovePress(e)} 65 | onRowPress={ (e)=>this.onListRowPress(e) } /> 66 | ) 67 | } 68 | } 69 | 70 | render() { 71 | return ( 72 | this.listView = view } 74 | removeClippedSubviews 75 | enableEmptySections = { true } 76 | onEndReachedThreshold={ 10 } 77 | initialListSize={ 10 } 78 | pageSize = { 10 } 79 | pagingEnabled={ false } 80 | scrollRenderAheadDistance={ 150 } 81 | dataSource={ this.state.dataSource } 82 | renderRow={ (e)=>this.renderListRow(e) } 83 | renderFooter={ (e)=>this.renderListFooter(e) }> 84 | 85 | ); 86 | } 87 | } 88 | 89 | export default connect((state, props) => ({ 90 | favorites: state.user[category], 91 | ui: state.userListUI[category] 92 | }), dispatch => ({ 93 | 94 | }))(UserFavoriteList); -------------------------------------------------------------------------------- /source/component/listview/userPostList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import UserPostRow from './userPostRow'; 11 | import Spinner from '../spinner'; 12 | import EndTag from '../endtag'; 13 | import ViewPage from '../view'; 14 | import { postCategory } from '../../config'; 15 | 16 | const category = postCategory.home; 17 | 18 | class UserPostList extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 23 | this.state = { 24 | dataSource: dataSource.cloneWithRows(props.posts||{}), 25 | }; 26 | 27 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | if (nextProps.posts && nextProps.posts.length && nextProps.posts !== this.props.posts) { 32 | this.setState({ 33 | dataSource: this.state.dataSource.cloneWithRows(nextProps.posts) 34 | }); 35 | } 36 | } 37 | 38 | renderListFooter() { 39 | const { ui } = this.props; 40 | if (ui.pagePending) { 41 | return ; 42 | } 43 | if(ui.refreshPending!==true && ui.pageEnabled!==true){ 44 | return ; 45 | } 46 | } 47 | 48 | formatUserPostDate(post){ 49 | if(post.Avatar){ 50 | post.Avatar = { uri: this.props.user.Avatar }; 51 | } 52 | post.AuthorEnabled = false; 53 | return post; 54 | } 55 | 56 | onListRowPress(post){ 57 | const postInfo = this.formatUserPostDate(post); 58 | this.props.router.push(ViewPage.post(), { 59 | id: postInfo.Id, 60 | category: category, 61 | post: postInfo 62 | }); 63 | } 64 | 65 | renderListRow(post) { 66 | if(post && post.Id){ 67 | return ( 68 | this.onListRowPress(e) } /> 73 | ) 74 | } 75 | } 76 | 77 | render() { 78 | return ( 79 | this.listView = view } 81 | removeClippedSubviews 82 | enableEmptySections = { true } 83 | onEndReachedThreshold={ 10 } 84 | initialListSize={ 10 } 85 | pageSize = { 10 } 86 | pagingEnabled={ false } 87 | scrollRenderAheadDistance={ 150 } 88 | dataSource={ this.state.dataSource } 89 | renderRow={ (e)=>this.renderListRow(e) } 90 | renderFooter={ (e)=>this.renderListFooter(e) }> 91 | 92 | ); 93 | } 94 | } 95 | 96 | export default connect((state, props) => ({ 97 | posts: state.user[category], 98 | user: state.user, 99 | ui: state.userListUI[category] 100 | }), dispatch => ({ 101 | 102 | }))(UserPostList); -------------------------------------------------------------------------------- /source/component/listview/userPostRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TouchableHighlight 7 | } from 'react-native'; 8 | 9 | import _ from 'lodash'; 10 | import moment from 'moment'; 11 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 12 | import { decodeHTML, getBloggerAvatar } from '../../common'; 13 | import { ComponentStyles, CommonStyles, StyleConfig } from '../../style'; 14 | 15 | class UserPostRow extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 20 | } 21 | 22 | getPostInfo(){ 23 | let { post } = this.props; 24 | let postInfo = {}; 25 | if (post && post.Id) { 26 | postInfo.Id = post.Id; 27 | postInfo.ViewCount = post.ViewCount; 28 | postInfo.CommentCount = post.CommentCount; 29 | postInfo.Title = decodeHTML(post.Title); 30 | if (post.Description) { 31 | postInfo.Description = _.truncate(decodeHTML(post.Description), { length : 70 }); 32 | } 33 | postInfo.DateAdded = moment(post.PostDate).startOf('minute').fromNow(); 34 | postInfo.Author = decodeHTML(post.Author); 35 | postInfo.Blogger = post.BlogApp; 36 | postInfo.Avatar = getBloggerAvatar(post.Avatar); 37 | } 38 | return postInfo; 39 | } 40 | 41 | renderPostTitle(postInfo){ 42 | return ( 43 | 44 | 45 | { postInfo.Title } 46 | 47 | 48 | ) 49 | } 50 | 51 | renderPostDescr(postInfo){ 52 | return ( 53 | 54 | 55 | { postInfo.Description } 56 | 57 | 58 | ) 59 | } 60 | 61 | renderPostMeta(postInfo){ 62 | return ( 63 | 64 | 65 | { postInfo.DateAdded } 66 | 67 | 68 | 69 | 70 | { postInfo.CommentCount + ' / ' + postInfo.ViewCount } 71 | 72 | 73 | 74 | ) 75 | } 76 | 77 | render() { 78 | const postInfo = this.getPostInfo(); 79 | return ( 80 | { this.props.onRowPress(postInfo) }} 82 | underlayColor={ StyleConfig.touchable_press_color } 83 | key={ postInfo.Id }> 84 | 85 | 86 | { this.renderPostTitle(postInfo) } 87 | { this.renderPostDescr(postInfo) } 88 | { this.renderPostMeta(postInfo) } 89 | 90 | 91 | ) 92 | } 93 | } 94 | 95 | export default UserPostRow; 96 | -------------------------------------------------------------------------------- /source/component/listview/userQuestionList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ListView 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import UserQuestionRow from './userQuestionRow'; 11 | import Spinner from '../spinner'; 12 | import EndTag from '../endtag'; 13 | import ViewPage from '../view'; 14 | import { postCategory } from '../../config'; 15 | 16 | const category = postCategory.question; 17 | 18 | class UserQuestionList extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | let dataSource = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 23 | this.state = { 24 | dataSource: dataSource.cloneWithRows(props.questions||{}), 25 | }; 26 | 27 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | if (nextProps.questions && nextProps.questions.length && nextProps.questions !== this.props.questions) { 32 | this.setState({ 33 | dataSource: this.state.dataSource.cloneWithRows(nextProps.questions) 34 | }); 35 | } 36 | } 37 | 38 | renderListFooter() { 39 | const { ui } = this.props; 40 | if (ui.pagePending) { 41 | return ; 42 | } 43 | if(ui.refreshPending!==true && ui.pageEnabled!==true){ 44 | return ; 45 | } 46 | } 47 | 48 | onListRowPress(question){ 49 | this.props.router.push(ViewPage.question(), { 50 | id: question.Id, 51 | category: category, 52 | question 53 | }); 54 | } 55 | 56 | renderListRow(question) { 57 | if(question && question.Qid){ 58 | return ( 59 | this.onListRowPress(e) } /> 64 | ) 65 | } 66 | } 67 | 68 | render() { 69 | return ( 70 | this.listView = view } 72 | removeClippedSubviews 73 | enableEmptySections = { true } 74 | onEndReachedThreshold={ 10 } 75 | initialListSize={ 10 } 76 | pageSize = { 10 } 77 | pagingEnabled={ false } 78 | scrollRenderAheadDistance={ 150 } 79 | dataSource={ this.state.dataSource } 80 | renderRow={ (e)=>this.renderListRow(e) } 81 | renderFooter={ (e)=>this.renderListFooter(e) }> 82 | 83 | ); 84 | } 85 | } 86 | 87 | export default connect((state, props) => ({ 88 | questions: state.user[category], 89 | ui: state.userListUI[category] 90 | }), dispatch => ({ 91 | 92 | }))(UserQuestionList); -------------------------------------------------------------------------------- /source/component/logo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Image, 4 | } from 'react-native'; 5 | 6 | import { ComponentStyles } from '../style'; 7 | import { logoImage } from '../common'; 8 | 9 | class Logo extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | return ( 17 | 20 | ) 21 | } 22 | } 23 | 24 | export default Logo; 25 | 26 | 27 | -------------------------------------------------------------------------------- /source/component/messager.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View 4 | } from 'react-native'; 5 | import { bindActionCreators } from 'redux'; 6 | import { connect } from 'react-redux'; 7 | import { CommonStyles, StyleConfig } from '../style'; 8 | import Toast from '@remobile/react-native-toast'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | 11 | class Messager extends Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 16 | } 17 | 18 | componentWillReceiveProps(nextProps, nextStates) { 19 | if (this.props.common.message.id !== nextProps.common.message.id) { 20 | const message = nextProps.common.message.text; 21 | if (message && typeof message === "string") { 22 | Toast.show(message); 23 | } 24 | } 25 | } 26 | 27 | render() { 28 | return null; 29 | } 30 | } 31 | 32 | export default connect(state => ({ 33 | common : state.common 34 | }), dispatch => ({ 35 | }), null, { 36 | withRef: true 37 | })(Messager); 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /source/component/navigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | NetInfo, 5 | Navigator 6 | } from 'react-native'; 7 | import { connect } from 'react-redux'; 8 | import Router from './router'; 9 | import Plugin from './plugin'; 10 | import ViewPage from './view'; 11 | import { ComponentStyles, StyleConfig } from '../style'; 12 | 13 | const defaultRoute = ViewPage.startup(); 14 | 15 | class Navigation extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | } 20 | 21 | renderScene(route, navigator) { 22 | this.router = this.router || new Router(navigator); 23 | let Component = route.component; 24 | if (Component) { 25 | return { route.sceneRef = view } }/> 29 | } 30 | } 31 | 32 | onDidFocus(route){ 33 | if(route.sceneRef.getWrappedInstance){ 34 | const wrappedComponent = route.sceneRef.getWrappedInstance(); 35 | if(wrappedComponent){ 36 | wrappedComponent.componentDidFocus && 37 | wrappedComponent.componentDidFocus(); 38 | } 39 | } 40 | route.sceneRef.componentDidFocus && route.sceneRef.componentDidFocus(); 41 | } 42 | 43 | configureScene(route) { 44 | if (route.sceneConfig) { 45 | return route.sceneConfig 46 | } 47 | return Navigator.SceneConfigs.PushFromRight 48 | } 49 | 50 | render() { 51 | return ( 52 | 53 | 58 | 59 | 60 | 61 | ) 62 | } 63 | } 64 | 65 | export default connect(state => ({ 66 | user: state.user, 67 | }), dispatch => ({ 68 | }), null, { 69 | withRef: true 70 | })(Navigation); 71 | -------------------------------------------------------------------------------- /source/component/panel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TouchableHighlight 6 | } from 'react-native'; 7 | 8 | import Icon from 'react-native-vector-icons/Ionicons'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import { ComponentStyles, CommonStyles, StyleConfig } from '../style'; 11 | 12 | class Panel extends Component { 13 | 14 | constructor (props) { 15 | super(props); 16 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 17 | } 18 | 19 | renderTitle(){ 20 | return ( 21 | 22 | 23 | { this.props.title } 24 | 25 | 26 | ) 27 | } 28 | 29 | renderDescr(){ 30 | if(this.props.descr){ 31 | return ( 32 | 33 | 34 | { this.props.descr } 35 | 36 | 37 | ) 38 | } 39 | } 40 | 41 | renderTail(){ 42 | if(this.props.tailControl){ 43 | return ( 44 | 45 | { this.props.tailControl } 46 | 47 | ) 48 | } 49 | } 50 | 51 | renderContent(){ 52 | return ( 53 | 54 | 55 | { this.renderTitle() } 56 | { this.renderDescr() } 57 | 58 | { this.renderTail() } 59 | 60 | ) 61 | } 62 | 63 | render() { 64 | if(this.props.onPress){ 65 | return ( 66 | this.props.onPress() } 68 | underlayColor ={ StyleConfig.touchable_press_color }> 69 | { this.renderContent() } 70 | 71 | ) 72 | } 73 | return this.renderContent(); 74 | } 75 | } 76 | 77 | export default Panel; 78 | -------------------------------------------------------------------------------- /source/component/plugin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Platform, 5 | StatusBar, 6 | } from 'react-native'; 7 | 8 | import Updater from './updater'; 9 | import Messager from './messager'; 10 | 11 | class Plugin extends Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | renderUpdater(){ 18 | if(Platform.OS === 'android'){ 19 | return ( 20 | 21 | ); 22 | } 23 | } 24 | 25 | renderMessager(){ 26 | return ( 27 | 28 | ); 29 | } 30 | 31 | render(){ 32 | return ( 33 | 34 | 38 | 39 | { this.renderUpdater() } 40 | { this.renderMessager() } 41 | 42 | ); 43 | } 44 | } 45 | 46 | export default Plugin; 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /source/component/router.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Platform, 3 | Navigator, 4 | BackAndroid, 5 | ToastAndroid 6 | } from 'react-native'; 7 | 8 | import TimerMixin from 'react-timer-mixin'; 9 | import * as View from '../view'; 10 | import * as RouterSceneConfig from '../config/routerScene'; 11 | 12 | class Router { 13 | constructor(navigator) { 14 | this.navigator = navigator; 15 | this._onHomeBackPress = this.onHomeBackPress.bind(this); 16 | this._onExitApp = this.exitApp.bind(this); 17 | if (Platform.OS === 'android') { 18 | BackAndroid.addEventListener('hardwareBackPress', this._onHomeBackPress); 19 | } 20 | } 21 | 22 | onHomeBackPress(){ 23 | let currentRoute = this.getCurrentRoute(); 24 | if (currentRoute.name !== 'home' && currentRoute.name!=='login' && currentRoute.name!='startup') { 25 | this.navigator.pop(); 26 | return true; 27 | } 28 | 29 | this.handleHomeBackPress(); 30 | return true; 31 | } 32 | 33 | handleHomeBackPress(){ 34 | if (Platform.OS === "android") { 35 | ToastAndroid.show("再按一次退出应用", ToastAndroid.SHORT); 36 | BackAndroid.removeEventListener("hardwareBackPress", this._onHomeBackPress); 37 | BackAndroid.addEventListener("hardwareBackPress", this._onExitApp); 38 | this.timer = TimerMixin.setInterval(() => { 39 | TimerMixin.clearInterval(this.timer); 40 | BackAndroid.removeEventListener("hardwareBackPress", this._onExitApp); 41 | BackAndroid.addEventListener("hardwareBackPress", this._onHomeBackPress); 42 | }, 2000); 43 | } 44 |  } 45 | 46 | exitApp(){ 47 | this.timer && TimerMixin.clearTimeout(this.timer); 48 | BackAndroid.exitApp(); 49 | } 50 | 51 | getRouteList(){ 52 | return this.navigator.getCurrentRoutes(); 53 | } 54 | 55 | getCurrentRoute(){ 56 | const routesList = this.getRouteList(); 57 | return routesList[routesList.length - 1]; 58 | } 59 | 60 | getPreviousRoute(){ 61 | const routesList = this.getRouteList(); 62 | return routesList[routesList.length - 2]; 63 | } 64 | 65 | getNavigator(){ 66 | return this.navigator; 67 | } 68 | 69 | setRoute(route, props = {}){ 70 | route.props = props; 71 | route.sceneConfig = route.sceneConfig ? route.sceneConfig : RouterSceneConfig.customPushFromRight; 72 | route.component = route.component; 73 | } 74 | 75 | pop() { 76 | this.navigator.pop(); 77 | } 78 | 79 | popN(n){ 80 | this.navigator.popN(n); 81 | } 82 | 83 | push(route, props = {}) { 84 | this.setRoute(route, props); 85 | this.navigator.push(route); 86 | } 87 | 88 | replace(route, props = {}){ 89 | this.setRoute(route, props); 90 | this.navigator.replace(route); 91 | } 92 | 93 | resetTo(route, props = {}){ 94 | this.setRoute(route, props); 95 | this.navigator.resetTo(route); 96 | } 97 | 98 | replacePrevious(route, props = {}){ 99 | this.setRoute(route, props); 100 | this.navigator.replacePrevious(route); 101 | } 102 | 103 | replacePreviousAndPop(route, props = {}){ 104 | this.setRoute(route, props); 105 | this.navigator.replacePreviousAndPop(route); 106 | } 107 | 108 | replaceAtIndex(route, index, props = {}){ 109 | this.setRoute(route, props); 110 | this.navigator.replaceAtIndex(route, index) 111 | } 112 | } 113 | 114 | export default Router; 115 | -------------------------------------------------------------------------------- /source/component/spinner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ActivityIndicator 5 | } from 'react-native'; 6 | 7 | import { CommonStyles, StyleConfig } from '../style'; 8 | 9 | class Spinner extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | 23 | 24 | ) 25 | } 26 | } 27 | 28 | export default Spinner; 29 | 30 | 31 | -------------------------------------------------------------------------------- /source/component/updater.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | NetInfo, 5 | Alert 6 | } from 'react-native'; 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import Config from '../config'; 10 | import Toast from '@remobile/react-native-toast'; 11 | import * as UpdateAction from '../action/update'; 12 | import { openLink } from '../common'; 13 | 14 | class Updater extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | } 19 | 20 | componentDidMount(){ 21 | this.getNetStatus().done((status)=>{ 22 | if (status) { 23 | this.getUpdateInfo(); 24 | }else{ 25 | Toast.showLongBottom("请检查你的网络连接"); 26 | } 27 | }); 28 | } 29 | 30 | getUpdateInfo(){ 31 | const { updateAction } = this.props; 32 | const currentVersion = Config.appInfo.version; 33 | updateAction.getUpdateInfo(currentVersion); 34 | } 35 | 36 | getNetStatus(){ 37 | return NetInfo.fetch().then((netinfo=> { 38 | return netinfo.toUpperCase() != 'NONE'; 39 | })); 40 | } 41 | 42 | formatUpdateContent(updateInfo){ 43 | let updateContent = updateInfo.content; 44 | if(updateContent){ 45 | return updateContent.split("|").join("\n"); 46 | } 47 | } 48 | 49 | showUpdateInfo(updateInfo){ 50 | const updateContent = this.formatUpdateContent(updateInfo); 51 | Alert.alert( 52 | updateInfo.title || '温馨提示', 53 | updateContent, 54 | [ 55 | { 56 | text: '拒绝', 57 | onPress: () => null 58 | }, 59 | { 60 | text: '支持', 61 | onPress: () => this.handleUpdatePress(updateInfo) 62 | } 63 | ] 64 | ) 65 | } 66 | 67 | handleUpdatePress(updateInfo){ 68 | openLink(updateInfo.link); 69 | } 70 | 71 | render() { 72 | const { router, update } = this.props; 73 | 74 | if(router){ 75 | const currentRoute = router.getCurrentRoute(); 76 | if(currentRoute && currentRoute.name && currentRoute.name === "home"){ 77 | if(update && update.content && update.link){ 78 | this.showUpdateInfo(update); 79 | } 80 | } 81 | } 82 | 83 | return null; 84 | } 85 | } 86 | 87 | export default connect(state => ({ 88 | update: state.update 89 | }), dispatch => ({ 90 | updateAction : bindActionCreators(UpdateAction, dispatch) 91 | }), null, { 92 | withRef: true 93 | })(Updater); 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /source/component/view.js: -------------------------------------------------------------------------------- 1 | import * as View from '../view'; 2 | 3 | export default ViewPage = { 4 | home: ()=>{ 5 | return { 6 | component: View.Home, 7 | name: 'home' 8 | } 9 | }, 10 | startup: ()=>{ 11 | return { 12 | component: View.Startup, 13 | name: 'startup' 14 | } 15 | }, 16 | login: ()=>{ 17 | return { 18 | component: View.Login, 19 | name: 'login' 20 | } 21 | }, 22 | post: ()=>{ 23 | return { 24 | component: View.Post, 25 | name: 'post' 26 | } 27 | }, 28 | author: ()=>{ 29 | return { 30 | component: View.Author, 31 | name: 'author' 32 | } 33 | }, 34 | postComment: ()=>{ 35 | return { 36 | component: View.PostComment, 37 | name: 'postComment' 38 | } 39 | }, 40 | search: ()=>{ 41 | return { 42 | component: View.Search, 43 | name: 'search' 44 | } 45 | }, 46 | setting: ()=>{ 47 | return { 48 | component: View.Setting, 49 | name: 'setting' 50 | } 51 | }, 52 | about: ()=>{ 53 | return { 54 | component: View.About, 55 | name: 'about' 56 | } 57 | }, 58 | offline: ()=>{ 59 | return { 60 | component: View.Offline, 61 | name: 'offline' 62 | } 63 | }, 64 | offlinePost: ()=>{ 65 | return { 66 | component: View.OfflinePost, 67 | name: 'offlinePost' 68 | } 69 | }, 70 | blink: ()=>{ 71 | return { 72 | component: View.Blink, 73 | name: 'blink' 74 | } 75 | }, 76 | question: ()=>{ 77 | return { 78 | component: View.Question, 79 | name: 'question' 80 | } 81 | }, 82 | commentAdd: ()=>{ 83 | return { 84 | component: View.CommentAdd, 85 | name: 'commentAdd' 86 | } 87 | }, 88 | blinkAdd: ()=>{ 89 | return { 90 | component: View.BlinkAdd, 91 | name: 'blinkAdd' 92 | } 93 | }, 94 | questionAdd: ()=>{ 95 | return { 96 | component: View.QuestionAdd, 97 | name: 'questionAdd' 98 | } 99 | }, 100 | user: ()=>{ 101 | return { 102 | component: View.User, 103 | name: 'user' 104 | } 105 | }, 106 | favorite: ()=>{ 107 | return { 108 | component: View.Favorite, 109 | name: 'favorite' 110 | } 111 | }, 112 | userAsset: ()=>{ 113 | return { 114 | component: View.UserAsset, 115 | name: 'userAsset' 116 | } 117 | }, 118 | searchDetail: ()=>{ 119 | return { 120 | component: View.SearchDetail, 121 | name: 'searchDetail' 122 | } 123 | }, 124 | feedback: ()=>{ 125 | return { 126 | component: View.Feedback, 127 | name: 'feedback' 128 | } 129 | }, 130 | update: ()=>{ 131 | return { 132 | component: View.Update, 133 | name: 'update' 134 | } 135 | }, 136 | tailSetting: ()=>{ 137 | return { 138 | component: View.TailSetting, 139 | name: 'tailSetting' 140 | } 141 | }, 142 | web: ()=>{ 143 | return { 144 | component: View.Web, 145 | name: 'web' 146 | } 147 | }, 148 | questionAnswerComment: ()=>{ 149 | return { 150 | component: View.QuestionAnswerComment, 151 | name: 'questionAnswerComment' 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /source/config/api.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | home: { 4 | list:"api/blogposts/@sitehome?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 5 | detail: "api/blogposts/<%=id%>/body", 6 | comments: "api/blogs/<%=blogger%>/posts/<%=id%>/comments?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 7 | comment_add: "api/blogs/<%=blogger%>/posts/<%=id%>/comments", 8 | }, 9 | rank: { 10 | //list:"api/blogposts/@picked?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 11 | //note: 上面这个官方接口,数据基本无更新,故替换为以下接口,数据格式是一致的。 12 | //接口说明:http://wcf.open.cnblogs.com/blog/help 13 | //因该接口返回数据格式为xml,故搭建了一个中间服务器,做了json化的处理。 14 | //2016-11-01 togayther 15 | list: "http://123.56.135.166/cnblog/post/rank?pageindex=<%=pageIndex%>&pagesize=<%=pageSize%>", 16 | detail: "api/blogposts/<%=id%>/body", 17 | comments: "api/blogs/<%=blogger%>/posts/<%=id%>/comments?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 18 | comment_add: "api/blogs/<%=blogger%>/posts/<%=id%>/comments", 19 | }, 20 | news: { 21 | list:"api/NewsItems?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 22 | detail: "api/newsitems/<%=id%>/body", 23 | comments: "api/news/<%=id%>/comments?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 24 | comment_add: "api/news/<%=id%>/comments", 25 | }, 26 | question: { 27 | list:"api/questions/@sitehome?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 28 | detail: "api/questions/<%=id%>", 29 | add:"api/questions", 30 | remove: "api/questions/<%=id%>", 31 | status: 'api/questions/<%=id%>?userId=<%=uid%>', 32 | comments: "api/questions/<%=id%>/answers", 33 | comment_add:"api/questions/<%=id%>/answers", 34 | }, 35 | answer: { 36 | comments: "api/questions/answers/<%=id%>/comments", 37 | comment_add: "api/questions/<%=id%>/answers/<%=id%>/comments" 38 | }, 39 | blink: { 40 | //这个列表接口,现在已经没有数据返回了。博客园应该做了什么调整 !-_-。 41 | list:"api/statuses/@all?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 42 | detail: "api/statuses/<%=id%>", 43 | add:"api/statuses", 44 | remove: "api/statuses/<%=id%>", 45 | comments: "api/statuses/<%=id%>/comments", 46 | comment_add: "api/statuses/<%=id%>/comments", 47 | }, 48 | favorite:{ 49 | list:"api/Bookmarks?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 50 | add: "api/Bookmarks", 51 | status:"api/Bookmarks?url=<%=url%>", 52 | remove:"api/bookmarks?url=<%=url%>" 53 | }, 54 | user: { 55 | info: "api/Users", 56 | auth: "token", 57 | home: "api/blogs/<%=blogger%>/posts?pageIndex=<%=pageIndex%>", 58 | blink: "api/statuses/@my?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 59 | question: "api/questions/@myquestion?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>", 60 | favorite: "api/Bookmarks?pageIndex=<%=pageIndex%>&pageSize=<%=pageSize%>" 61 | }, 62 | author: { 63 | detail: "api/blogs/<%=blogger%>", 64 | posts: "api/blogs/<%=blogger%>/posts?pageIndex=<%=pageIndex%>" 65 | }, 66 | search: { 67 | blog: "api/ZzkDocuments/1?keyWords=<%=key%>&pageIndex=<%=pageIndex%>&pageSize=10", 68 | news: "api/ZzkDocuments/2?keyWords=<%=key%>&pageIndex=<%=pageIndex%>", 69 | kb: "api/ZzkDocuments/4?keyWords=<%=key%>&pageIndex=<%=pageIndex%>", 70 | }, 71 | update: { 72 | info: "http://123.56.135.166/cnblog/update?version=<%=version%>" 73 | } 74 | } -------------------------------------------------------------------------------- /source/config/drawer.js: -------------------------------------------------------------------------------- 1 | 2 | import { postCategory } from './index'; 3 | 4 | export default [{ 5 | text: "首页", 6 | icon: "ios-home-outline", 7 | action: "refresh", 8 | flag: postCategory.home 9 | },{ 10 | text: "排行", 11 | icon: "ios-analytics-outline", 12 | action: "refresh", 13 | flag: postCategory.rank 14 | },{ 15 | text: "新闻", 16 | icon: "ios-color-filter-outline", 17 | action: "refresh", 18 | flag: postCategory.news 19 | },{ 20 | text: "闪存", 21 | icon: "ios-color-palette-outline", 22 | action: "refresh", 23 | flag: postCategory.blink 24 | },{ 25 | text: "博问", 26 | icon: "ios-document-outline", 27 | action: "refresh", 28 | flag: postCategory.question 29 | },{ 30 | text: "离线", 31 | icon: "ios-download-outline", 32 | action: "push", 33 | flag:"offline" 34 | }]; 35 | -------------------------------------------------------------------------------- /source/config/index.js: -------------------------------------------------------------------------------- 1 |  2 | export default { 3 | appInfo:{ 4 | name:'博客园', 5 | descr:'开发者的网上家园', 6 | site:'www.cnblogs.com', 7 | version: '3.6.0', 8 | copyright: '©2016 powered by react-native', 9 | registerUri: 'https://passport.cnblogs.com/register.aspx?ReturnUrl=http://www.cnblogs.com/', 10 | declare: '博客园创立于2004年1月,是一个面向开发者的知识分享社区。自创建以来,博客园一直致力并专注于为开发者打造一个纯净的技术交流社区,推动并帮助开发者通过互联网分享知识,从而让更多开发者从中受益。博客园的使命是帮助开发者用代码改变世界。' 11 | }, 12 | authorInfo: { 13 | name:'togayther', 14 | email:'sleepsleepsleep@foxmail.com', 15 | homepage: 'https://github.com/togayther', 16 | declare: '本软件为个人学习交流作品,内容来源于博客园官方开放接口,版权归博客园及原作者所有。' 17 | }, 18 | commentTail: 'from [url=http://fir.im/togayther]rn-cnblogs[/url]', 19 | apiDomain:'https://api.cnblogs.com/' 20 | }; 21 | 22 | export const postCategory = { 23 | home: "home", 24 | rank: "rank", 25 | news: "news", 26 | blink: "blink", 27 | question: "question", 28 | favorite: "favorite", 29 | answer: "answer" 30 | }; 31 | 32 | export const authData = { 33 | pubKey : "", 34 | clientId: "", 35 | clientSecret: "" 36 | }; 37 | 38 | export const pageSize = 10; 39 | 40 | export const storageKey = { 41 | OFFLINE_POSTS: "OFFLINE_POSTS", 42 | USER_TOKEN: "USER_TOKEN", 43 | TAIL_CONTENT: "TAIL_CONTENT", 44 | TAIL_ENABLED: "TAIL_ENABLED" 45 | }; -------------------------------------------------------------------------------- /source/config/refreshControl.js: -------------------------------------------------------------------------------- 1 | 2 | export default refreshControlConfig = { 3 | tintColor: "rgba(255, 255, 255, 1)", 4 | title: "加载中...", 5 | titleColor: "rgba(255, 255, 255, 1)", 6 | colors: ["rgba(199, 85, 74, 1)", "rgba(199, 85, 74, 0.9)", "rgba(199, 85, 74, 0.8)"] 7 | }; -------------------------------------------------------------------------------- /source/config/routerScene.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Dimensions, 3 | Navigator 4 | } from 'react-native'; 5 | 6 | const { width } = Dimensions.get('window'); 7 | 8 | const baseConfig = Navigator.SceneConfigs.PushFromRight; 9 | 10 | const popGestureConfig = Object.assign({}, baseConfig.gestures.pop, { 11 | edgeHitWidth: width / 4 12 | }); 13 | 14 | const fullPopGestureConfig = Object.assign({}, Navigator.SceneConfigs.FloatFromBottom.gestures.pop, { 15 | edgeHitWidth: width 16 | }); 17 | 18 | export const customFloatFromBottom = Object.assign({}, Navigator.SceneConfigs.FloatFromBottom, { 19 | gestures: { 20 | pop: fullPopGestureConfig 21 | } 22 | }); 23 | 24 | export const customPushFromRight = Object.assign({}, baseConfig, { 25 | gestures: { 26 | pop: popGestureConfig 27 | } 28 | }); 29 | 30 | -------------------------------------------------------------------------------- /source/constant/actiontype.js: -------------------------------------------------------------------------------- 1 | //post 2 | export const ADD_POST = 'ADD_POST'; 3 | export const REMOVE_POST = 'REMOVE_POST'; 4 | export const FETCH_POST_BY_ID = 'FETCH_POST_BY_ID'; 5 | export const FETCH_POSTS_BY_CATEGORY = 'FETCH_POSTS_BY_CATEGORY'; 6 | export const FETCH_POSTS_BY_CATEGORY_WITHPAGE = 'FETCH_POSTS_BY_CATEGORY_WITHPAGE'; 7 | 8 | //comment 9 | export const ADD_COMMENT = 'ADD_COMMENT'; 10 | export const FETCH_COMMENTS_BY_POST = 'FETCH_COMMENTS_BY_POST'; 11 | export const FETCH_COMMENTS_BY_POST_WITHPAGE = 'FETCH_COMMENTS_BY_POST_WITHPAGE'; 12 | 13 | //author 14 | export const FETCH_AUTHORS_BY_RANK = 'FETCH_AUTHORS_BY_RANK'; 15 | export const FETCH_AUTHORS_BY_KEY = 'FETCH_AUTHORS_BY_KEY'; 16 | export const FETCH_AUTHOR_DETAIL = 'FETCH_AUTHOR_DETAIL'; 17 | export const FETCH_AUTHOR_POSTS_WITHPAGE = 'FETCH_AUTHOR_POSTS_WITHPAGE'; 18 | export const FETCH_AUTHOR_POSTS = 'FETCH_AUTHOR_POSTS'; 19 | 20 | //user 21 | export const LOGIN = 'LOGIN'; 22 | export const REFRESH_TOKEN = 'REFRESH_TOKEN'; 23 | export const FETCH_USER_INFO = 'FETCH_USER_INFO'; 24 | export const FETCH_USER_ASSET = 'FETCH_USER_ASSET'; 25 | export const FETCH_USER_ASSET_WITHPAGE = 'FETCH_USER_ASSET_WITHPAGE'; 26 | 27 | //config 28 | export const UPDATE_CONFIG = 'UPDATE_CONFIG'; 29 | export const GET_CONFIG = 'GET_CONFIG'; 30 | export const REMOVE_CONFIG = 'REMOVE_CONFIG'; 31 | 32 | //offline 33 | export const OFFLINE_POST_TO_STORAGE = 'OFFLINE_POST_TO_STORAGE'; 34 | export const GET_POSTS_FROM_STORAGE = 'GET_POSTS_FROM_STORAGE'; 35 | export const REMOVE_POSTS_IN_STORAGE = 'REMOVE_POSTS_IN_STORAGE'; 36 | export const REMOVE_POST_IN_STORAGE = 'REMOVE_POST_IN_STORAGE'; 37 | export const GET_POST_FROM_STORAGE = 'GET_POST_FROM_STORAGE'; 38 | 39 | //search 40 | export const SEARCH_BY_KEY = 'SEARCH_BY_KEY'; 41 | export const SEARCH_BY_KEY_WITHPAGE = 'SEARCH_BY_KEY_WITHPAGE'; 42 | export const CLEAR_SEARCH_RESULT = 'CLEAR_SEARCH_RESULT'; 43 | 44 | //common 45 | export const SHOW_MESSAGE = 'SHOW_MESSAGE'; 46 | export const FETCH_UPDATE_INFO = 'FETCH_UPDATE_INFO'; -------------------------------------------------------------------------------- /source/image/author.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/author.png -------------------------------------------------------------------------------- /source/image/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/avatar.jpg -------------------------------------------------------------------------------- /source/image/header/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/1.jpg -------------------------------------------------------------------------------- /source/image/header/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/10.jpg -------------------------------------------------------------------------------- /source/image/header/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/11.jpg -------------------------------------------------------------------------------- /source/image/header/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/2.jpg -------------------------------------------------------------------------------- /source/image/header/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/3.jpg -------------------------------------------------------------------------------- /source/image/header/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/4.jpg -------------------------------------------------------------------------------- /source/image/header/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/5.jpg -------------------------------------------------------------------------------- /source/image/header/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/6.jpg -------------------------------------------------------------------------------- /source/image/header/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/7.jpg -------------------------------------------------------------------------------- /source/image/header/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/8.jpg -------------------------------------------------------------------------------- /source/image/header/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/header/9.jpg -------------------------------------------------------------------------------- /source/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/togayther/react-native-cnblogs/0248fda72e50758a9517c5c8328736ba069bc7a4/source/image/logo.png -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Store } from './reducer/store'; 4 | import Navigation from './component/navigation'; 5 | 6 | class ReactNativeCnblogsApp extends Component { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | 16 | export default ReactNativeCnblogsApp; 17 | -------------------------------------------------------------------------------- /source/middleware/callback.js: -------------------------------------------------------------------------------- 1 | export default function callback() { 2 | return next => action => { 3 | const { meta = {}, error, payload } = action; 4 | const { sequence = {}, resolved, rejected } = meta; 5 | if (sequence.type !== 'next') return next(action); 6 | 7 | error ? (rejected && rejected(payload)) : (resolved && resolved(payload)); 8 | 9 | next(action); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/middleware/common.js: -------------------------------------------------------------------------------- 1 | import * as commonAction from '../action/common'; 2 | 3 | export default function common({dispatch}) { 4 | return next => action => { 5 | const { payload, error, meta={} } = action; 6 | if (error === true && payload && payload.message) { 7 | dispatch(commonAction.message(payload.message)); 8 | } 9 | next(action); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/middleware/index.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk'; 2 | import logger from './logger'; 3 | import promise from './promise'; 4 | import common from './common'; 5 | import pending from './pending'; 6 | import callback from './callback'; 7 | 8 | export default [ 9 | logger, 10 | thunk, 11 | promise, 12 | pending, 13 | callback, 14 | common 15 | ]; -------------------------------------------------------------------------------- /source/middleware/logger.js: -------------------------------------------------------------------------------- 1 | 2 | export default function logger({ getState }) { 3 | return (next) => (action) => { 4 | if (__DEV__){ 5 | console.log('logger dispatching', action); 6 | } 7 | const result = next(action); 8 | if (__DEV__){ 9 | console.log('next state', getState()); 10 | } 11 | return result; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /source/middleware/pending.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constant/actiontype'; 2 | 3 | let sequenceList = {}; 4 | const minPendingTime = 500; 5 | 6 | /* 7 | * I just use this to fix a bugs about when the request is too quickly, the RefreshControl can't update the refreshing. 8 | * It's a bug, but I don't know how to fix it, so use this hack way to do it. 9 | * */ 10 | 11 | export default function ({dispatch}) { 12 | return next => action => { 13 | const { meta={}, payload } = action; 14 | const { sequence={}, category, pending } = meta; 15 | if(pending === true){ 16 | if (sequence.type == 'start') { 17 | sequenceList[sequence.id] = { 18 | start: new Date().getTime() 19 | }; 20 | return next(action); 21 | } 22 | 23 | if (sequence.type == 'next' && sequenceList[sequence.id]) { 24 | let start = sequenceList[sequence.id].start; 25 | let end = new Date().getTime(); 26 | let leftTime = minPendingTime - (end - start); 27 | delete sequenceList[sequence.id]; 28 | if (leftTime < 0) { 29 | return next(action); 30 | } 31 | else { 32 | return setTimeout(()=> { 33 | next(action); 34 | }, leftTime); 35 | } 36 | } 37 | } 38 | return next(action); 39 | }; 40 | } -------------------------------------------------------------------------------- /source/middleware/promise.js: -------------------------------------------------------------------------------- 1 | import { isFSA } from 'flux-standard-action'; 2 | import _ from 'lodash'; 3 | 4 | function isPromise(val) { 5 | return val && typeof val.then === 'function'; 6 | } 7 | 8 | export default function promise({ dispatch }) { 9 | return next => action => { 10 | if (!isFSA(action)) { 11 | return isPromise(action) 12 | ? action.then(dispatch) 13 | : next(action); 14 | } 15 | const { meta = {}, payload } = action; 16 | 17 | const uniqueid = _.uniqueId(); 18 | 19 | if (isPromise(payload)) { 20 | dispatch({ 21 | ...action, 22 | payload: undefined, 23 | meta: { 24 | ...meta, 25 | sequence: { 26 | type: 'start', 27 | uniqueid 28 | } 29 | } 30 | }); 31 | 32 | return payload.then( 33 | result => dispatch({ 34 | ...action, 35 | payload: result, 36 | meta: { 37 | ...meta, 38 | sequence: { 39 | type: 'next', 40 | uniqueid 41 | } 42 | } 43 | }), 44 | error => dispatch({ 45 | ...action, 46 | payload: error, 47 | error: true, 48 | meta: { 49 | ...meta, 50 | sequence: { 51 | type: 'next', 52 | uniqueid 53 | } 54 | } 55 | }) 56 | ); 57 | } 58 | 59 | return next(action); 60 | }; 61 | } -------------------------------------------------------------------------------- /source/reducer/author.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constant/actiontype'; 2 | 3 | export default function (state = {}, action) { 4 | 5 | const { payload, meta = {}, type, error } = action; 6 | const { sequence = {}, blogger } = meta; 7 | 8 | if (sequence.type === 'start' || error) { 9 | return state; 10 | } 11 | 12 | switch (type) { 13 | case types.FETCH_AUTHOR_DETAIL: 14 | return { 15 | ...state, 16 | [blogger]: payload 17 | }; 18 | case types.FETCH_AUTHOR_POSTS: 19 | return { 20 | ...state, 21 | [blogger]: { 22 | ...state[blogger], 23 | posts: payload 24 | } 25 | }; 26 | case types.FETCH_AUTHOR_POSTS_WITHPAGE: 27 | return { 28 | ...state, 29 | [blogger]: { 30 | ...state[blogger], 31 | posts: state[blogger].posts.concat(payload) 32 | } 33 | }; 34 | default: 35 | return state; 36 | } 37 | } -------------------------------------------------------------------------------- /source/reducer/authorUI.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constant/actiontype'; 2 | import { pageSize } from '../config'; 3 | 4 | 5 | 6 | export default function (state = {}, action) { 7 | const { type, meta={}, payload = [], error } = action; 8 | const { sequence={}, blogger } = meta; 9 | const pendingStatus = sequence.type == 'start'; 10 | 11 | switch (type) { 12 | case types.FETCH_AUTHOR_DETAIL: 13 | return { 14 | ...state, 15 | [blogger]:{ 16 | ...state[blogger], 17 | refreshPending: pendingStatus 18 | } 19 | }; 20 | case types.FETCH_AUTHOR_POSTS: 21 | return { 22 | ...state, 23 | [blogger]:{ 24 | ...state[blogger], 25 | refreshPending: pendingStatus, 26 | postPageEnabled: payload.length >= pageSize, 27 | postPageIndex: 1 28 | } 29 | }; 30 | case types.FETCH_AUTHOR_POSTS_WITHPAGE: 31 | return { 32 | ...state, 33 | [blogger]:{ 34 | ...state[blogger], 35 | postPageEnabled: payload.length >= pageSize, 36 | postPagePending: pendingStatus, 37 | postPageIndex: (!error && !pendingStatus) ? state[blogger].postPageIndex + 1: state[blogger].postPageIndex 38 | } 39 | }; 40 | default: 41 | return state; 42 | } 43 | } -------------------------------------------------------------------------------- /source/reducer/comment.js: -------------------------------------------------------------------------------- 1 | import { postCategory } from '../config'; 2 | import * as types from '../constant/actiontype'; 3 | 4 | export default function (state = {}, action) { 5 | const { payload, meta = {}, type, error } = action; 6 | const { sequence = {}, category, id } = meta; 7 | if (sequence.type === 'start' || error) { 8 | return state; 9 | } 10 | switch (type) { 11 | case types.FETCH_COMMENTS_BY_POST: 12 | return { 13 | ...state, 14 | [id]: payload 15 | }; 16 | case types.FETCH_COMMENTS_BY_POST_WITHPAGE: 17 | return { 18 | ...state, 19 | [id]:state[id].concat(payload) 20 | }; 21 | case types.ADD_COMMENT: 22 | return { 23 | ...state, 24 | }; 25 | default: 26 | return state; 27 | } 28 | } -------------------------------------------------------------------------------- /source/reducer/commentListUI.js: -------------------------------------------------------------------------------- 1 | 2 | import * as types from '../constant/actiontype'; 3 | import { pageSize } from '../config'; 4 | 5 | export default function (state = [], action) { 6 | 7 | const { payload = [], meta={}, type, error } = action; 8 | const { sequence = {}, id } = meta; 9 | const pendingStatus = sequence.type === 'start'; 10 | 11 | switch (type) { 12 | case types.FETCH_COMMENTS_BY_POST: 13 | return { 14 | ...state, 15 | [id]: { 16 | ...state[id], 17 | refreshPending: pendingStatus, 18 | pageIndex: 1, 19 | pageEnabled: payload.length >= pageSize 20 | } 21 | }; 22 | case types.FETCH_COMMENTS_BY_POST_WITHPAGE: 23 | return { 24 | ...state, 25 | [id]: { 26 | ...state[id], 27 | pagePending: pendingStatus, 28 | pageEnabled: payload.length >= pageSize, 29 | pageIndex: (!error && !pendingStatus) ? state[id].pageIndex + 1: state[id].pageIndex 30 | } 31 | }; 32 | default: 33 | return state; 34 | } 35 | } -------------------------------------------------------------------------------- /source/reducer/common.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constant/actiontype'; 2 | 3 | const initialState = { 4 | message: { 5 | id: null, 6 | text: null 7 | } 8 | }; 9 | 10 | export default function (state = initialState, action) { 11 | const { payload = {} } = action; 12 | switch (action.type) { 13 | case types.SHOW_MESSAGE: 14 | return { 15 | ...state, 16 | message: { 17 | ...state.message, 18 | ...payload 19 | } 20 | }; 21 | default : 22 | return state; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/reducer/config.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import * as types from '../constant/actiontype'; 4 | 5 | export default function (state = {}, action) { 6 | 7 | const { payload, meta = {}, type, error } = action; 8 | const { key, value } = meta; 9 | switch (type) { 10 | 11 | case types.GET_CONFIG: 12 | return { 13 | ...state, 14 | [key]: payload 15 | }; 16 | case types.UPDATE_CONFIG: 17 | return { 18 | ...state, 19 | [key]: value 20 | } 21 | 22 | case types.REMOVE_CONFIG: 23 | delete state[key]; 24 | return state; 25 | 26 | default: 27 | return state; 28 | } 29 | } -------------------------------------------------------------------------------- /source/reducer/index.js: -------------------------------------------------------------------------------- 1 | export { default as post } from './post'; 2 | export { default as postListUI } from './postListUI'; 3 | export { default as postDetailUI } from './postDetailUI'; 4 | export { default as author } from './author'; 5 | export { default as authorUI } from './authorUI'; 6 | export { default as comment } from './comment'; 7 | export { default as commentListUI } from './commentListUI'; 8 | export { default as user } from './user'; 9 | export { default as userListUI } from './userListUI'; 10 | export { default as offline } from './offline'; 11 | export { default as common } from './common'; 12 | export { default as config } from './config'; 13 | export { default as search } from './search'; 14 | export { default as update } from './update'; 15 | export { default as searchUI } from './searchUI'; -------------------------------------------------------------------------------- /source/reducer/offline.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import { postCategory } from '../config'; 4 | import * as types from '../constant/actiontype'; 5 | 6 | function formatOfflinePosts(posts){ 7 | let results = []; 8 | if (posts) { 9 | _.mapValues(posts, (post)=>{ 10 | if (post && post.Id) { 11 | delete post.postContent; 12 | results.push(post); 13 | } 14 | }); 15 | results = _.orderBy(results, ["offlineDate"], ["desc"]); 16 | } 17 | 18 | return results; 19 | } 20 | 21 | function removePostById(posts, id){ 22 | let results = []; 23 | if (posts && posts.length && id) { 24 | posts.map((post)=>{ 25 | if (post.Id !== id) { 26 | results.push(post); 27 | } 28 | }); 29 | } 30 | return results; 31 | } 32 | 33 | export default function (state = {}, action) { 34 | 35 | const { payload, meta = {}, type, error } = action; 36 | const { sequence = {} } = meta; 37 | 38 | if (sequence.type === 'start' || error) { 39 | return state; 40 | } 41 | 42 | switch (type) { 43 | 44 | case types.GET_POSTS_FROM_STORAGE: 45 | return { 46 | ...state, 47 | posts: formatOfflinePosts(payload) 48 | }; 49 | 50 | case types.GET_POST_FROM_STORAGE: 51 | return { 52 | ...state, 53 | postContent: payload 54 | }; 55 | 56 | case types.REMOVE_POST_IN_STORAGE: 57 | return{ 58 | ...state, 59 | posts: removePostById(state.posts, meta.id) 60 | }; 61 | 62 | case types.REMOVE_POSTS_IN_STORAGE: 63 | return []; 64 | 65 | default: 66 | return state; 67 | } 68 | } -------------------------------------------------------------------------------- /source/reducer/post.js: -------------------------------------------------------------------------------- 1 | import { postCategory } from '../config'; 2 | import * as types from '../constant/actiontype'; 3 | 4 | let initialState = { 5 | posts: {} 6 | }; 7 | 8 | Object.keys(postCategory).map((item)=> { 9 | initialState[item] = []; 10 | }); 11 | 12 | function removePost(state, category, id){ 13 | let results = [], 14 | posts = state[category]; 15 | if(posts && posts.length){ 16 | for(let i = 0, len = posts.length; i { 8 | initialState[item] = { 9 | pageEnabled: true, 10 | pageIndex: 1, 11 | pagePending: false, 12 | refreshPending: false 13 | } 14 | }); 15 | 16 | export default function (state = initialState, action) { 17 | 18 | const { payload = [], meta={}, type, error } = action; 19 | const { sequence = {}, category, authorId } = meta; 20 | const pendingStatus = sequence.type === 'start'; 21 | 22 | switch (type) { 23 | case types.FETCH_POSTS_BY_CATEGORY: 24 | return { 25 | ...state, 26 | [category]: { 27 | ...state[category], 28 | refreshPending: pendingStatus, 29 | pageEnabled: payload && (payload.length >= pageSize), 30 | pageIndex: initialState[category].pageIndex 31 | } 32 | }; 33 | case types.FETCH_POSTS_BY_CATEGORY_WITHPAGE: 34 | return { 35 | ...state, 36 | [category]: { 37 | ...state[category], 38 | pagePending: pendingStatus, 39 | pageEnabled: payload && (payload.length >= pageSize), 40 | pageIndex: (!error && !pendingStatus) ? state[category].pageIndex + 1: state[category].pageIndex 41 | } 42 | }; 43 | default: 44 | return state; 45 | } 46 | } -------------------------------------------------------------------------------- /source/reducer/search.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constant/actiontype'; 2 | 3 | export default function (state = {}, action) { 4 | 5 | const { payload, meta = {}, type, error } = action; 6 | const { sequence = {}, category, key } = meta; 7 | 8 | if (sequence.type === 'start' || error) { 9 | return state; 10 | } 11 | 12 | switch (type) { 13 | case types.SEARCH_BY_KEY: 14 | return { 15 | ...state, 16 | [category]: payload 17 | }; 18 | case types.SEARCH_BY_KEY_WITHPAGE: 19 | return { 20 | ...state, 21 | [category]: state[category].concat(payload) 22 | }; 23 | case types.CLEAR_SEARCH_RESULT: 24 | return { 25 | ...state, 26 | [category]: [] 27 | }; 28 | default: 29 | return state; 30 | } 31 | } -------------------------------------------------------------------------------- /source/reducer/searchUI.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constant/actiontype'; 2 | 3 | let initialState = {}; 4 | 5 | ["blog", "news", "kb"].map((item)=> { 6 | initialState[item] = { 7 | searchPending: false, 8 | pagePending: false, 9 | pageEnabled: true, 10 | pageIndex: 1 11 | } 12 | }); 13 | 14 | const pageSize = 15; 15 | 16 | export default function (state = initialState, action) { 17 | 18 | const { payload = [], meta={}, type, error } = action; 19 | const { sequence = {}, category } = meta; 20 | const pendingStatus = sequence.type === 'start'; 21 | 22 | switch (type) { 23 | case types.SEARCH_BY_KEY: 24 | return { 25 | ...state, 26 | [category]: { 27 | ...state[category], 28 | searchPending: pendingStatus, 29 | pagePending: pendingStatus, 30 | pageEnabled: payload.length >= pageSize, 31 | pageIndex: initialState[category].pageIndex 32 | } 33 | }; 34 | case types.SEARCH_BY_KEY_WITHPAGE: 35 | return { 36 | ...state, 37 | [category]: { 38 | ...state[category], 39 | pagePending: pendingStatus, 40 | pageEnabled: payload.length >= pageSize, 41 | pageIndex: (!error && !pendingStatus) ? state[category].pageIndex + 1: state[category].pageIndex 42 | } 43 | }; 44 | default: 45 | return state; 46 | } 47 | } -------------------------------------------------------------------------------- /source/reducer/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; 2 | import * as reducers from '../reducer'; 3 | import middlewares from '../middleware'; 4 | 5 | const isDebuggingInBrowser = __DEV__ && !!window.navigator.userAgent; 6 | 7 | const reducer = combineReducers(reducers); 8 | 9 | const store = applyMiddleware( 10 | ...middlewares 11 | )(createStore)(reducer); 12 | 13 | if (module.hot) { 14 | module.hot.accept(() => { 15 | store.replaceReducer(reducer); 16 | }); 17 | } 18 | 19 | if (isDebuggingInBrowser) { 20 | window.store = store; 21 | } 22 | 23 | export const Store = store; 24 | -------------------------------------------------------------------------------- /source/reducer/update.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constant/actiontype'; 2 | 3 | export default function (state = {}, action) { 4 | 5 | const { payload, meta = {}, type } = action; 6 | switch (type) { 7 | 8 | case types.FETCH_UPDATE_INFO: 9 | return { 10 | ...state, 11 | ...payload 12 | }; 13 | 14 | default: 15 | return state; 16 | } 17 | } -------------------------------------------------------------------------------- /source/reducer/user.js: -------------------------------------------------------------------------------- 1 | import Config, { postCategory } from '../config'; 2 | import * as types from '../constant/actiontype'; 3 | 4 | function removePost(state, category, id, url){ 5 | let results = [], 6 | posts = state[category]; 7 | if(posts && posts.length){ 8 | for(let i = 0, len = posts.length; i { 8 | initialState[item] = { 9 | pageEnabled: true, 10 | pageIndex: 1, 11 | pagePending: false, 12 | refreshPending: false 13 | } 14 | }); 15 | 16 | export default function (state = initialState, action) { 17 | 18 | const { payload = [], meta={}, type, error } = action; 19 | const { sequence = {}, category } = meta; 20 | const pendingStatus = sequence.type === 'start'; 21 | 22 | switch (type) { 23 | case types.FETCH_USER_ASSET: 24 | return { 25 | ...state, 26 | [category]: { 27 | ...state[category], 28 | refreshPending: pendingStatus, 29 | pageEnabled: payload.length >= pageSize, 30 | pageIndex: initialState[category].pageIndex 31 | } 32 | }; 33 | case types.FETCH_USER_ASSET_WITHPAGE: 34 | return { 35 | ...state, 36 | [category]: { 37 | ...state[category], 38 | pagePending: pendingStatus, 39 | pageEnabled: payload.length >= pageSize, 40 | pageIndex: (!error && !pendingStatus) ? state[category].pageIndex + 1: state[category].pageIndex 41 | } 42 | }; 43 | default: 44 | return state; 45 | } 46 | } -------------------------------------------------------------------------------- /source/service/authorService.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as requestService from './request'; 3 | import { pageSize } from '../config'; 4 | import dataApi from '../config/api'; 5 | 6 | export function getAuthorDetail(blogger){ 7 | let params = { blogger }; 8 | 9 | let fetchApi = dataApi['author']['detail']; 10 | let strCompiled = _.template(fetchApi); 11 | fetchApi = strCompiled(params); 12 | 13 | return requestService.get(fetchApi); 14 | } 15 | 16 | export function getAuthorPosts(blogger, params){ 17 | params.blogger = blogger; 18 | params.pageSize = pageSize; 19 | 20 | let fetchApi = dataApi['author']['posts']; 21 | let strCompiled = _.template(fetchApi); 22 | fetchApi = strCompiled(params); 23 | 24 | return requestService.get(fetchApi); 25 | } -------------------------------------------------------------------------------- /source/service/commentService.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as requestService from './request'; 3 | import { convertJSONToFormData } from '../common'; 4 | import { pageSize, postCategory } from '../config'; 5 | import dataApi from '../config/api'; 6 | 7 | function formatCommentData(category, data){ 8 | //经过多次测试,官方的博文评论添加接口,仅支持xml 9 | //其它支持常规的表单提交,所以这里兼容处理一下 10 | let commentData; 11 | if(category === postCategory.home || category === postCategory.rank){ 12 | commentData = '' + data.Content + ''; 13 | }else{ 14 | commentData = convertJSONToFormData(data); 15 | } 16 | return commentData; 17 | } 18 | 19 | function formatCommentHeader(category){ 20 | let headers; 21 | if(category === postCategory.home || category === postCategory.rank){ 22 | headers = { 23 | "Content-type":'application/xml' 24 | } 25 | } 26 | return headers; 27 | } 28 | 29 | export function getCommentsByPost(category, id, params = {}){ 30 | params.pageSize = pageSize; 31 | params.id = id; 32 | let fetchApi = dataApi[category]["comments"]; 33 | let strCompiled = _.template(fetchApi); 34 | fetchApi = strCompiled(params); 35 | return requestService.get(fetchApi); 36 | } 37 | 38 | export function addComment(category, params, data){ 39 | let fetchApi = dataApi[category]["comment_add"]; 40 | let strCompiled = _.template(fetchApi); 41 | fetchApi = strCompiled(params); 42 | 43 | let commentHeaders = formatCommentHeader(category); 44 | let commentData = formatCommentData(category, data); 45 | return requestService.post(fetchApi, commentData, commentHeaders); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /source/service/postService.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as requestService from './request'; 3 | import { convertJSONToFormData } from '../common'; 4 | import { postCategory, pageSize } from '../config'; 5 | import dataApi from '../config/api'; 6 | 7 | 8 | function formatPostData(category, data){ 9 | let postData; 10 | if( category === postCategory.question ){ 11 | let questionData = []; 12 | questionData.push(''); 13 | questionData.push('' + data.Content +''); 14 | questionData.push('' + data.Flags+ ''); 15 | questionData.push('' + data.Tags + ''); 16 | questionData.push('' + data.Title + ''); 17 | questionData.push(''); 18 | postData = questionData.join(""); 19 | }else{ 20 | postData = convertJSONToFormData(data); 21 | } 22 | return postData; 23 | } 24 | 25 | function formatPostHeader(category){ 26 | let headers; 27 | if(category === postCategory.question){ 28 | headers = { 29 | "Content-type":'application/xml' 30 | } 31 | } 32 | return headers; 33 | } 34 | 35 | export function getPostByCategory(category = postCategory.home , params = {}){ 36 | params.pageSize = pageSize; 37 | 38 | let fetchApi = dataApi[category]["list"]; 39 | let strCompiled = _.template(fetchApi); 40 | fetchApi = strCompiled(params); 41 | 42 | return requestService.get(fetchApi); 43 | } 44 | 45 | export function getPostById(category, id){ 46 | let params = { id }; 47 | let fetchApi = dataApi[category]["detail"]; 48 | let strCompiled = _.template(fetchApi); 49 | fetchApi = strCompiled(params); 50 | 51 | return requestService.get(fetchApi); 52 | } 53 | 54 | export function addPost(category, params){ 55 | let fetchApi = dataApi[category]["add"]; 56 | let strCompiled = _.template(fetchApi); 57 | fetchApi = strCompiled(params); 58 | 59 | let data = formatPostData(category, params); 60 | let headers = formatPostHeader(category); 61 | return requestService.post(fetchApi, data, headers); 62 | } 63 | 64 | export function removePost(category, params){ 65 | let fetchApi = dataApi[category]["remove"]; 66 | let strCompiled = _.template(fetchApi); 67 | fetchApi = strCompiled(params); 68 | 69 | return requestService.remove(fetchApi); 70 | } 71 | 72 | -------------------------------------------------------------------------------- /source/service/request.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Config, { authData } from '../config'; 3 | import { Base64 } from '../common/base64'; 4 | import * as UserService from './userService'; 5 | 6 | const apiDomain = Config.apiDomain; 7 | const timeout = 15000; 8 | 9 | function filterJSON(res) { 10 | try{ 11 | if(res.headers.get("content-length") > 0){ 12 | return res.json(); 13 | } 14 | } 15 | catch(e){ 16 | throw new Error('data format error'); 17 | } 18 | } 19 | 20 | function filterStatus(res) { 21 | if (res.ok) { 22 | return res; 23 | } else { 24 | throw new Error(''); 25 | } 26 | } 27 | 28 | function timeoutFetch(ms, promise) { 29 | return new Promise((resolve, reject) => { 30 | const timer = setTimeout(() => { 31 | reject(new Error("fetch time out")); 32 | }, ms); 33 | promise.then( 34 | (res) => { 35 | clearTimeout(timer); 36 | resolve(res); 37 | }, 38 | (err) => { 39 | clearTimeout(timer); 40 | reject(err); 41 | } 42 | ); 43 | }) 44 | } 45 | 46 | export function request(uri, type = "GET", headers = {}, data = ""){ 47 | return UserService.getToken().then((token)=>{ 48 | if(!headers["Authorization"]){ 49 | headers["Authorization"] = `Bearer ${token && token.access_token}`; 50 | } 51 | if(!_.startsWith(uri, "http")){ 52 | uri = Config.apiDomain + uri; 53 | } 54 | 55 | let fetchOption = { 56 | method: type, 57 | headers: headers 58 | }; 59 | 60 | if(type === "POST"){ 61 | fetchOption.body = data; 62 | } 63 | 64 | if(__DEV__){ 65 | console.log("fetch data from uri:"); 66 | console.log(uri); 67 | console.log("type"); 68 | console.log(type); 69 | console.log("headers:"); 70 | console.log(headers); 71 | console.log("data:"); 72 | console.log(data); 73 | } 74 | 75 | return timeoutFetch(timeout, fetch(uri, fetchOption)) 76 | .then(filterStatus) 77 | .then(filterJSON) 78 | .catch(function(error) { 79 | throw error; 80 | }); 81 | }); 82 | } 83 | 84 | export function get(uri, headers = {}) { 85 | return request(uri, "GET", headers); 86 | } 87 | 88 | export function post(uri, data = "", headers = {}) { 89 | if(!headers["Content-type"]){ 90 | headers["Content-type"] = 'application/x-www-form-urlencoded'; 91 | } 92 | return request(uri, "POST", headers, data); 93 | } 94 | 95 | export function remove(uri, headers = {}) { 96 | return request(uri, "DELETE", headers); 97 | } -------------------------------------------------------------------------------- /source/service/searchService.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as requestService from './request'; 3 | import dataApi from '../config/api'; 4 | 5 | export function searchByKey(category, key, params = {}){ 6 | params.key = key; 7 | let fetchApi = dataApi.search[category]; 8 | let strCompiled = _.template(fetchApi); 9 | fetchApi = strCompiled(params); 10 | return requestService.get(fetchApi); 11 | } 12 | -------------------------------------------------------------------------------- /source/service/storage.js: -------------------------------------------------------------------------------- 1 | import React,{ 2 | AsyncStorage 3 | } from 'react-native'; 4 | 5 | export function setItem(key, value) { 6 | if (key && value){ 7 | return AsyncStorage.setItem(key, JSON.stringify(value)); 8 | } 9 | } 10 | 11 | export function mergeItem(key, value) { 12 | if (key && value){ 13 | return AsyncStorage.mergeItem(key, JSON.stringify(value)); 14 | } 15 | } 16 | 17 | export function getItem(key) { 18 | return AsyncStorage.getItem(key) 19 | .then(function (value) { 20 | return JSON.parse(value) 21 | }); 22 | } 23 | 24 | export function multiGet(keys) { 25 | return AsyncStorage.multiGet(keys) 26 | .then(results=> { 27 | return results.map(item=> { 28 | return [item[0], JSON.parse(item[1])] 29 | }) 30 | }); 31 | } 32 | 33 | export function multiRemove(keys) { 34 | return AsyncStorage.multiRemove(keys) 35 | } 36 | 37 | export const removeItem = AsyncStorage.removeItem; 38 | 39 | export const clear = AsyncStorage.clear; -------------------------------------------------------------------------------- /source/service/updateService.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as requestService from './request'; 3 | import dataApi from '../config/api'; 4 | 5 | 6 | export function getUpdateInfo(version){ 7 | let fetchApi = dataApi.update.info; 8 | let strCompiled = _.template(fetchApi); 9 | fetchApi = strCompiled({ version }); 10 | return requestService.get(fetchApi); 11 | } 12 | -------------------------------------------------------------------------------- /source/service/userService.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as requestService from './request'; 3 | import { Base64 } from '../common/base64'; 4 | import * as storageService from './storage'; 5 | import { authData, storageKey, pageSize } from '../config'; 6 | import dataApi from '../config/api'; 7 | 8 | export function login(username, password){ 9 | let fetchApi = dataApi.user.auth; 10 | let data = `grant_type=password&username=${username}&password=${password}`.replace(/\+/g, "%2B"); 11 | let headers = { 12 | 'Authorization': "Basic " + Base64.encode(`${authData.clientId}:${authData.clientSecret}`) 13 | }; 14 | return requestService.post(fetchApi, data, headers); 15 | } 16 | 17 | export function refreshToken(token){ 18 | let fetchApi = dataApi.user.auth; 19 | let data = `grant_type=refresh_token&refresh_token=${token}`; 20 | let headers = { 21 | 'Authorization': "Basic " + Base64.encode(`${authData.clientId}:${authData.clientSecret}`) 22 | }; 23 | return requestService.post(fetchApi, data, headers); 24 | } 25 | 26 | export function getToken(){ 27 | return storageService.getItem(storageKey.USER_TOKEN); 28 | } 29 | 30 | export function getUserInfo(){ 31 | let fetchApi = dataApi.user.info; 32 | return requestService.get(fetchApi); 33 | } 34 | 35 | export function getUserAsset(category, params = {}){ 36 | params.pageSize = pageSize; 37 | let fetchApi = dataApi.user[category]; 38 | let strCompiled = _.template(fetchApi); 39 | fetchApi = strCompiled(params); 40 | return requestService.get(fetchApi); 41 | } -------------------------------------------------------------------------------- /source/view/about.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | Platform, 7 | ScrollView, 8 | StyleSheet 9 | } from 'react-native'; 10 | 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import Panel from '../component/panel'; 13 | import Navbar from '../component/navbar'; 14 | import Config from '../config'; 15 | import ViewPage from '../component/view'; 16 | import { ComponentStyles, CommonStyles } from '../style'; 17 | 18 | const navTitle = '关于'; 19 | const authorAvatar = require('../image/author.png'); 20 | 21 | class AboutPage extends Component { 22 | 23 | constructor (props) { 24 | super(props); 25 | } 26 | 27 | renderNavbar(){ 28 | return ( 29 | this.props.router.pop() } 31 | title={ navTitle }/> 32 | ) 33 | } 34 | 35 | renderAboutItem(){ 36 | return ( 37 | 40 | ) 41 | } 42 | 43 | renderDeclareItem(){ 44 | return ( 45 | 48 | ) 49 | } 50 | 51 | renderAuthorItem(){ 52 | const tailImage = 56 | 57 | return ( 58 | 62 | ) 63 | } 64 | 65 | renderUpdateItem(){ 66 | if(Platform.OS === 'android'){ 67 | const tailIcon = 71 | 72 | return ( 73 | this.props.router.push(ViewPage.update())} 76 | descr = { "这里可以查看更新历史记录" } 77 | tailControl = { tailIcon }/> 78 | ) 79 | } 80 | } 81 | renderFooterPatch(){ 82 | return ( 83 | 84 | 85 | ) 86 | } 87 | 88 | 89 | renderCopyright(){ 90 | return ( 91 | 92 | 93 | { Config.appInfo.copyright } 94 | 95 | 96 | ) 97 | } 98 | 99 | renderContent(){ 100 | return ( 101 | 104 | { this.renderAboutItem() } 105 | { this.renderUpdateItem() } 106 | { this.renderDeclareItem() } 107 | { this.renderAuthorItem() } 108 | { this.renderFooterPatch() } 109 | 110 | ) 111 | } 112 | 113 | render() { 114 | return ( 115 | 116 | { this.renderNavbar() } 117 | { this.renderContent() } 118 | { this.renderCopyright() } 119 | 120 | ); 121 | } 122 | } 123 | 124 | export const styles = StyleSheet.create({ 125 | avatar:{ 126 | width: 50, 127 | height: 50, 128 | borderRadius: 25 129 | }, 130 | footer:{ 131 | bottom : 0 132 | }, 133 | footerPatch: { 134 | height: 60 135 | } 136 | }); 137 | 138 | export default AboutPage; -------------------------------------------------------------------------------- /source/view/author.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | RefreshControl 5 | } from 'react-native'; 6 | 7 | import { bindActionCreators } from 'redux'; 8 | import { connect } from 'react-redux'; 9 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 10 | import * as AuthorAction from '../action/author'; 11 | import SingleButton from '../component/button/single'; 12 | import AuthorRender from '../component/header/author'; 13 | import AuthorPostList from '../component/listview/authorPostList'; 14 | import refreshControlConfig from '../config/refreshControl'; 15 | import { ComponentStyles } from '../style'; 16 | 17 | class AuthorPage extends Component { 18 | 19 | constructor (props) { 20 | super(props); 21 | this.state = { 22 | hasFocus: false 23 | }; 24 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 25 | } 26 | 27 | componentDidFocus() { 28 | this.setState({ 29 | hasFocus: true 30 | }); 31 | } 32 | 33 | componentDidMount(){ 34 | const { authorAction, blogger } = this.props; 35 | authorAction.getAuthorDetail(blogger).then(()=>{ 36 | authorAction.getAuthorPosts(blogger); 37 | }); 38 | } 39 | 40 | onListEndReached(){ 41 | const { authorAction, ui, blogger } = this.props; 42 | if (ui && ui.postPageEnabled) { 43 | authorAction.getAuthorPostsWithPage(blogger, { 44 | pageIndex: ui.postPageIndex + 1 45 | }); 46 | } 47 | } 48 | 49 | renderListRefreshControl(){ 50 | const { authorAction, blogger, ui } = this.props; 51 | if(ui && typeof ui.postPageEnabled !== "undefined"){ 52 | return ( 53 | { authorAction.getAuthorPosts(blogger) } } /> 56 | ); 57 | } 58 | } 59 | 60 | renderAuthorContent(){ 61 | const { author, ui } = this.props; 62 | 63 | if (this.state.hasFocus === false || (ui && ui.refreshPending !== false)) { 64 | return null; 65 | } 66 | 67 | if (author && author.posts) { 68 | return ( 69 | 73 | ) 74 | } 75 | } 76 | 77 | render() { 78 | return ( 79 | 80 | this.onListEndReached() } > 86 | { this.renderAuthorContent() } 87 | 88 | 89 | this.props.router.pop() }/> 90 | 91 | ); 92 | } 93 | } 94 | 95 | export default connect((state, props) => ({ 96 | author: state.author[props.blogger], 97 | ui: state.authorUI[props.blogger] 98 | }), dispatch => ({ 99 | authorAction : bindActionCreators(AuthorAction, dispatch) 100 | }), null, { 101 | withRef: true 102 | })(AuthorPage); -------------------------------------------------------------------------------- /source/view/favorite.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View 4 | } from 'react-native'; 5 | 6 | import { bindActionCreators } from 'redux'; 7 | import { connect } from 'react-redux'; 8 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 9 | import * as PostAction from '../action/post'; 10 | import Spinner from '../component/spinner'; 11 | import SingleButton from '../component/button/single'; 12 | import HtmlConvertor from '../component/htmlConvertor'; 13 | import HintMessage from '../component/hintMessage'; 14 | import FavoriteRender from '../component/header/favorite'; 15 | import { storageKey, postCategory } from '../config'; 16 | import { StyleConfig, ComponentStyles, CommonStyles } from '../style'; 17 | 18 | class FavoritePage extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | hasFocus: false 24 | }; 25 | this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | const { postAction, id, post, postContent, category } = this.props; 30 | if(!postContent){ 31 | postAction.getPostById(category, id); 32 | } 33 | } 34 | 35 | componentDidFocus() { 36 | this.setState({ 37 | hasFocus: true 38 | }); 39 | } 40 | 41 | renderPost() { 42 | const { id, postContent, ui, config, router } = this.props; 43 | if (this.state.hasFocus === false || ui.loadPending[id] !== false) { 44 | return ( 45 | 46 | ) 47 | } 48 | if (postContent) { 49 | return ( 50 | 51 | 54 | 55 | 56 | ) 57 | } 58 | return( 59 | 60 | ); 61 | } 62 | 63 | render() { 64 | const { post, router } = this.props; 65 | return ( 66 | 67 | 70 | { this.renderPost() } 71 | 72 | this.props.router.pop() }/> 73 | 74 | ) 75 | } 76 | } 77 | 78 | export default connect((state, props) => ({ 79 | postContent: state.post.posts[props.id], 80 | config: state.config, 81 | ui: state.postDetailUI 82 | }), dispatch => ({ 83 | postAction : bindActionCreators(PostAction, dispatch) 84 | }), null, { 85 | withRef: true 86 | })(FavoritePage); -------------------------------------------------------------------------------- /source/view/index.js: -------------------------------------------------------------------------------- 1 | export { default as Home } from './home'; 2 | export { default as Post } from './post'; 3 | export { default as Search } from './search'; 4 | export { default as About } from './about'; 5 | export { default as Author } from './author'; 6 | export { default as PostComment } from './postComment'; 7 | export { default as Setting } from './setting'; 8 | export { default as Offline } from './offline'; 9 | export { default as OfflinePost } from './offlinePost'; 10 | export { default as Login } from './login'; 11 | export { default as Startup } from './startup'; 12 | export { default as Blink } from './blink'; 13 | export { default as Question } from './question'; 14 | export { default as CommentAdd } from './commentAdd'; 15 | export { default as BlinkAdd } from './blinkAdd'; 16 | export { default as QuestionAdd } from './questionAdd'; 17 | export { default as User } from './user'; 18 | export { default as Favorite } from './favorite'; 19 | export { default as UserAsset } from './userAsset'; 20 | export { default as SearchDetail } from './searchDetail'; 21 | export { default as Feedback } from './feedback'; 22 | export { default as Update } from './update'; 23 | export { default as TailSetting } from './tailSetting'; 24 | export { default as Web } from './web'; 25 | export { default as QuestionAnswerComment } from './questionAnswerComment'; -------------------------------------------------------------------------------- /source/view/offlinePost.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Alert, 6 | StyleSheet 7 | } from 'react-native'; 8 | 9 | import { bindActionCreators } from 'redux'; 10 | import { connect } from 'react-redux'; 11 | import * as OfflineAction from '../action/offline'; 12 | import Spinner from '../component/spinner'; 13 | import SingleButton from '../component/button/single'; 14 | import HtmlConvertor from '../component/htmlConvertor'; 15 | import HintMessage from '../component/hintMessage'; 16 | import OfflinePostRender from '../component/header/offlinePost'; 17 | import { StyleConfig, ComponentStyles, CommonStyles } from '../style'; 18 | 19 | class OfflinePostPage extends Component { 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | hasFocus: false 25 | } 26 | } 27 | 28 | componentDidMount(){ 29 | const { post, offlineAction } = this.props; 30 | offlineAction.getPost(post.Id); 31 | } 32 | 33 | onRemovePress(){ 34 | const { post } = this.props; 35 | if (post && post.Id) { 36 | Alert.alert( 37 | '系统提示', 38 | '确定要清除该离线记录吗?', 39 | [ 40 | {text: '取消', onPress: () => null }, 41 | {text: '确定', onPress: () => this.handleRemove() }, 42 | ] 43 | ) 44 | } 45 | } 46 | 47 | handleRemove(){ 48 | const { post, offlineAction } = this.props; 49 | if (post && post.Id) { 50 | offlineAction.removePost(post.Id).then(()=>{ 51 | this.props.router.pop(); 52 | }); 53 | } 54 | } 55 | 56 | componentDidFocus() { 57 | this.setState({ 58 | hasFocus: true 59 | }); 60 | } 61 | 62 | renderPost() { 63 | const { postContent, router } = this.props; 64 | if (this.state.hasFocus === false) { 65 | return ( 66 | 67 | ) 68 | } 69 | if (postContent) { 70 | return ( 71 | 72 | 75 | 76 | 77 | ) 78 | } 79 | return( 80 | 81 | ); 82 | } 83 | 84 | render() { 85 | const { post, router } = this.props; 86 | return ( 87 | 88 | 89 | { this.renderPost() } 90 | 91 | 92 | this.onRemovePress() }/> 97 | 98 | this.props.router.pop() }/> 101 | 102 | ) 103 | } 104 | } 105 | 106 | const styles = StyleSheet.create({ 107 | bar_patch:{ 108 | height: StyleConfig.bottomBar_height - 15 109 | } 110 | }); 111 | 112 | export default connect((state, props) => ({ 113 | postContent: state.offline.postContent 114 | }), dispatch => ({ 115 | offlineAction : bindActionCreators(OfflineAction, dispatch) 116 | }), null, { 117 | withRef: true 118 | })(OfflinePostPage); -------------------------------------------------------------------------------- /source/view/web.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | WebView, 6 | StyleSheet 7 | } from 'react-native'; 8 | import Toast from '@remobile/react-native-toast'; 9 | import TimerMixin from 'react-timer-mixin'; 10 | import Spinner from '../component/spinner'; 11 | import Navbar from '../component/navbar'; 12 | import { StyleConfig, ComponentStyles, CommonStyles } from '../style'; 13 | 14 | const loadTimeout = 6000; 15 | 16 | class WebPage extends Component { 17 | 18 | constructor (props) { 19 | super(props); 20 | this.state = { 21 | loaded: false 22 | } 23 | } 24 | 25 | componentWillUnmount() { 26 | this.timer && TimerMixin.clearTimeout(this.timer); 27 | } 28 | 29 | onError(){ 30 | Toast.show("加载外部链接失败"); 31 | this.setWebViewLoaded(); 32 | } 33 | 34 | onLoadStart(){ 35 | this.timer = TimerMixin.setTimeout(() => { 36 | if(this.state.loaded === false){ 37 | Toast.show("页面响应不太给力"); 38 | } 39 | this.setWebViewLoaded(); 40 | TimerMixin.clearTimeout(this.timer); 41 | }, loadTimeout); 42 | } 43 | 44 | onLoadEnd(){ 45 | this.setWebViewLoaded(); 46 | } 47 | 48 | setWebViewLoaded(){ 49 | this.setState({ 50 | loaded: true 51 | }); 52 | } 53 | 54 | renderNavbar(){ 55 | const { title, router } = this.props; 56 | let titleText; 57 | if(title.length < 20){ 58 | titleText = title; 59 | }else{ 60 | titleText = title.substring(0, 25) + "..."; 61 | } 62 | 63 | return ( 64 | router.pop() }/> 67 | ) 68 | } 69 | 70 | renderLoading(){ 71 | if(this.state.loaded === false){ 72 | return ( 73 | 74 | ); 75 | } 76 | } 77 | 78 | renderWebView(){ 79 | const { url } = this.props; 80 | return ( 81 | this.onError() } 84 | onLoadEnd = { ()=>this.onLoadEnd() } 85 | onLoadStart = { ()=>this.onLoadStart() } 86 | /> 87 | ); 88 | } 89 | 90 | render() { 91 | return ( 92 | 93 | { this.renderNavbar() } 94 | { this.renderWebView() } 95 | { this.renderLoading() } 96 | 97 | ); 98 | } 99 | } 100 | 101 | export const styles = StyleSheet.create({ 102 | pending: { 103 | top: StyleConfig.navbar_height, 104 | height: StyleConfig.screen_height - (StyleConfig.navbar_height * 3), 105 | backgroundColor:'transparent' 106 | } 107 | }); 108 | 109 | export default WebPage; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true 4 | }, 5 | "exclude": [ 6 | "node_modules" 7 | ] 8 | } --------------------------------------------------------------------------------