├── .DS_Store ├── .gitignore ├── .vscode └── settings.json ├── .watchmanconfig ├── App.js ├── README.md ├── app.json ├── assets ├── NotoSansKR-Bold.otf ├── NotoSansKR-Regular.otf ├── NotoSerifKR-Bold.otf ├── NotoSerifKR-Regular.otf ├── Sweet_Sensations_Persona_Use.ttf ├── icon.png └── splash.png ├── babel.config.js ├── package.json └── src ├── Components ├── AuthLoadingScreen.js ├── CardComponent.js ├── MarkdownView.js ├── MarkdownView2.js └── SteemConnectModal.js ├── DUMMY.js ├── config.example.js ├── constants ├── Colors.js └── Layout.js ├── dsteem.js.bak ├── navagation ├── MainNavigation.js └── TabNavigation.js ├── postParser.js.bak ├── provider └── SteemProvider.js ├── reducers ├── index.js └── steemReducer.js ├── screens ├── AddMediaTab │ ├── AddMediaTabContainer.js │ ├── AddMediaTabPresenter.js │ └── index.js ├── DetailScreen.js ├── HomeTab │ ├── HomeTabContainer.js │ ├── HomeTabPresenter.js │ └── index.js ├── LikesTab │ ├── LikesTabContainer.js │ ├── LikesTabPresenter.js │ └── index.js ├── LoginScreen.js ├── ProfileTab │ ├── ProfileTab.1.js │ ├── ProfileTab.js │ ├── ProfileTabContainer.js │ ├── ProfileTabPresenter.js │ └── index.js └── SearchTab │ ├── SearchTabContainer.js │ ├── SearchTabPresenter.js │ └── index.js ├── steem.js └── steemConnect.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.lock 9 | *-lock.json 10 | .env 11 | config.js -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.minimap.enabled": false, 3 | "editor.tabSize": 2, 4 | "editor.autoIndent": true, 5 | "javascript.updateImportsOnFileMove.enabled": "never", 6 | "workbench.editor.highlightModifiedTabs": true, 7 | "workbench.editor.tabSizing": "shrink", 8 | "materialTheme.autoApplyIcons": true, 9 | "materialTheme.accent": "Acid Lime", 10 | "workbench.colorCustomizations": { 11 | "activityBarBadge.background": "#C6FF00", 12 | "list.activeSelectionForeground": "#C6FF00", 13 | "list.inactiveSelectionForeground": "#C6FF00", 14 | "list.highlightForeground": "#C6FF00", 15 | "scrollbarSlider.activeBackground": "#C6FF0050", 16 | "editorSuggestWidget.highlightForeground": "#C6FF00", 17 | "textLink.foreground": "#C6FF00", 18 | "progressBar.background": "#C6FF00", 19 | "pickerGroup.foreground": "#C6FF00", 20 | "tab.activeBorder": "#C6FF00", 21 | "notificationLink.foreground": "#C6FF00", 22 | "editorWidget.resizeBorder": "#C6FF00", 23 | "editorWidget.border": "#C6FF00", 24 | "settings.modifiedItemIndicator": "#C6FF00", 25 | "settings.headerForeground": "#C6FF00", 26 | "panelTitle.activeBorder": "#C6FF00", 27 | "breadcrumb.activeSelectionForeground": "#C6FF00", 28 | "menu.selectionForeground": "#C6FF00", 29 | "menubar.selectionForeground": "#C6FF00" 30 | }, 31 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { StatusBar } from 'react-native'; 3 | import { Root } from "native-base"; 4 | import { Font, AppLoading } from "expo"; 5 | import thunk from 'redux-thunk' 6 | import { Provider } from 'react-redux'; 7 | import { createStore, applyMiddleware } from 'redux'; 8 | import { createLogger } from 'redux-logger'; 9 | 10 | // import App from './src/App'; 11 | import MainNavigation from './src/navagation/MainNavigation'; 12 | import allReducers from './src/reducers'; 13 | 14 | const logger = createLogger(); 15 | const store = createStore(allReducers, applyMiddleware(logger, thunk)); 16 | 17 | // export default () => ; 18 | export default class RootApp extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | loaded: false 24 | }; 25 | } 26 | 27 | // async componentWillMount() { 28 | // console.log('Run App!!!'); 29 | 30 | // await this.loadAssets(); 31 | // this.setState({ loaded: true }); 32 | // } 33 | 34 | handleError = (error) => console.log(error); 35 | 36 | handleLoaded = () => this.setState({ loaded: true }); 37 | 38 | loadAssets = async() => { 39 | // Font Preloading 40 | await Font.loadAsync({ 41 | Roboto: require("native-base/Fonts/Roboto.ttf"), 42 | Roboto_medium: require("native-base/Fonts/Roboto_medium.ttf"), 43 | 'Sweet Sensations Persona Use': require('./assets/Sweet_Sensations_Persona_Use.ttf'), 44 | 'Noto Sans KR': require('./assets/NotoSansKR-Regular.otf'), 45 | 'Noto Sans KR Bold': require('./assets/NotoSansKR-Bold.otf'), 46 | 'Noto Serif KR': require('./assets/NotoSerifKR-Regular.otf'), 47 | 'Noto Serif KR Bold': require('./assets/NotoSerifKR-Bold.otf'), 48 | }); 49 | 50 | // Images Preloading 51 | // await Asset.loadAsync([ 52 | // require("./assets/icon.png") 53 | // ]) 54 | 55 | console.log('loadAssets complete!!!'); 56 | } 57 | 58 | render() { 59 | const { loaded } = this.state; 60 | return ( 61 | <> 62 | 65 | { 66 | (loaded) 67 | ? 68 | 69 | 70 | 71 | 72 | 73 | : 74 | 78 | } 79 | 80 | ); 81 | } 82 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 리액트 네이티브로 인스타그램 UI 클론 코딩하기 2 | 3 | 리액트 네이티브를 사용하여 인스타그램 UI를 구현한 코드입니다. 4 | 5 | 아래 유투브 동영상 강의를 보고 구현하였습니다. 6 | 7 | [![](https://img.youtube.com/vi/cgg1HidN4mQ/0.jpg)](https://www.youtube.com/watch?v=cgg1HidN4mQ) 8 | 9 | 10 | --- 11 | 12 | 강의 동영상을 보면서 정리한 블로그입니다. 13 | 14 | * [[React Native] 인스타그램 UI 만들기 #1](https://steemit.com/kr/@anpigon/react-native-ui-1) 15 | * [[React Native] 인스타그램 UI 만들기 #2](https://steemit.com/kr/@anpigon/react-native-ui-2-1548079722841) 16 | * [[React Native] 인스타그램 UI 만들기 #3](https://steemit.com/kr/@anpigon/react-native-ui-3-1548134597178) 17 | * [[React Native] 인스타그램 UI 만들기 #4](https://steemit.com/kr/@anpigon/react-native-ui-4-1548207724086) 18 | * [[React Native] 인스타그램 UI 만들기 #5](https://steemit.com/kr/@anpigon/react-native-ui-5-1548346515419) 19 | 20 | --- 21 | 22 | 실제 구현한 앱의 동작 화면입니다. 23 | 24 | ![](https://cdn.steemitimages.com/DQmNdRKUTLPzkJzEsddqkDF7tWhuZefHP6qS2PKpbL78W46/2019-01-25%2000-21-27.2019-01-25%2000_23_38.gif) 25 | 26 | Download APK: https://exp-shell-app-assets.s3.us-west-1.amazonaws.com/android/%40markan/insteemgram-9899aac90fc84566883eadf494772601-signed.apk 27 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "insteemgram", 4 | "slug": "insteemgram", 5 | "description": "This is a sample app for Instagram clone coding. Let's make Steamit SNS look like Instagram. See the source: https://github.com/anpigon/rn_instagram_clone", 6 | "privacy": "public", 7 | "sdkVersion": "32.0.0", 8 | "platforms": [ 9 | "ios", 10 | "android" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true, 28 | "bundleIdentifier": "com.anpigon.insteemgram" 29 | }, 30 | "androidStatusBar": { 31 | "barStyle": "dark-content", 32 | "backgroundColor": "#ffffff" 33 | }, 34 | "android": { 35 | "package": "com.anpigon.insteemgram" 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /assets/NotoSansKR-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/assets/NotoSansKR-Bold.otf -------------------------------------------------------------------------------- /assets/NotoSansKR-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/assets/NotoSansKR-Regular.otf -------------------------------------------------------------------------------- /assets/NotoSerifKR-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/assets/NotoSerifKR-Bold.otf -------------------------------------------------------------------------------- /assets/NotoSerifKR-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/assets/NotoSerifKR-Regular.otf -------------------------------------------------------------------------------- /assets/Sweet_Sensations_Persona_Use.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/assets/Sweet_Sensations_Persona_Use.ttf -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anpigon/rn_instagram_clone/563ad3b4ee4ed04167e7802a9b5fe48a5ab9e6ef/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "eject": "expo eject" 8 | }, 9 | "dependencies": { 10 | "@expo/vector-icons": "^9.0.0", 11 | "axios": "^0.18.0", 12 | "expo": "^32.0.0", 13 | "impagination": "^1.0.0-alpha.3", 14 | "native-base": "^2.10.0", 15 | "prop-types": "^15.7.2", 16 | "react": "16.5.0", 17 | "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz", 18 | "react-native-config": "^0.11.7", 19 | "react-native-markdown-renderer": "^3.2.8", 20 | "react-native-render-html": "^4.1.2", 21 | "react-navigation": "^3.0.9", 22 | "react-redux": "^6.0.0", 23 | "redux": "^4.0.1", 24 | "redux-actions": "^2.6.4", 25 | "redux-thunk": "^2.3.0", 26 | "remarkable": "^1.7.1", 27 | "remove-markdown": "^0.3.0", 28 | "steemconnect": "^2.0.1", 29 | "styled-components": "^4.1.3", 30 | "util": "^0.11.1", 31 | "whatwg-fetch": "2.0.4" 32 | }, 33 | "devDependencies": { 34 | "babel-preset-expo": "^5.0.0", 35 | "redux-devtools-extension": "^2.13.5", 36 | "redux-logger": "^3.0.6" 37 | }, 38 | "private": true 39 | } 40 | -------------------------------------------------------------------------------- /src/Components/AuthLoadingScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Constants, SecureStore } from 'expo'; 3 | import { AsyncStorage } from 'react-native'; 4 | import { Spinner, Container } from 'native-base'; 5 | import { connect } from 'react-redux'; 6 | import { TINT_COLOR } from '../constants/Colors'; 7 | import { setUsername } from '../reducers/steemReducer'; 8 | 9 | class AuthLoadingScreen extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | goMainScreen() { 15 | this.props.navigation.navigate('App'); 16 | } 17 | 18 | goLoginScreen() { 19 | this.props.navigation.navigate('Auth'); 20 | } 21 | 22 | async componentWillMount() { 23 | 24 | const userToken = await SecureStore.getItemAsync('userToken', { keychainService: Constants.deviceId }); 25 | // console.log('userToken:', userToken); 26 | 27 | if ( !userToken ) { 28 | return this.goLoginScreen(); 29 | } 30 | 31 | try { 32 | const { 33 | access_token, 34 | issued_at, 35 | expires_in, 36 | username 37 | } = JSON.parse(userToken); 38 | 39 | // 1. exp 날짜 체크 40 | if ( (issued_at + expires_in) <= (Date.now() / 1000) ) { 41 | // 만료일이 지났으면 토큰 삭제 42 | console.log('expired token!!!'); 43 | await SecureStore.deleteItemAsync('userToken', { keychainService: Constants.deviceId }); 44 | this.goLoginScreen(); 45 | } else { 46 | // 인증(로그인) 상태 47 | // console.log('authed'); 48 | this.props.setUsername({ username }); 49 | this.goMainScreen(); 50 | } 51 | 52 | } catch(error) { 53 | console.error(error); 54 | await SecureStore.deleteItemAsync('userToken', { keychainService: Constants.deviceId }); 55 | this.goLoginScreen(); 56 | } 57 | 58 | // This will switch to the App screen or Auth screen and this loading 59 | // screen will be unmounted and thrown away. 60 | // this.props.navigation.navigate(userToken ? 'App' : 'Auth'); 61 | }; 62 | 63 | // Render any loading content that you like here 64 | render() { 65 | return ( 66 | 67 | 68 | 69 | ); 70 | } 71 | } 72 | 73 | const mapStateToProps = () => ({}); 74 | const mapDispatchToProps = { 75 | setUsername 76 | }; 77 | export default connect( 78 | mapStateToProps, 79 | mapDispatchToProps 80 | )(AuthLoadingScreen); -------------------------------------------------------------------------------- /src/Components/CardComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View, Image, Text, StyleSheet, TouchableNativeFeedback, TouchableOpacity } from 'react-native'; 3 | 4 | import { Card, CardItem, Thumbnail, Body, Left, Right, Button, Icon, Title } from 'native-base'; 5 | 6 | export default class CardCompnent extends Component { 7 | render() { 8 | const { data, onPress } = this.props; 9 | const { image } = JSON.parse(data.json_metadata); 10 | return ( 11 | 12 | 13 | 14 | 15 | {/* 프로필 이미지 */} 16 | 18 | 19 | {data.author} 20 | {new Date(data.created).toDateString()} 21 | 22 | 23 | 24 | { 25 | image && image.length ? 26 | 27 | {/* 피드 대문 이미지 */} 28 | 31 | : null 32 | } 33 | {/* 34 | { data.active_votes.length } likes 35 | */} 36 | 37 | 38 | { data.title } 39 | 40 | {/* { data.body.replace(/\n/g,' ').slice(0, 200) } */} 41 | { data.summary } 42 | 43 | 44 | 45 | 46 | 47 | 51 | 55 | 58 | 59 | 60 | { data.pending_payout_value } 61 | 62 | 63 | 64 | 65 | ); 66 | } 67 | } 68 | 69 | const style = StyleSheet.create({ 70 | container: { 71 | flex: 1, 72 | alignItems: 'center', 73 | justifyContent: 'center', 74 | } 75 | }); -------------------------------------------------------------------------------- /src/Components/MarkdownView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Linking, Text, TouchableOpacity } from 'react-native'; 3 | import styled from "styled-components"; 4 | import Layout from '../constants/Layout'; 5 | import { Icon } from 'native-base'; 6 | 7 | // https://github.com/archriss/react-native-render-html 8 | import HTMLView from 'react-native-render-html'; 9 | 10 | import Remarkable from 'remarkable'; 11 | // import console = require('console'); 12 | const md = new Remarkable({ 13 | html: true, 14 | linkify: true, 15 | breaks: false 16 | }); 17 | 18 | const BASE_FONT_SIZE = 18; 19 | const BASE_FONT_FAMILY = 'Noto Serif KR'; 20 | 21 | const baseFontStyle = { 22 | fontFamily: BASE_FONT_FAMILY, 23 | fontSize: BASE_FONT_SIZE 24 | } 25 | 26 | // Tag Styles 27 | const tagsStyles = { 28 | hr: { marginVertical: BASE_FONT_SIZE / 2, height: 1, backgroundColor: '#e9e7e7' }, 29 | a: { textDecorationLine: 'underline', color: '#2088ff' }, 30 | b: { fontWeight: 'bold', fontFamily: `${BASE_FONT_FAMILY} Bold` }, 31 | strong: { fontWeight: 'bold', fontFamily: `${BASE_FONT_FAMILY} Bold` }, 32 | h1: _generateHeadingStyle(BASE_FONT_SIZE, 2, 0.67), 33 | h2: _generateHeadingStyle(BASE_FONT_SIZE, 1.5, 0.83), 34 | h3: _generateHeadingStyle(BASE_FONT_SIZE, 1.17, 1), 35 | h4: _generateHeadingStyle(BASE_FONT_SIZE, 1, 1.33), 36 | h5: _generateHeadingStyle(BASE_FONT_SIZE, 1, 1, 0.2), 37 | h6: _generateHeadingStyle(BASE_FONT_SIZE, 1, 1, 0.2), 38 | }; 39 | function _generateHeadingStyle (baseFontSize, fontMultiplier, marginMultiplier, marginBottomMultiplier) { 40 | return { 41 | fontSize: baseFontSize * fontMultiplier, 42 | marginTop: baseFontSize * marginMultiplier, 43 | marginBottom: baseFontSize * (marginBottomMultiplier || marginMultiplier), 44 | fontWeight: 'bold', 45 | fontFamily: `${BASE_FONT_FAMILY} Bold`, 46 | }; 47 | } 48 | 49 | // Tag Renders 50 | const renderers = { 51 | a: (htmlAttribs, children, convertedCSSStyles, passProps) => { 52 | const style = [ 53 | passProps.tagsStyles ? passProps.tagsStyles['a'] : undefined, 54 | htmlAttribs.style ? htmlAttribs.style : undefined, 55 | ]; 56 | const { parentWrapper, onLinkPress, key, data, parentTag } = passProps; 57 | const onPress = (evt) => onLinkPress && htmlAttribs && htmlAttribs.href ? 58 | onLinkPress(evt, htmlAttribs.href, htmlAttribs) : 59 | undefined; 60 | if (parentWrapper === 'Text') { 61 | return ( 62 | 63 | { children || data } 64 | 72 | 73 | ); 74 | } else { 75 | return ( 76 | 77 | { children || data } 78 | 79 | ); 80 | } 81 | } 82 | } 83 | 84 | // [Event] onLinkPress 85 | const onLinkPress = (evt, href) => { Linking.openURL(href); } 86 | 87 | // MarkdownView Component 88 | const MarkdownView = ({ children }) => { 89 | 90 | let htmlContent = md.render(children); 91 | 92 | return ( 93 | 102 | ); 103 | 104 | } 105 | 106 | export default MarkdownView; -------------------------------------------------------------------------------- /src/Components/MarkdownView2.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Text } from 'react-native'; 3 | import { Icon } from 'native-base'; 4 | import styled from 'styled-components'; 5 | import Markdown, { 6 | getUniqueID, 7 | MarkdownIt, 8 | // renderRules, 9 | PluginContainer 10 | } from 'react-native-markdown-renderer'; 11 | 12 | // https://github.com/markdown-it/markdown-it 13 | const md = new MarkdownIt({ 14 | typographer: true, 15 | linkify: true, 16 | // html: true, 17 | // xhtmlOut: true, 18 | }); 19 | 20 | md.linkify.tlds('.py', false); // disables .py as top level domain 21 | // Reload with full tlds list 22 | md.linkify.tlds('onion', true) // Add unofficial `.onion` domain 23 | md.linkify.add('git:', 'http:') // Add `git:` protocol as "alias" 24 | md.linkify.add('ftp:', null) // Disable `ftp:` ptotocol 25 | md.linkify.set({ fuzzyIP: true }); // Enable IPs in fuzzy links (without schema) 26 | md.linkify.add('@', { 27 | validate: function (text, pos, self) { 28 | var tail = text.slice(pos); 29 | 30 | if (!self.re.twitter) { 31 | self.re.twitter = new RegExp( 32 | '^([a-zA-Z0-9_]){1,15}(?!_)(?=$|' + self.re.src_ZPCc + ')' 33 | ); 34 | } 35 | if (self.re.twitter.test(tail)) { 36 | // Linkifier allows punctuation chars before prefix, 37 | // but we additionally disable `@` ("@@mention" is invalid) 38 | if (pos >= 2 && tail[pos - 2] === '@') { 39 | return false; 40 | } 41 | return tail.match(self.re.twitter)[0].length; 42 | } 43 | return 0; 44 | }, 45 | normalize: function (match) { 46 | match.url = 'https://steemit.com/' + match.url.replace(/^@/, ''); 47 | } 48 | }); 49 | 50 | // renderRules['html_block'] = (node, children, parent, styles) => { 51 | // console.log('html_block', { 52 | // node, children, parent, styles 53 | // }) 54 | // return ( 55 | // 56 | // {node.content} 57 | // 58 | // ); 59 | // }; 60 | // renderRules['html_inline'] = (node, children, parent, styles) => { 61 | // return {children}; 62 | // }; 63 | 64 | const renderRules = { 65 | link: (node, children, parent, styles) => { 66 | return ( 67 | openUrl(node.attributes.href)}> 68 | {children} 69 | 70 | ); 71 | }, 72 | } 73 | 74 | const plugin = new PluginContainer( 75 | (md, name, options) => { 76 | console.log('plugin', {md, name, options}) 77 | const parse = state => { 78 | console.log('parse', state) 79 | const Token = state.Token; 80 | 81 | for (let i = 0; i < state.tokens.length; i++) { 82 | const block = state.tokens[i]; 83 | if (block.type !== 'inline') { 84 | continue; 85 | } 86 | 87 | for (let j = 0; j < block.children.length; j++) { 88 | const token = block.children[j]; 89 | if (token.type !== 'text') { 90 | continue; 91 | } 92 | // if (token.content === name) { 93 | // const newToken = new Token(name, '', token.nesting); 94 | // newToken.content = token.content; 95 | // block.children = md.utils.arrayReplaceAt(block.children, j, [newToken]); 96 | // } 97 | // if (/<\/?center>/.test(token.content)) { 98 | // const newToken = new Token('text', '', token.nesting); 99 | // // newToken.content = token.content; 100 | // block.children = md.utils.arrayReplaceAt(block.children, j, [newToken]); 101 | // } 102 | // if (//.test(token.content)) { 103 | // const newToken = new Token('text', '', token.nesting); 104 | // // newToken.content = token.content; 105 | // block.children = md.utils.arrayReplaceAt(block.children, j, [newToken]); 106 | // } 107 | // 108 | } 109 | } 110 | }; 111 | 112 | md.core.ruler.after('inline', name, parse); 113 | }, 114 | 'plugin', 115 | {} 116 | ); 117 | 118 | const parseHtml = (html) => { 119 | return html 120 | .replace(/<\/?center>/gi, '') 121 | .replace(//gi, '\n') 122 | ; 123 | } 124 | 125 | export default ({ children }) => ( 126 | {parseHtml(children)} 132 | ); 133 | 134 | // https://github.com/mientjan/react-native-markdown-renderer/blob/master/src/lib/styles.js 135 | const styles = StyleSheet.create({ 136 | heading: { 137 | borderBottomWidth: 1, 138 | borderColor: '#000000', 139 | }, 140 | heading1: { 141 | fontSize: 32, 142 | backgroundColor: '#000000', 143 | color: '#FFFFFF', 144 | }, 145 | heading2: { 146 | fontSize: 24, 147 | }, 148 | heading3: { 149 | fontSize: 18, 150 | }, 151 | heading4: { 152 | fontSize: 16, 153 | }, 154 | heading5: { 155 | fontSize: 13, 156 | }, 157 | heading6: { 158 | fontSize: 11, 159 | }, 160 | link: { 161 | color: '#2088ff', 162 | textDecorationLine: 'underline', 163 | }, 164 | text: { 165 | fontFamily: 'Noto Serif KR', 166 | fontSize: 18, 167 | } 168 | }); 169 | -------------------------------------------------------------------------------- /src/Components/SteemConnectModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { WebView, Platform } from 'react-native'; 3 | import { Icon, Container, Button, Header, Right } from 'native-base'; 4 | // import Config from 'react-native-config'; 5 | 6 | import steemConnect from '../steemConnect'; 7 | 8 | class SteemConnectModal extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | 13 | // console.log(Config); 14 | } 15 | 16 | // webview 상태 변경 체크 17 | _onNavigationStateChange = (event) => { 18 | if (event.url.indexOf('?access_token') > -1) { 19 | this.webview.stopLoading(); 20 | try { 21 | const tokens = {}; 22 | 23 | // 콜백 URL에서 accessToken 정보 추출하기 24 | let params = event.url.split('?')[1]; 25 | params = params.split('&'); 26 | params.forEach(e => { 27 | const [key, val] = e.split('='); 28 | tokens[key] = val; 29 | }); 30 | 31 | // console.log('tokens:', tokens); 32 | this.props.onSteemconnectSuccess(tokens); 33 | } catch (error) { 34 | console.log(error); 35 | } 36 | } 37 | }; 38 | 39 | render() { 40 | // 로그인 URL 생성 41 | const link = steemConnect.getLoginURL(); 42 | console.log('link:', link); 43 | 44 | return ( 45 | 46 |
47 | {/*
*/} 52 | 53 | 68 | 69 |
70 | { this.webview = ref }} 74 | /> 75 |
76 | ); 77 | } 78 | } 79 | 80 | export default SteemConnectModal; 81 | -------------------------------------------------------------------------------- /src/DUMMY.js: -------------------------------------------------------------------------------- 1 | export const dummy = { 2 | 1: { 3 | title: '사기카드 Gold Dragon 으로 JJM 토큰 삽니다.', 4 | body:"![K-016.png](https://s3.ap-northeast-2.amazonaws.com/dclick/image/bji1203/1552963828394)\n\n사기카드 Gold Dragon 3장 팝니다 ㅋㅋ\n2장은 올려놨는데 긴시간동안 안팔리네요.\n얼마에 올려놨는지도 까먹음ㅋㅋㅋ\n\n![K-017.png](https://s3.ap-northeast-2.amazonaws.com/dclick/image/bji1203/1552963898576)\n\n1렙(1장)은 좀 애매하지만 \n2렙(3장)부터는 자력 힐 능력이 있어서 정말 좋은 카드죠.\n3렙이 되면 Blast 스킬을 배우기때문에 정말 짱짱맨입니다.\n\n이러한 능력을 가진 골드 드래곤으로\nJJM 토큰 삽니다 ㅋㅋ\n가격 제시 부탁! \n\n근데 그 스팀 토큰들 \n보유하거나 거래하려면 어떻게 해야하나요..? ㅋㅋㅋ\n귀차니즘때문에 관심을 못가졌네요 ㅠㅠ\n\n\n\n---\n\n##### **Sponsored ( Powered by [dclick](https://www.dclick.io) )** \n##### [헌트 토큰 IEO 및 상장계획이 드디어 확정되었습니다!](https://api.dclick.io/v1/c?x=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjIjoiYmppMTIwMyIsInMiOiItZ29sZC1kcmFnb24tamptLS0xNTUyOTY0MDY0MjQ2IiwiYSI6WyJ0LTE1MDAiXSwidXJsIjoiaHR0cHM6Ly9zdGVlbWl0LmNvbS9rci9AcHJvamVjdDcvaWVvLTEwIiwiaWF0IjoxNTUyOTY0MDY0LCJleHAiOjE4NjgzMjQwNjR9.ZLbyydvnS2NK7E83vrTa1-OyENAsIDmnrdTpVU5RWIk)\n사전 등록 스팀 유저 10% 추가 보너스 이벤트!!\n", 5 | }, 6 | 2: { 7 | title: '스팀잇에 불어오는 새바람', 8 | body:`
![](https://cdn.steemitimages.com/DQmbzvm8un6rauqPh1TCk9nvdLks441KxCuLsnra5Qj2e1s/image.png)
9 | 10 | 안녕하세요 @ayogom 입니다.

11 | 암호화폐 시장의 오랜 기간 부진함으로 스팀잇에도 긴 겨울이 지속되고 있습니다. 한시간내에 많은 글들이 올라와서 정말 올라오는 글에 쫓기던 시절이 있었는데, 언제부턴가 스팀잇이 조용합니다. 한때는 1글 1닭이라는 문구와 함께 많은 보상과 더불어 스팀잇만으로도 생활이 가능하지 않을까 라는 행복 회로가 있었는데, 지금은 일주일 내내 글을 써도 치킨한마리 먹기 힘들어 보입니다. 아마도 치킨값이 많이 올라서 그러지 않을까? 하고 스스로를 위로해보지만 결과적으로 보상이 많이 줄어듬에 따라 포스팅수도 사용자들도 줄어들어 버렸다는 것은 사실입니다. 12 | 13 | 하지만 개인적으로 보았을 떄, 아직 스팀잇은 무궁무진 합니다. 14 | 먼저 많은 디앱들이 런칭을 했습니다. 15 | - IEO를 성공적으로 마무리 했던 제품 소개 플렛폼 스팀헌트 (https://steemhunt.com/) 16 | - 블록체인 기반의 카드 게임 스팀 몬스터 (https://steemmonsters.com) 17 | - 블록체인 기반의 PVP 게임의 드러그워 (https://drugwars.io) 18 | - 밥 먹고 맛집을 소개하는 테이스팀 (https://kr.tasteem.io/) 19 | - 여행을 다녀와서 여행지를 소개하는 트립스팀 (https://kr.tripsteem.com/) 20 | - 예술 창작자를 지원해주는 엔토파즈 (https://www.ntopaz.com/) 21 | - 광고 사업을 시도 하는 디클릭 (https://dclick.io) 22 | - 수막은 타짜를 만들어 낸 매직다이스 (https://magic-dice.com/) 23 | 24 | 등 오로지 도박 디앱이 주를 이루는 타 블록체인과 달리 유저들과 함께 소통 할 수 있는 완성도 높은 디앱들이 많이 런칭된 스팀입니다. 암호화폐의 회복과 함께 이러한 디앱들이 날개를 달아서 스팀이 저 높은 곳까지 갈 수 있지 않을까요? 이 뿐만 아니라, 최근 DDoS 공격에서도 중앙화된 홈페이지들이 꼼짝 못 하는 모습을 보았습니다. 25 | 26 | **인스타그램이 접속이 안되면 어떻게 하죠? 페이스북이 접속이 안되면 어떻게 하죠?** 27 | 28 | 스팀잇이 접속이 안되면 어떻게 하죠? 맞습니다. 우리는 Steemit 이 접속이 안되어도, busy / steemkr / steempeak 을 통해 활동할 수 있습니다. 증인 노드가 공격을 받으면요? 그 아래 증인 노드가 살아 있어서 괜찮습니다. 우리의 스팀 체인이 단 하나의 증인노드라도 정상적으로 돌아간다면, 우리는 이 스팀활동을 할 수 있습니다. 대단하지 않나요? 분산화 된 시스템의 장점이라고 생각 합니다. 29 | 30 | 최근 @cyberrn 교수님을 통해 많은 학생들이 스팀잇에 가입을 하게 되었습니다. 31 | 예전에 저도 스팀잇 활성화를 위해 대학교에서 강의를 하였으나, 결과가 좋지 않아 아쉬움이 남았던 부분인데, 그 부분을 직접 해주셔서 너무나 감사하고 기대가 됩니다. 32 | 33 | 위에 언급할 것 처럼 좋은 시스템이 있어도, 그것을 사용할 사람이 없다면 다 무용지물이죠 34 | 결국 이용자가 많아져야 우리의 스팀잇이 더욱 탄탄하게 성장 할 수 있는 기초가 될 수 있을 것 입니다. 35 | 36 | 그래서 개인적으로 스팀잇 단체 카카오톡 방을 만들었습니다. 37 | 목적은 스팀잇에 새로오신 분들을 적응 시키기 위한 Q&A 방입니다. 38 | 스팀잇을 하시다가 궁금한게 있다면 자유롭게 들어오셔서 질문 하시면 됩니다. 39 | 물론, 저와함께 답변을 해주실 수 있는 분들도 환영 입니다 ^^ 40 | 41 | https://open.kakao.com/o/gHhqKsib 42 | 43 | 스팀잇에 불어온 작은 바람의 나비효과를 기대해 봅니다`} 44 | }; -------------------------------------------------------------------------------- /src/config.example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SC2_APP_ID: '', 3 | SC2_CALLBACK_URL: '', 4 | SC2_GET_TOKEN_URL: '' 5 | } -------------------------------------------------------------------------------- /src/constants/Colors.js: -------------------------------------------------------------------------------- 1 | export const TINT_COLOR = TEAL; 2 | 3 | // colors 4 | // https://graf1x.com/shades-of-blue-color-palette/ 5 | export const SKY = '#95C8D8'; 6 | export const BABY_BLUE = '#89CFF0'; 7 | export const TURQUOISE = '#3FE0D0'; 8 | export const CAROLINA = '#57A0D3'; 9 | export const TEAL = '#008b81'; -------------------------------------------------------------------------------- /src/constants/Layout.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from "react-native"; 2 | 3 | const { width, height } = Dimensions.get("screen"); 4 | 5 | export default { 6 | width, 7 | height 8 | }; -------------------------------------------------------------------------------- /src/dsteem.js.bak: -------------------------------------------------------------------------------- 1 | import { Client, PrivateKey } from 'dsteem'; 2 | import steemConnect from 'steemconnect'; 3 | import Config from 'react-native-config'; 4 | 5 | import { getServer } from '../../realm/realm'; 6 | import { getUnreadActivityCount } from '../esteem/esteem'; 7 | import { userActivity } from '../esteem/ePoint'; 8 | // Utils 9 | import { decryptKey } from '../../utils/crypto'; 10 | import { parsePosts, parsePost, parseComments } from '../../utils/postParser'; 11 | import { getName, getAvatar } from '../../utils/user'; 12 | import { getReputation } from '../../utils/reputation'; 13 | import parseToken from '../../utils/parseToken'; 14 | import filterNsfwPost from '../../utils/filterNsfwPost'; 15 | 16 | // Constant 17 | import AUTH_TYPE from '../../constants/authType'; 18 | 19 | const DEFAULT_SERVER = 'https://api.steemit.com'; 20 | let client = new Client(DEFAULT_SERVER); 21 | 22 | export const checkClient = async () => { 23 | let selectedServer = DEFAULT_SERVER; 24 | 25 | await getServer().then((response) => { 26 | if (response) { 27 | selectedServer = response; 28 | } 29 | }); 30 | 31 | client = new Client(selectedServer); 32 | }; 33 | 34 | checkClient(); 35 | 36 | export const getDigitPinCode = pin => decryptKey(pin, Config.PIN_KEY); 37 | 38 | export const getDynamicGlobalProperties = () => client.database.getDynamicGlobalProperties(); 39 | 40 | export const getRewardFund = () => client.database.call('get_reward_fund', ['post']); 41 | 42 | export const getFeedHistory = async () => { 43 | try { 44 | const feedHistory = await client.database.call('get_feed_history'); 45 | return feedHistory; 46 | } catch (error) { 47 | return error; 48 | } 49 | }; 50 | 51 | export const fetchGlobalProps = async () => { 52 | let globalDynamic; 53 | let feedHistory; 54 | let rewardFund; 55 | 56 | try { 57 | globalDynamic = await getDynamicGlobalProperties(); 58 | feedHistory = await getFeedHistory(); 59 | rewardFund = await getRewardFund(); 60 | } catch (e) { 61 | return; 62 | } 63 | 64 | const steemPerMVests = (parseToken(globalDynamic.total_vesting_fund_steem) 65 | / parseToken(globalDynamic.total_vesting_shares)) 66 | * 1e6; 67 | const base = parseToken(feedHistory.current_median_history.base); 68 | const quote = parseToken(feedHistory.current_median_history.quote); 69 | const fundRecentClaims = rewardFund.recent_claims; 70 | const fundRewardBalance = parseToken(rewardFund.reward_balance); 71 | const globalProps = { 72 | steemPerMVests, 73 | base, 74 | quote, 75 | fundRecentClaims, 76 | fundRewardBalance, 77 | }; 78 | 79 | return globalProps; 80 | }; 81 | 82 | /** 83 | * @method getAccount get account data 84 | * @param user username 85 | */ 86 | export const getAccount = user => new Promise((resolve, reject) => { 87 | try { 88 | const account = client.database.getAccounts([user]); 89 | resolve(account); 90 | } catch (error) { 91 | reject(error); 92 | } 93 | }); 94 | 95 | /** 96 | * @method getAccount get account data 97 | * @param user username 98 | */ 99 | export const getState = async (path) => { 100 | try { 101 | const state = await client.database.getState(path); 102 | return state; 103 | } catch (error) { 104 | return error; 105 | } 106 | }; 107 | 108 | /** 109 | * @method getUser get account data 110 | * @param user username 111 | */ 112 | export const getUser = async (user) => { 113 | try { 114 | const account = await client.database.getAccounts([user]); 115 | 116 | if (account && account.length < 1) return null; 117 | 118 | // get global properties to calculate Steem Power 119 | const globalProperties = await client.database.getDynamicGlobalProperties(); 120 | const rcPower = await client.call('rc_api', 'find_rc_accounts', { accounts: [user] }); 121 | const unreadActivityCount = await getUnreadActivityCount({ user }); 122 | 123 | account[0].reputation = getReputation(account[0].reputation); 124 | account[0].username = account[0].name; 125 | account[0].unread_activity_count = unreadActivityCount; 126 | account[0].rc_manabar = rcPower.rc_accounts[0].rc_manabar; 127 | account[0].steem_power = await vestToSteem( 128 | account[0].vesting_shares, 129 | globalProperties.total_vesting_shares, 130 | globalProperties.total_vesting_fund_steem, 131 | ); 132 | account[0].received_steem_power = await vestToSteem( 133 | account[0].received_vesting_shares, 134 | globalProperties.total_vesting_shares, 135 | globalProperties.total_vesting_fund_steem, 136 | ); 137 | account[0].delegated_steem_power = await vestToSteem( 138 | account[0].delegated_vesting_shares, 139 | globalProperties.total_vesting_shares, 140 | globalProperties.total_vesting_fund_steem, 141 | ); 142 | 143 | account[0].about = account[0].json_metadata && JSON.parse(account[0].json_metadata); 144 | account[0].avatar = getAvatar(account[0].about); 145 | account[0].display_name = getName(account[0].about); 146 | 147 | return account[0]; 148 | } catch (error) { 149 | return Promise.reject(error); 150 | } 151 | }; 152 | 153 | // TODO: Move to utils folder 154 | export const vestToSteem = async (vestingShares, totalVestingShares, totalVestingFundSteem) => ( 155 | parseFloat(totalVestingFundSteem) 156 | * (parseFloat(vestingShares) / parseFloat(totalVestingShares)) 157 | ).toFixed(0); 158 | 159 | export const getFollows = username => client.database.call('get_follow_count', [username]); 160 | 161 | export const getFollowing = (follower, startFollowing, followType = 'blog', limit = 100) => client.database.call('get_following', [follower, startFollowing, followType, limit]); 162 | 163 | export const getFollowers = (follower, startFollowing, followType = 'blog', limit = 100) => client.database.call('get_followers', [follower, startFollowing, followType, limit]); 164 | 165 | export const getIsFollowing = (user, author) => new Promise((resolve, reject) => { 166 | client.database 167 | .call('get_following', [author, user, 'blog', 1]) 168 | .then((result) => { 169 | if (result[0] && result[0].follower === author && result[0].following === user) { 170 | resolve(true); 171 | } else { 172 | resolve(false); 173 | } 174 | }) 175 | .catch((err) => { 176 | reject(err); 177 | }); 178 | }); 179 | 180 | export const getFollowSearch = (user, targetUser) => new Promise((resolve, reject) => { 181 | client.database 182 | .call('get_following', [targetUser, user, 'blog', 1]) 183 | .then((result) => { 184 | if (result[0] && result[0].follower === targetUser && result[0].following === user) { 185 | resolve(result[0].follower); 186 | } else { 187 | resolve(null); 188 | } 189 | }) 190 | .catch((err) => { 191 | reject(err); 192 | }); 193 | }); 194 | 195 | export const getIsMuted = async (username, targetUsername) => { 196 | let resp; 197 | 198 | try { 199 | resp = await getFollowing(username, targetUsername, 'ignore', 1); 200 | } catch (err) { 201 | return false; 202 | } 203 | 204 | if (resp && resp.length > 0) { 205 | if (resp[0].follower === username && resp[0].following === targetUsername) { 206 | return true; 207 | } 208 | } 209 | 210 | return false; 211 | }; 212 | 213 | export const ignoreUser = async (currentAccount, pin, data) => { 214 | const digitPinCode = getDigitPinCode(pin); 215 | const key = getAnyPrivateKey(currentAccount.local, digitPinCode); 216 | 217 | if (currentAccount.local.authType === AUTH_TYPE.STEEM_CONNECT) { 218 | const token = decryptKey(currentAccount.local.accessToken, digitPinCode); 219 | const api = steemConnect.Initialize({ 220 | accessToken: token, 221 | }); 222 | 223 | return api.ignore(data.follower, data.following); 224 | } 225 | 226 | if (key) { 227 | const privateKey = PrivateKey.fromString(key); 228 | 229 | const json = { 230 | id: 'follow', 231 | json: JSON.stringify([ 232 | 'follow', 233 | { 234 | follower: `${data.follower}`, 235 | following: `${data.following}`, 236 | what: ['ignore'], 237 | }, 238 | ]), 239 | required_auths: [], 240 | required_posting_auths: [`${data.follower}`], 241 | }; 242 | 243 | return new Promise((resolve, reject) => { 244 | client.broadcast 245 | .json(json, privateKey) 246 | .then((result) => { 247 | resolve(result); 248 | }) 249 | .catch((err) => { 250 | reject(err); 251 | }); 252 | }); 253 | } 254 | 255 | return Promise.reject(new Error('You dont have permission!')); 256 | }; 257 | 258 | /** 259 | * @method getPosts get posts method 260 | * @param by get discussions by trending, created, active etc. 261 | * @param query tag, limit, start_author?, start_permalink? 262 | */ 263 | export const getPosts = async (by, query, user) => { 264 | try { 265 | let posts = await client.database.getDiscussions('feed', query); 266 | 267 | if (posts) { 268 | posts = await parsePosts(posts, user); 269 | } 270 | return posts; 271 | } catch (error) { 272 | return error; 273 | } 274 | }; 275 | 276 | export const getActiveVotes = (author, permlink) => client.database.call('get_active_votes', [author, permlink]); 277 | 278 | export const getPostsSummary = async (by, query, currentUserName, filterNsfw) => { 279 | try { 280 | let posts = await client.database.getDiscussions(by, query); 281 | 282 | if (posts) { 283 | posts = await parsePosts(posts, currentUserName, true); 284 | 285 | if (filterNsfw !== '0') { 286 | const updatedPosts = filterNsfwPost(posts, filterNsfw); 287 | return updatedPosts; 288 | } 289 | } 290 | return posts; 291 | } catch (error) { 292 | return error; 293 | } 294 | }; 295 | 296 | export const getUserComments = async (query) => { 297 | try { 298 | let comments = await client.database.getDiscussions('comments', query); 299 | comments = parseComments(comments); 300 | return comments; 301 | } catch (error) { 302 | return error; 303 | } 304 | }; 305 | 306 | export const getRepliesByLastUpdate = async (query) => { 307 | try { 308 | let replies = await client.database.call('get_replies_by_last_update', [ 309 | query.start_author, 310 | query.start_permlink, 311 | query.limit, 312 | ]); 313 | replies = parseComments(replies); 314 | return replies; 315 | } catch (error) { 316 | return error; 317 | } 318 | }; 319 | 320 | /** 321 | * @method getUser get user data 322 | * @param user post author 323 | * @param permlink post permlink 324 | * @param currentUserName active accounts username 325 | */ 326 | export const getPost = async (author, permlink, currentUserName = null) => { 327 | try { 328 | const post = await client.database.call('get_content', [author, permlink]); 329 | 330 | return post ? await parsePost(post, currentUserName) : null; 331 | } catch (error) { 332 | return error; 333 | } 334 | }; 335 | 336 | export const getPurePost = async (author, permlink) => { 337 | try { 338 | return await client.database.call('get_content', [author, permlink]); 339 | } catch (error) { 340 | return error; 341 | } 342 | }; 343 | 344 | /** 345 | * @method getUser get user data 346 | * @param user post author 347 | * @param permlink post permlink 348 | */ 349 | export const getComments = (user, permlink) => { 350 | let comments; 351 | return new Promise((resolve, reject) => { 352 | client.database 353 | .call('get_content_replies', [user, permlink]) 354 | .then((result) => { 355 | comments = parseComments(result); 356 | }) 357 | .then(() => { 358 | resolve(comments); 359 | }) 360 | .catch((error) => { 361 | reject(error); 362 | }); 363 | }); 364 | }; 365 | 366 | /** 367 | * @method getPostWithComments get user data 368 | * @param user post author 369 | * @param permlink post permlink 370 | */ 371 | export const getPostWithComments = async (user, permlink) => { 372 | let post; 373 | let comments; 374 | 375 | await getPost(user, permlink).then((result) => { 376 | post = result; 377 | }); 378 | await getComments(user, permlink).then((result) => { 379 | comments = result; 380 | }); 381 | 382 | return [post, comments]; 383 | }; 384 | 385 | /** 386 | * @method upvote upvote a content 387 | * @param vote vote object(author, permlink, voter, weight) 388 | * @param postingKey private posting key 389 | */ 390 | 391 | export const vote = (account, pin, author, permlink, weight) => _vote( 392 | account, pin, author, permlink, weight, 393 | ) 394 | .then((resp) => { 395 | userActivity(account.username, 120, resp.block_num, resp.id); 396 | return resp; 397 | }); 398 | 399 | const _vote = async (currentAccount, pin, author, permlink, weight) => { 400 | const digitPinCode = getDigitPinCode(pin); 401 | const key = getAnyPrivateKey(currentAccount.local, digitPinCode); 402 | 403 | if (currentAccount.local.authType === AUTH_TYPE.STEEM_CONNECT) { 404 | const token = decryptKey(currentAccount.local.accessToken, digitPinCode); 405 | const api = steemConnect.Initialize({ 406 | accessToken: token, 407 | }); 408 | 409 | const voter = currentAccount.name; 410 | 411 | return new Promise((resolve, reject) => { 412 | api 413 | .vote(voter, author, permlink, weight) 414 | .then((result) => { 415 | resolve(result); 416 | }) 417 | .catch((err) => { 418 | reject(err); 419 | }); 420 | }); 421 | } 422 | 423 | if (key) { 424 | const privateKey = PrivateKey.fromString(key); 425 | const voter = currentAccount.name; 426 | const args = { 427 | voter, 428 | author, 429 | permlink, 430 | weight, 431 | }; 432 | 433 | return new Promise((resolve, reject) => { 434 | client.broadcast 435 | .vote(args, privateKey) 436 | .then((result) => { 437 | resolve(result); 438 | }) 439 | .catch((err) => { 440 | reject(err); 441 | }); 442 | }); 443 | } 444 | 445 | return Promise.reject(new Error('You dont have permission!')); 446 | }; 447 | 448 | /** 449 | * @method upvoteAmount estimate upvote amount 450 | */ 451 | export const upvoteAmount = async (input) => { 452 | let medianPrice; 453 | const rewardFund = await getRewardFund(); 454 | 455 | await client.database.getCurrentMedianHistoryPrice().then((res) => { 456 | medianPrice = res; 457 | }); 458 | 459 | const estimated = (input / parseFloat(rewardFund.recent_claims)) 460 | * parseFloat(rewardFund.reward_balance) 461 | * (parseFloat(medianPrice.base) / parseFloat(medianPrice.quote)); 462 | return estimated; 463 | }; 464 | 465 | export const transferToken = (data, activeKey) => { 466 | const key = PrivateKey.fromString(activeKey); 467 | return new Promise((resolve, reject) => { 468 | client.broadcast 469 | .transfer(data, key) 470 | .then((result) => { 471 | resolve(result); 472 | }) 473 | .catch((err) => { 474 | reject(err); 475 | }); 476 | }); 477 | }; 478 | 479 | export const followUser = async (currentAccount, pin, data) => { 480 | const digitPinCode = getDigitPinCode(pin); 481 | const key = getAnyPrivateKey(currentAccount.local, digitPinCode); 482 | 483 | if (currentAccount.local.authType === AUTH_TYPE.STEEM_CONNECT) { 484 | const token = decryptKey(currentAccount.local.accessToken, digitPinCode); 485 | const api = steemConnect.Initialize({ 486 | accessToken: token, 487 | }); 488 | 489 | return api.follow(data.follower, data.following); 490 | } 491 | 492 | if (key) { 493 | const privateKey = PrivateKey.fromString(key); 494 | const json = { 495 | id: 'follow', 496 | json: JSON.stringify([ 497 | 'follow', 498 | { 499 | follower: `${data.follower}`, 500 | following: `${data.following}`, 501 | what: ['blog'], 502 | }, 503 | ]), 504 | required_auths: [], 505 | required_posting_auths: [`${data.follower}`], 506 | }; 507 | 508 | return new Promise((resolve, reject) => { 509 | client.broadcast 510 | .json(json, privateKey) 511 | .then((result) => { 512 | resolve(result); 513 | }) 514 | .catch((err) => { 515 | reject(err); 516 | }); 517 | }); 518 | } 519 | 520 | return Promise.reject(new Error('You dont have permission!')); 521 | }; 522 | 523 | export const unfollowUser = async (currentAccount, pin, data) => { 524 | const digitPinCode = getDigitPinCode(pin); 525 | const key = getAnyPrivateKey(currentAccount.local, digitPinCode); 526 | 527 | if (currentAccount.local.authType === AUTH_TYPE.STEEM_CONNECT) { 528 | const token = decryptKey(currentAccount.local.accessToken, digitPinCode); 529 | const api = steemConnect.Initialize({ 530 | accessToken: token, 531 | }); 532 | 533 | return api.unfollow(data.follower, data.following); 534 | } 535 | 536 | if (key) { 537 | const privateKey = PrivateKey.fromString(key); 538 | 539 | const json = { 540 | id: 'follow', 541 | json: JSON.stringify([ 542 | 'follow', 543 | { 544 | follower: `${data.follower}`, 545 | following: `${data.following}`, 546 | what: [''], 547 | }, 548 | ]), 549 | required_auths: [], 550 | required_posting_auths: [`${data.follower}`], 551 | }; 552 | 553 | return new Promise((resolve, reject) => { 554 | client.broadcast 555 | .json(json, privateKey) 556 | .then((result) => { 557 | resolve(result); 558 | }) 559 | .catch((err) => { 560 | reject(err); 561 | }); 562 | }); 563 | } 564 | 565 | return Promise.reject(new Error('You dont have permission!')); 566 | }; 567 | 568 | export const delegate = (data, activeKey) => { 569 | const privateKey = PrivateKey.fromString(activeKey); 570 | 571 | return new Promise((resolve, reject) => { 572 | client.broadcast 573 | .delegateVestingShares(data, privateKey) 574 | .then((result) => { 575 | resolve(result); 576 | }) 577 | .catch((err) => { 578 | reject(err); 579 | }); 580 | }); 581 | }; 582 | 583 | export const transferToVesting = (data, activeKey) => { 584 | const privateKey = PrivateKey.fromString(activeKey); 585 | 586 | const op = [ 587 | 'transfer_to_vesting', 588 | { 589 | from: data.from, 590 | to: data.to, 591 | amount: data.amount, 592 | }, 593 | ]; 594 | 595 | return new Promise((resolve, reject) => { 596 | client.broadcast 597 | .sendOperations([op], privateKey) 598 | .then((result) => { 599 | resolve(result); 600 | }) 601 | .catch((error) => { 602 | reject(error); 603 | }); 604 | }); 605 | }; 606 | 607 | export const withdrawVesting = (data, activeKey) => { 608 | const privateKey = PrivateKey.fromString(activeKey); 609 | const op = [ 610 | 'withdraw_vesting', 611 | { 612 | account: data.account, 613 | vesting_shares: data.vesting_shares, 614 | }, 615 | ]; 616 | 617 | return new Promise((resolve, reject) => { 618 | client.broadcast 619 | .sendOperations([op], privateKey) 620 | .then((result) => { 621 | resolve(result); 622 | }) 623 | .catch((error) => { 624 | reject(error); 625 | }); 626 | }); 627 | }; 628 | 629 | export const lookupAccounts = async (username) => { 630 | try { 631 | const users = await client.database.call('lookup_accounts', [username, 20]); 632 | return users; 633 | } catch (error) { 634 | throw error; 635 | } 636 | }; 637 | 638 | export const getTrendingTags = async (tag) => { 639 | try { 640 | const users = await client.database.call('get_trending_tags', [tag, 20]); 641 | return users; 642 | } catch (error) { 643 | throw error; 644 | } 645 | }; 646 | 647 | export const postContent = ( 648 | account, 649 | pin, 650 | parentAuthor, 651 | parentPermlink, 652 | permlink, 653 | title, 654 | body, 655 | jsonMetadata, 656 | options = null, 657 | voteWeight = null, 658 | ) => _postContent( 659 | account, 660 | pin, 661 | parentAuthor, 662 | parentPermlink, 663 | permlink, 664 | title, 665 | body, 666 | jsonMetadata, 667 | options, 668 | voteWeight, 669 | ).then((resp) => { 670 | if (options) { 671 | const t = title ? 100 : 110; 672 | userActivity(account.username, t, resp.block_num, resp.id); 673 | } 674 | return resp; 675 | }); 676 | 677 | /** 678 | * @method postComment post a comment/reply 679 | * @param comment comment object { author, permlink, ... } 680 | */ 681 | const _postContent = async ( 682 | account, 683 | pin, 684 | parentAuthor, 685 | parentPermlink, 686 | permlink, 687 | title, 688 | body, 689 | jsonMetadata, 690 | options = null, 691 | voteWeight = null, 692 | ) => { 693 | const { name: author } = account; 694 | const digitPinCode = getDigitPinCode(pin); 695 | const key = getAnyPrivateKey(account.local, digitPinCode); 696 | 697 | if (account.local.authType === AUTH_TYPE.STEEM_CONNECT) { 698 | const token = decryptKey(account.local.accessToken, digitPinCode); 699 | const api = steemConnect.Initialize({ 700 | accessToken: token, 701 | }); 702 | 703 | const params = { 704 | parent_author: parentAuthor, 705 | parent_permlink: parentPermlink, 706 | author, 707 | permlink, 708 | title, 709 | body, 710 | json_metadata: JSON.stringify(jsonMetadata), 711 | }; 712 | 713 | const opArray = [['comment', params]]; 714 | 715 | if (options) { 716 | const e = ['comment_options', options]; 717 | opArray.push(e); 718 | } 719 | 720 | if (voteWeight) { 721 | const e = [ 722 | 'vote', 723 | { 724 | voter: author, 725 | author, 726 | permlink, 727 | weight: voteWeight, 728 | }, 729 | ]; 730 | opArray.push(e); 731 | } 732 | 733 | return api.broadcast(opArray); 734 | } 735 | 736 | if (key) { 737 | const opArray = [ 738 | [ 739 | 'comment', 740 | { 741 | parent_author: parentAuthor, 742 | parent_permlink: parentPermlink, 743 | author, 744 | permlink, 745 | title, 746 | body, 747 | json_metadata: JSON.stringify(jsonMetadata), 748 | }, 749 | ], 750 | ]; 751 | 752 | if (options) { 753 | const e = ['comment_options', options]; 754 | opArray.push(e); 755 | } 756 | 757 | if (voteWeight) { 758 | const e = [ 759 | 'vote', 760 | { 761 | voter: author, 762 | author, 763 | permlink, 764 | weight: voteWeight, 765 | }, 766 | ]; 767 | opArray.push(e); 768 | } 769 | 770 | const privateKey = PrivateKey.fromString(key); 771 | 772 | return new Promise((resolve, reject) => { 773 | client.broadcast 774 | .sendOperations(opArray, privateKey) 775 | .then((result) => { 776 | resolve(result); 777 | }) 778 | .catch((error) => { 779 | reject(error); 780 | }); 781 | }); 782 | } 783 | 784 | return Promise.reject(new Error('You dont have permission!')); 785 | }; 786 | 787 | // Re-blog 788 | // TODO: remove pinCode 789 | export const reblog = (account, pinCode, author, permlink) => _reblog( 790 | account, pinCode, author, permlink, 791 | ).then((resp) => { 792 | userActivity(account.name, 130, resp.block_num, resp.id); 793 | return resp; 794 | }); 795 | 796 | const _reblog = async (account, pinCode, author, permlink) => { 797 | const pin = getDigitPinCode(pinCode); 798 | const key = getAnyPrivateKey(account.local, pin); 799 | 800 | if (account.local.authType === AUTH_TYPE.STEEM_CONNECT) { 801 | const token = decryptKey(account.local.accessToken, pin); 802 | const api = steemConnect.Initialize({ 803 | accessToken: token, 804 | }); 805 | 806 | const follower = account.name; 807 | 808 | return api.reblog(follower, author, permlink); 809 | } 810 | 811 | if (key) { 812 | const privateKey = PrivateKey.fromString(key); 813 | const follower = account.name; 814 | 815 | const json = { 816 | id: 'follow', 817 | json: JSON.stringify([ 818 | 'reblog', 819 | { 820 | account: follower, 821 | author, 822 | permlink, 823 | }, 824 | ]), 825 | required_auths: [], 826 | required_posting_auths: [follower], 827 | }; 828 | 829 | return client.broadcast.json(json, privateKey); 830 | } 831 | 832 | return Promise.reject(new Error('You dont have permission!')); 833 | }; 834 | 835 | export const claimRewardBalance = (account, pinCode, rewardSteem, rewardSbd, rewardVests) => { 836 | const pin = getDigitPinCode(pinCode); 837 | const key = getAnyPrivateKey(account.local, pin); 838 | 839 | if (account.local.authType === AUTH_TYPE.STEEM_CONNECT) { 840 | const token = decryptKey(account.local.accessToken, pin); 841 | const api = steemConnect.Initialize({ 842 | accessToken: token, 843 | }); 844 | 845 | return api.claimRewardBalance(account.name, rewardSteem, rewardSbd, rewardVests); 846 | } 847 | 848 | if (key) { 849 | const privateKey = PrivateKey.fromString(key); 850 | 851 | const opArray = [ 852 | [ 853 | 'claim_reward_balance', 854 | { 855 | account: account.name, 856 | reward_steem: rewardSteem, 857 | reward_sbd: rewardSbd, 858 | reward_vests: rewardVests, 859 | }, 860 | ], 861 | ]; 862 | 863 | return client.broadcast.sendOperations(opArray, privateKey); 864 | } 865 | 866 | return Promise.reject(new Error('You dont have permission!')); 867 | }; 868 | 869 | const getAnyPrivateKey = (local, pin) => { 870 | const { postingKey, activeKey } = local; 871 | 872 | if (postingKey) { 873 | return decryptKey(local.postingKey, pin); 874 | } 875 | 876 | if (activeKey) { 877 | return decryptKey(local.postingKey, pin); 878 | } 879 | 880 | return false; 881 | }; 882 | © 2019 GitHub, Inc. -------------------------------------------------------------------------------- /src/navagation/MainNavigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | createSwitchNavigator, // here 4 | createStackNavigator, 5 | createAppContainer 6 | } from 'react-navigation'; 7 | 8 | // import MainScreen from '../components/MainScreen'; 9 | import TabNavigation from '../navagation/TabNavigation'; 10 | import LoginScreen from '../screens/LoginScreen'; 11 | import AuthLoadingScreen from '../components/AuthLoadingScreen'; 12 | 13 | import DetailScreen from '../screens/DetailScreen'; 14 | 15 | const AppStack = createStackNavigator({ 16 | Main: { 17 | screen: TabNavigation 18 | }, 19 | Details: { 20 | screen: DetailScreen 21 | } 22 | }, 23 | { 24 | initialRouteName: "Main", 25 | headerMode: 'screen', 26 | defaultNavigationOptions: { 27 | header: null 28 | } 29 | }); 30 | 31 | const AuthStack = createStackNavigator({ 32 | Login: { 33 | screen: LoginScreen, 34 | navigationOptions: { 35 | // title: 'Login', 36 | header: null 37 | } 38 | } 39 | }); 40 | 41 | export default createAppContainer(createSwitchNavigator( 42 | { 43 | AuthLoading: AuthLoadingScreen, 44 | App: AppStack, 45 | Auth: AuthStack, 46 | }, 47 | { 48 | initialRouteName: 'AuthLoading' 49 | } 50 | )); -------------------------------------------------------------------------------- /src/navagation/TabNavigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform } from 'react-native'; 3 | import { Icon } from 'native-base'; 4 | import { createMaterialTopTabNavigator, createBottomTabNavigator, createAppContainer, createStackNavigator } from 'react-navigation'; // 추가된 코드 5 | 6 | // import HomeTab from '../components/AppTabNavigator/HomeTab'; 7 | import HomeTab from '../screens/HomeTab'; 8 | import SearchTab from '../screens/SearchTab'; 9 | import AddMediaTab from '../screens/AddMediaTab'; 10 | import LikesTab from '../screens/LikesTab'; 11 | import ProfileTab from '../screens/ProfileTab'; 12 | 13 | // const TabNavigation = createMaterialTopTabNavigator({ 14 | const TabNavigation = createBottomTabNavigator({ 15 | Home:{ 16 | // screen: createStackNavigator({ 17 | // HomeTab : { 18 | // screen: HomeTab, 19 | // navigationOptions: { 20 | // title: 'Home', 21 | // } 22 | // } 23 | // }), 24 | screen: HomeTab, 25 | navigationOptions: { 26 | tabBarIcon: ({ focused, tintColor }) => 27 | } 28 | }, 29 | Search:{ 30 | screen: SearchTab, 31 | navigationOptions: { 32 | tabBarIcon: ({ focused, tintColor }) => 33 | } 34 | }, 35 | AddMedia:{ screen: AddMediaTab, 36 | navigationOptions: { 37 | tabBarIcon: ({ focused, tintColor }) => 38 | } 39 | }, 40 | Likes:{ 41 | screen: LikesTab, 42 | navigationOptions: { 43 | tabBarIcon: ({ focused, tintColor }) => 44 | } 45 | }, 46 | Profile:{ 47 | screen: ProfileTab, 48 | navigationOptions: { 49 | tabBarIcon: ({ focused, tintColor }) => 50 | } 51 | } 52 | }, { 53 | // animationEnabled: true, 54 | // swipeEnabled: true, 55 | // tabBarPosition: "bottom", 56 | tabBarOptions: { 57 | style: { 58 | backgroundColor:'white' 59 | }, 60 | iconStyle: { 61 | ...Platform.select({ 62 | ios:{ 63 | height: 35, 64 | marginBottom: 20 65 | } 66 | }) 67 | }, 68 | activeTintColor: '#000', 69 | inactiveTintColor: '#d1cece', 70 | upperCaseLabel: false, 71 | showLabel: false, 72 | showIcon: true, 73 | }, 74 | // defaultNavigationOptions: { 75 | // header: null 76 | // } 77 | }); 78 | 79 | export default createAppContainer(TabNavigation); -------------------------------------------------------------------------------- /src/postParser.js.bak: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { markDown2Html } from './markdownToHtml'; 3 | import { getPostSummary } from './formatter'; 4 | import { getReputation } from './reputation'; 5 | 6 | export const parsePosts = (posts, currentUserName, isSummary) => (!posts ? null : posts.map(post => parsePost(post, currentUserName, isSummary))); 7 | 8 | export const parsePost = (post, currentUserName, isSummary = false) => { 9 | if (!post) { 10 | return null; 11 | } 12 | const _post = post; 13 | 14 | _post.json_metadata = JSON.parse(post.json_metadata); 15 | _post.image = postImage(post.json_metadata, post.body); 16 | _post.vote_count = post.active_votes.length; 17 | _post.author_reputation = getReputation(post.author_reputation); 18 | _post.avatar = `https://steemitimages.com/u/${post.author}/avatar/small`; 19 | _post.active_votes.sort((a, b) => b.rshares - a.rshares); 20 | 21 | _post.body = markDown2Html(post.body); 22 | _post.summary = getPostSummary(post.body, 150); 23 | _post.is_declined_payout = Number(parseFloat(post.max_accepted_payout)) === 0; 24 | 25 | if (currentUserName) { 26 | _post.is_voted = isVoted(_post.active_votes, currentUserName); 27 | } else { 28 | _post.is_voted = false; 29 | } 30 | 31 | if (currentUserName === _post.author) { 32 | _post.markdownBody = post.body; 33 | } 34 | 35 | const totalPayout = parseFloat(_post.pending_payout_value) 36 | + parseFloat(_post.total_payout_value) 37 | + parseFloat(_post.curator_payout_value); 38 | 39 | _post.total_payout = totalPayout.toFixed(3); 40 | 41 | const voteRshares = _post.active_votes.reduce((a, b) => a + parseFloat(b.rshares), 0); 42 | const ratio = totalPayout / voteRshares; 43 | 44 | if (_post.active_votes && _post.active_votes.length > 0) { 45 | for (const i in _post.active_votes) { 46 | _post.vote_perecent = post.active_votes[i].voter === currentUserName ? post.active_votes[i].percent : null; 47 | _post.active_votes[i].value = (post.active_votes[i].rshares * ratio).toFixed(3); 48 | _post.active_votes[i].reputation = getReputation(post.active_votes[i].reputation); 49 | _post.active_votes[i].percent = post.active_votes[i].percent / 100; 50 | _post.active_votes[i].is_down_vote = Math.sign(post.active_votes[i].percent) < 0; 51 | _post.active_votes[i].avatar = `https://steemitimages.com/u/${ 52 | _post.active_votes[i].voter 53 | }/avatar/small`; 54 | } 55 | } 56 | 57 | return _post; 58 | }; 59 | 60 | const isVoted = (activeVotes, currentUserName) => activeVotes.some(v => v.voter === currentUserName && v.percent > 0); 61 | 62 | const postImage = (metaData, body) => { 63 | const imgTagRegex = /(]*>)/g; 64 | const markdownImageRegex = /!\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)/g; 65 | const urlRegex = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/gm; 66 | const imageRegex = /(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png)/g; 67 | let imageLink; 68 | 69 | if (metaData && metaData.image && metaData.image[0]) { 70 | imageLink = metaData.image[0]; 71 | } else if (body && markdownImageRegex.test(body)) { 72 | const markdownMatch = body.match(markdownImageRegex); 73 | if (markdownMatch[0]) { 74 | const firstMarkdownMatch = markdownMatch[0]; 75 | imageLink = firstMarkdownMatch.match(urlRegex)[0]; 76 | } 77 | } 78 | 79 | if (!imageLink && imageRegex.test(body)) { 80 | const imageMatch = body.match(imageRegex); 81 | imageLink = imageMatch[0]; 82 | } 83 | 84 | if (!imageLink && imgTagRegex.test(body)) { 85 | const _imgTag = body.match(imgTagRegex); 86 | const match = _imgTag[0].match(urlRegex); 87 | 88 | if (match && match[0]) { 89 | imageLink = match[0]; 90 | } 91 | } 92 | 93 | if (imageLink) { 94 | return `https://steemitimages.com/600x0/${imageLink}`; 95 | } 96 | return ''; 97 | }; 98 | 99 | // export const protocolUrl2Obj = (url) => { 100 | // let urlPart = url.split('://')[1]; 101 | 102 | // // remove last char if / 103 | // if (urlPart.endsWith('/')) { 104 | // urlPart = urlPart.substring(0, urlPart.length - 1); 105 | // } 106 | 107 | // const parts = urlPart.split('/'); 108 | 109 | // // filter 110 | // if (parts.length === 1) { 111 | // return { type: 'filter' }; 112 | // } 113 | 114 | // // filter with tag 115 | // if (parts.length === 2) { 116 | // return { type: 'filter-tag', filter: parts[0], tag: parts[1] }; 117 | // } 118 | 119 | // // account 120 | // if (parts.length === 1 && parts[0].startsWith('@')) { 121 | // return { type: 'account', account: parts[0].replace('@', '') }; 122 | // } 123 | 124 | // // post 125 | // if (parts.length === 3 && parts[1].startsWith('@')) { 126 | // return { 127 | // type: 'post', 128 | // cat: parts[0], 129 | // author: parts[1].replace('@', ''), 130 | // permlink: parts[2], 131 | // }; 132 | // } 133 | // }; 134 | 135 | export const parseComments = (comments) => { 136 | comments.map((comment) => { 137 | comment.pending_payout_value = parseFloat(comment.pending_payout_value).toFixed(3); 138 | comment.vote_count = comment.active_votes.length; 139 | comment.author_reputation = getReputation(comment.author_reputation); 140 | comment.avatar = `https://steemitimages.com/u/${comment.author}/avatar/small`; 141 | comment.body = markDown2Html(comment.body); 142 | comment.summary = getPostSummary(comment.body, 100, true); 143 | }); 144 | return comments; 145 | }; -------------------------------------------------------------------------------- /src/provider/SteemProvider.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createContext } from 'react'; 2 | 3 | const Context = createContext(); 4 | 5 | const { Provider, Consumer: SteemConsumer } = Context; 6 | 7 | class SteemProvider extends Component { 8 | state = { 9 | username: '', // 사용자 이름 10 | } 11 | 12 | actions = { 13 | setUsername: (username) => { 14 | this.setState({ username }); 15 | } 16 | } 17 | 18 | render() { 19 | const { state, actions } = this; 20 | const value = { state, actions }; 21 | return ( 22 | 23 | {this.props.children} 24 | 25 | ) 26 | } 27 | } 28 | 29 | export { 30 | SteemProvider, 31 | SteemConsumer, 32 | }; -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import steem from './steemReducer'; 3 | 4 | export default combineReducers({ 5 | steem 6 | }); -------------------------------------------------------------------------------- /src/reducers/steemReducer.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | 3 | // 액션 타입을 정의해줍니다. 4 | const SET_USERNAME = 'steem/setUsername'; 5 | 6 | // 액션 생성 함수를 만듭니다. 7 | export const setUsername = createAction(SET_USERNAME); 8 | 9 | const initialState = { 10 | username: '', 11 | } 12 | 13 | export default handleActions({ 14 | [SET_USERNAME]: (state, action) => { 15 | // console.log(GET_ACCOUNT, action); 16 | let newState = { 17 | ...state, 18 | username: action.payload.username, 19 | } 20 | return newState; 21 | } 22 | }, initialState); -------------------------------------------------------------------------------- /src/screens/AddMediaTab/AddMediaTabContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import AddMediaTabPresenter from './AddMediaTabPresenter'; 3 | 4 | export default class AddMediaTabContainer extends Component{ 5 | render() { 6 | return ; 7 | } 8 | } -------------------------------------------------------------------------------- /src/screens/AddMediaTab/AddMediaTabPresenter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | import { Icon } from 'native-base'; 4 | 5 | const AddMediaTabPresenter = () => ( 6 | 7 | AddMediaTab 8 | 9 | ) 10 | 11 | const style = StyleSheet.create({ 12 | container: { 13 | flex: 1, 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | } 17 | }); 18 | 19 | export default AddMediaTabPresenter; -------------------------------------------------------------------------------- /src/screens/AddMediaTab/index.js: -------------------------------------------------------------------------------- 1 | import AddMediaTabContainer from './AddMediaTabContainer'; 2 | export default AddMediaTabContainer; -------------------------------------------------------------------------------- /src/screens/DetailScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, StatusBar } from 'react-native'; 3 | import { 4 | Container, 5 | Content, 6 | Icon, 7 | Thumbnail, 8 | Header, 9 | Title, 10 | Left, 11 | Right, 12 | Body, 13 | Button, 14 | Spinner , 15 | Text 16 | } from 'native-base'; 17 | import styled from 'styled-components'; 18 | import MarkdownView from '../components/MarkdownView'; 19 | 20 | const PostTitle = styled.Text` 21 | color: black; 22 | font-size: 28px; 23 | font-family: 'Noto Serif KR Bold'; 24 | font-weight: bold; 25 | `; 26 | 27 | export default (props) => { 28 | // console.log(props); 29 | 30 | const { content }= props.navigation.state.params; 31 | // console.log(content); 32 | 33 | return ( 34 | 35 |
39 | 42 | 43 | 47 | 48 | 49 | Details 50 | 51 | 52 |
53 | 54 | {content.title} 55 | {content.body} 56 | 57 |
58 | ) 59 | }; 60 | 61 | const styles = StyleSheet.create({ 62 | container: { 63 | flex: 1, 64 | backgroundColor: 'white' 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /src/screens/HomeTab/HomeTabContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getFeeds, getFollowing } from '../../steem'; 4 | import Dataset from 'impagination'; 5 | 6 | import HomeTabPresenter from './HomeTabPresenter'; 7 | 8 | const DEFAULT_LIMIT = 5; 9 | 10 | class HomeTabContainer extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | loading: true, 17 | dataset: null, 18 | next: { 19 | startAuthor: '', 20 | startPermlink: '', 21 | }, 22 | feeds: [], 23 | followings: [] 24 | }; 25 | 26 | // TEST 27 | // this.props.navigation.navigate('Details', { content: require('../../DUMMY').dummy[1] }); 28 | } 29 | 30 | // 피드 가져오기 31 | setupImpagination = () => { 32 | // 다음 피드 조회 33 | _fetchFeeds = () => { 34 | const { startAuthor, startPermlink } = this.state.next; 35 | return getFeeds({ 36 | tag: this.props.username, 37 | limit: DEFAULT_LIMIT + 1, 38 | start_author: startAuthor, 39 | start_permlink: startPermlink 40 | }).then(feeds => { 41 | let next = { 42 | startAuthor: '', 43 | startPermlink: '', 44 | } 45 | if(feeds.length > DEFAULT_LIMIT) { 46 | const { author, permlink } = feeds.pop(); 47 | next = { 48 | startAuthor: author, 49 | startPermlink: permlink 50 | } 51 | } 52 | this.setState({ next }); 53 | return feeds; 54 | }) 55 | .catch(error => console.log(error)); 56 | } 57 | 58 | let dataset = new Dataset({ 59 | pageSize: DEFAULT_LIMIT, // 한번에 가져올 레코드 갯수 60 | observe: (nextState) => { 61 | // 새로운 `state`가 생성될때 마다 호출된다. 62 | this.setState({ feeds: nextState }); 63 | }, 64 | fetch(pageOffset, pageSize, stats) { 65 | return _fetchFeeds(); 66 | } 67 | }); 68 | 69 | dataset.setReadOffset(0); 70 | this.setState({ dataset }); 71 | } 72 | 73 | setCurrentReadOffset = (event) => { 74 | let itemHeight = 402; 75 | let currentOffset = Math.floor(event.nativeEvent.contentOffset.y); 76 | let currentItemIndex = Math.ceil(currentOffset / itemHeight); 77 | this.state.dataset.setReadOffset(currentItemIndex); 78 | } 79 | 80 | componentWillMount() { 81 | this.setupImpagination(); 82 | 83 | // getFollowing('anpigon', '', 10).then(followings => { 84 | getFollowing(this.props.username, '', 20).then(followings => { 85 | this.setState({ 86 | followings 87 | }); 88 | }); 89 | } 90 | 91 | render() { 92 | const { 93 | loading, 94 | followings, 95 | feeds 96 | } = this.state; 97 | // console.log({ loading, followings, feeds }); 98 | 99 | return ; 106 | } 107 | } 108 | 109 | const mapStateToProps = (state) => { 110 | const { username } = state.steem; 111 | return { 112 | username, 113 | } 114 | }; 115 | 116 | // const mapDispatchToProps = { fetchFeeds }; 117 | 118 | export default connect( 119 | mapStateToProps, 120 | // mapDispatchToProps 121 | )(HomeTabContainer); -------------------------------------------------------------------------------- /src/screens/HomeTab/HomeTabPresenter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { View, StyleSheet, ScrollView, StatusBar } from 'react-native'; 4 | import { 5 | Container, 6 | Content, 7 | Icon, 8 | Thumbnail, 9 | Header, 10 | Title, 11 | Left, 12 | Right, 13 | Body, 14 | Spinner 15 | } from 'native-base'; 16 | 17 | import CardComponent from '../../components/CardComponent'; 18 | import { TINT_COLOR } from '../../constants/Colors' 19 | 20 | function HomeTabPresenter(props) { 21 | // console.log(props); 22 | 23 | const { 24 | loading, 25 | followings, 26 | feeds, 27 | onScroll 28 | } = props; 29 | 30 | return ( 31 | 32 |
35 | 38 | 39 | 40 | Insteemgram 41 | 42 | 43 |
44 | 48 | {/* 여기부터 스토리 헤더 시작 */} 49 | 50 | {/* 51 | Stories 52 | 53 | 54 | Watch All 55 | 56 | */} 57 | 58 | 66 | { 67 | (followings||[]).map(following => ( 68 | 72 | )) 73 | } 74 | 75 | 76 | 77 | {/* 여기까지 스토리 헤더 끝 */} 78 | { 79 | (feeds||[]).map(record => { 80 | if (!record.isSettled) { 81 | return ; 82 | } 83 | const { content } = record; 84 | return { 89 | event.stopPropagation(); 90 | props.navigation.navigate('Details', { content }); 91 | } 92 | } 93 | /> 94 | }) 95 | } 96 | 97 |
98 | ); 99 | } 100 | 101 | HomeTabPresenter.propTypes = { 102 | loading: PropTypes.bool.isRequired, 103 | followings: PropTypes.array, 104 | feeds: PropTypes.object, 105 | onScroll: PropTypes.func.isRequired, 106 | navigation: PropTypes.object.isRequired, 107 | } 108 | 109 | const styles = StyleSheet.create({ 110 | container: { 111 | flex: 1, 112 | backgroundColor: 'white' 113 | }, 114 | title: { 115 | fontFamily: 'Sweet Sensations Persona Use', 116 | fontSize: 30, 117 | color: '#242424', 118 | } 119 | }); 120 | 121 | export default HomeTabPresenter; -------------------------------------------------------------------------------- /src/screens/HomeTab/index.js: -------------------------------------------------------------------------------- 1 | import HomeTabContainer from './HomeTabContainer'; 2 | export default HomeTabContainer; -------------------------------------------------------------------------------- /src/screens/LikesTab/LikesTabContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LikesTabPresenter from './LikesTabPresenter'; 3 | 4 | export default class LikesTabContainer extends Component{ 5 | render() { 6 | return ; 7 | } 8 | } -------------------------------------------------------------------------------- /src/screens/LikesTab/LikesTabPresenter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | import { Icon } from 'native-base'; 4 | 5 | const LikesTabPresenter = () => ( 6 | 7 | LikesTab 8 | 9 | ) 10 | 11 | const style = StyleSheet.create({ 12 | container: { 13 | flex: 1, 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | } 17 | }); 18 | 19 | export default LikesTabPresenter; -------------------------------------------------------------------------------- /src/screens/LikesTab/index.js: -------------------------------------------------------------------------------- 1 | import LikesTabContainer from './LikesTabContainer'; 2 | export default LikesTabContainer; -------------------------------------------------------------------------------- /src/screens/LoginScreen.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | StyleSheet, 5 | View, 6 | Modal, 7 | // AsyncStorage // here 8 | } from 'react-native'; 9 | import { Icon, Container, Button, Text, Input } from 'native-base'; 10 | import styled from 'styled-components'; 11 | 12 | import { Constants, SecureStore } from 'expo'; 13 | import SteemConnectModal from '../components/SteemConnectModal'; 14 | import { setUsername } from '../reducers/steemReducer'; 15 | // import steemConnect from '../steemConnect'; 16 | 17 | const LogoText = styled.Text` 18 | font-family: 'Sweet Sensations Persona Use'; 19 | font-size: 60; 20 | margin-bottom: 50; 21 | color: #242424; 22 | `; 23 | 24 | class LoginScreen extends Component { 25 | 26 | constructor(props) { 27 | super(props); 28 | 29 | this.state = { 30 | modalVisible: false, 31 | loggedin: false 32 | // username: null, 33 | } 34 | } 35 | 36 | _signInAsync = async (userToken) => { 37 | this.setState({ loggedin: true }); 38 | 39 | // 토큰 발급일 40 | userToken['issued_at'] = Math.floor(Date.now() / 1000); 41 | 42 | // 토큰 저장 43 | await SecureStore.setItemAsync('userToken', JSON.stringify(userToken), { keychainService: Constants.deviceId }); 44 | // console.log('userToken:', userToken); 45 | 46 | this.props.setUsername({ username: userToken.username }); 47 | 48 | this.props.navigation.navigate('App'); 49 | }; 50 | 51 | // 모달창 닫기 52 | _handleOnModalClose = () => { 53 | this.setState({ modalVisible: false }); 54 | } 55 | 56 | // 스팀커넥트 성공 57 | _onSteemconnectSuccess = (tokens) => { 58 | this.setState({ modalVisible: false }); 59 | // console.log('tokens', tokens); 60 | 61 | this._signInAsync(tokens); // 로그인 성공 62 | 63 | /* 64 | // AccessToken 셋팅 65 | steemConnect.setAccessToken(tokens.access_token); 66 | 67 | // 계정 정보 조회 68 | steemConnect.me().then(({ account }) => { 69 | const { profile } = JSON.parse(account.json_metadata); 70 | console.log('profile', profile); 71 | this.setState({ username: profile.name }); 72 | }); 73 | */ 74 | } 75 | 76 | render() { 77 | return ( 78 | 79 | Insteemgram 80 | 91 | {/** 스팀커넥트 모달창 **/} 92 | 97 | 101 | 102 | 103 | ) 104 | } 105 | } 106 | 107 | const styles = StyleSheet.create({ 108 | container: { 109 | flex: 1, 110 | alignItems: 'center', 111 | justifyContent: 'center', 112 | }, 113 | loginButton: { 114 | marginHorizontal: 35, 115 | backgroundColor: '#3798f2', 116 | height: 55, 117 | borderRadius: 7, 118 | } 119 | }); 120 | 121 | const mapStateToProps = (state) => { 122 | const { username } = state.steem; 123 | return { username } 124 | }; 125 | const mapDispatchToProps = { 126 | setUsername 127 | }; 128 | export default connect( 129 | mapStateToProps, 130 | mapDispatchToProps, 131 | )(LoginScreen); -------------------------------------------------------------------------------- /src/screens/ProfileTab/ProfileTab.1.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View, Text, StyleSheet, Image } from 'react-native'; 3 | import { Icon, Container, Content, Header, Left, Body, Right, Button } from 'native-base'; 4 | import EntypoIcon from 'react-native-vector-icons/Entypo' 5 | 6 | export default class ProfileTab extends Component{ 7 | 8 | static navigationOptions = { 9 | tabBarIcon: ({ tintColor }) => ( 10 | 11 | ) 12 | } 13 | 14 | state = { 15 | name: '', 16 | reputation: 0, 17 | profile: {}, 18 | postCount: 0, 19 | followingCount: 0, 20 | followerCount: 0, 21 | activeIndex: 0, 22 | } 23 | 24 | segmentClicked = (activeIndex) => { 25 | this.setState({ activeIndex }) 26 | } 27 | 28 | renderSection = () => { 29 | if(this.state.activeIndex === 0) { 30 | return ( 31 | 32 | This is first section 33 | 34 | ) 35 | } 36 | } 37 | 38 | fetchAccount(username) { 39 | // const data = { 40 | // id: 3, 41 | // jsonrpc: "2.0", 42 | // method: "call", 43 | // params: [ 44 | // "database_api", 45 | // "get_accounts", 46 | // [[username]] 47 | // ] 48 | // }; 49 | // return fetch('https://api.steemit.com', 50 | // { 51 | // method: 'POST', 52 | // body: JSON.stringify(data) 53 | // }) 54 | // .then(res => res.json()) 55 | // .then(res => res.result[0]) 56 | } 57 | 58 | fetchFollowCount(username) { 59 | // const data = { 60 | // id: 4, 61 | // jsonrpc: "2.0", 62 | // method: "call", 63 | // params: [ 64 | // "follow_api", 65 | // "get_follow_count", 66 | // [username] 67 | // ] 68 | // }; 69 | // return fetch('https://api.steemit.com', 70 | // { 71 | // method: 'POST', 72 | // body: JSON.stringify(data) 73 | // }) 74 | // .then(res => res.json()) 75 | // .then(res => res.result) 76 | } 77 | 78 | componentWillMount() { 79 | const username = 'anpigon'; 80 | 81 | // this.fetchAccount(username).then(({name, post_count, reputation, json_metadata}) => { 82 | // const { profile } = JSON.parse(json_metadata); 83 | // const log = Math.log(parseInt(reputation.substring(0, 4))) / Math.log(10); 84 | // this.setState({ 85 | // name, 86 | // reputation: Math.max(((reputation.length - 1) + (log - parseInt(log))) - 9, 0) * 9 + 25, 87 | // postCount: post_count, 88 | // profile 89 | // }) 90 | // }); 91 | 92 | // this.fetchFollowCount(username).then(({following_count, follower_count}) => { 93 | // this.setState({ 94 | // followingCount: following_count, 95 | // followerCount: follower_count 96 | // }) 97 | // }); 98 | } 99 | 100 | render() { 101 | const { 102 | name, 103 | reputation, 104 | profile, 105 | postCount, 106 | followingCount, 107 | followerCount 108 | } = this.state; 109 | 110 | return ( 111 | 112 |
113 | 114 | {name} 115 | 116 |
117 | 118 | 119 | 120 | 122 | 123 | 124 | 125 | 126 | {postCount} 127 | posts 128 | 129 | 130 | {followingCount} 131 | follower 132 | 133 | 134 | {followerCount} 135 | following 136 | 137 | 138 | 139 | 143 | 147 | 148 | 149 | 150 | 151 | {profile.name} ({reputation.toFixed(2)}) 152 | {profile.about} 153 | {profile.website} 154 | 155 | 156 | 157 | 163 | 169 | 175 | 181 | 182 | { this.renderSection() } 183 | 184 |
185 | ); 186 | } 187 | } 188 | 189 | const style = StyleSheet.create({ 190 | container: { 191 | flex: 1, 192 | alignItems: 'center', 193 | justifyContent: 'center', 194 | } 195 | }); -------------------------------------------------------------------------------- /src/screens/ProfileTab/ProfileTab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View, Text, StyleSheet, Image, Dimensions } from 'react-native'; 3 | import { Icon, Container, Content, Header, Left, Body, Right, Button } from 'native-base'; 4 | import EntypoIcon from 'react-native-vector-icons/Entypo'; 5 | 6 | import CardComponent from '../CardComponent'; 7 | 8 | let images = [ 9 | "https://cdn.pixabay.com/photo/2018/11/29/21/19/hamburg-3846525__480.jpg", 10 | "https://cdn.pixabay.com/photo/2018/11/11/16/51/ibis-3809147__480.jpg", 11 | "https://cdn.pixabay.com/photo/2018/11/23/14/19/forest-3833973__480.jpg", 12 | "https://cdn.pixabay.com/photo/2019/01/05/17/05/man-3915438__480.jpg", 13 | "https://cdn.pixabay.com/photo/2018/12/04/22/38/road-3856796__480.jpg", 14 | "https://cdn.pixabay.com/photo/2018/11/04/20/21/harley-davidson-3794909__480.jpg", 15 | "https://cdn.pixabay.com/photo/2018/12/25/21/45/crystal-ball-photography-3894871__480.jpg", 16 | "https://cdn.pixabay.com/photo/2018/12/29/23/49/rays-3902368__480.jpg", 17 | "https://cdn.pixabay.com/photo/2017/05/05/16/57/buzzard-2287699__480.jpg", 18 | "https://cdn.pixabay.com/photo/2018/08/06/16/30/mushroom-3587888__480.jpg", 19 | "https://cdn.pixabay.com/photo/2018/12/15/02/53/flower-3876195__480.jpg", 20 | "https://cdn.pixabay.com/photo/2018/12/16/18/12/open-fire-3879031__480.jpg", 21 | "https://cdn.pixabay.com/photo/2018/11/24/02/05/lichterkette-3834926__480.jpg", 22 | "https://cdn.pixabay.com/photo/2018/11/29/19/29/autumn-3846345__480.jpg" 23 | ] 24 | 25 | const { width, height } = Dimensions.get('window'); 26 | 27 | export default class ProfileTab extends Component { 28 | 29 | // static navigationOptions = { 30 | // tabBarIcon: ({ tintColor }) => ( 31 | // 32 | // ) 33 | // } 34 | 35 | constructor(props){ 36 | super(props) 37 | 38 | this.state = { 39 | name: '', 40 | reputation: 0, 41 | profile: {}, 42 | postCount: 0, 43 | followingCount: 0, 44 | followerCount: 0, 45 | activeIndex: 0, 46 | blogs: [] 47 | }; 48 | } 49 | 50 | segmentClicked = (index) => { 51 | this.setState({ 52 | activeIndex: index 53 | }) 54 | } 55 | 56 | renderSectionOne = () => { 57 | return images.map((image, index) => { 58 | return ( 59 | 60 | 61 | 62 | ) 63 | }) 64 | } 65 | 66 | renderSectionTwo = () => { 67 | return this.state.blogs.map(blog => ( 68 | 69 | )); 70 | } 71 | 72 | renderSection = () => { 73 | if(this.state.activeIndex === 0) { 74 | return ( 75 | 76 | { this.renderSectionOne() } 77 | 78 | ) 79 | } 80 | else if(this.state.activeIndex === 1) { 81 | return ( 82 | 83 | { this.renderSectionTwo() } 84 | 85 | ) 86 | } 87 | } 88 | 89 | fetchState(username) { 90 | const data = { 91 | id: 4, 92 | jsonrpc: "2.0", 93 | method: "call", 94 | params: [ 95 | "database_api", 96 | "get_state", 97 | [`/@${username}`] 98 | ] 99 | }; 100 | return fetch('https://api.steemit.com', 101 | { 102 | method: 'POST', 103 | body: JSON.stringify(data) 104 | }) 105 | .then(res => res.json()) 106 | .then(res => res.result) 107 | } 108 | 109 | fetchBlog(tag, limit=20, start_author='', start_permlink='') { 110 | const data = { 111 | id: 5, 112 | jsonrpc: "2.0", 113 | method: "call", 114 | params: [ 115 | "database_api", 116 | "get_discussions_by_blog", 117 | [{ tag, limit, start_author, start_permlink }] 118 | ] 119 | }; // react-native-steemjs--1546529527678 120 | return fetch('https://api.steemit.com', 121 | { 122 | method: 'POST', 123 | body: JSON.stringify(data) 124 | }) 125 | .then(res => res.json()) 126 | .then(res => res.result) 127 | } 128 | 129 | fetchAccount(username) { 130 | const data = { 131 | id: 3, 132 | jsonrpc: "2.0", 133 | method: "call", 134 | params: [ 135 | "database_api", 136 | "get_accounts", 137 | [[username]] 138 | ] 139 | }; 140 | return fetch('https://api.steemit.com', 141 | { 142 | method: 'POST', 143 | body: JSON.stringify(data) 144 | }) 145 | .then(res => res.json()) 146 | .then(res => res.result[0]) 147 | } 148 | 149 | fetchFollowCount(username) { 150 | const data = { 151 | id: 4, 152 | jsonrpc: "2.0", 153 | method: "call", 154 | params: [ 155 | "follow_api", 156 | "get_follow_count", 157 | [username] 158 | ] 159 | }; 160 | return fetch('https://api.steemit.com', 161 | { 162 | method: 'POST', 163 | body: JSON.stringify(data) 164 | }) 165 | .then(res => res.json()) 166 | .then(res => res.result) 167 | } 168 | 169 | componentWillMount() { 170 | const username = 'anpigon'; 171 | /* 172 | this.fetchState(username).then(({ 173 | accounts, 174 | content, 175 | feed_price, 176 | props, 177 | }) => { 178 | // feed_price: {base: "0.399 SBD", quote: "1.000 STEEM"} 179 | // props: {time: "2019-01-24T14:53:33", sbd_print_rate: 10000, sbd_interest_rate: 0, head_block_number: 29738762, total_vesting_shares: "411678853570.303833 VESTS", …} 180 | const { name, post_count, reputation, json_metadata, blog, net_vesting_shares, created } = accounts[username]; 181 | const { profile } = JSON.parse(json_metadata); 182 | const log = Math.log(parseInt(String(reputation).substring(0, 4))) / Math.log(10); 183 | this.setState({ 184 | name, 185 | reputation: Math.max(((String(reputation).length - 1) + (log - parseInt(log))) - 9, 0) * 9 + 25, 186 | postCount: post_count, 187 | profile, 188 | blogs: Object.values(content) 189 | }) 190 | }); 191 | */ 192 | /*this.fetchAccount(username).then(({name, post_count, reputation, json_metadata}) => { 193 | const { profile } = JSON.parse(json_metadata); 194 | const log = Math.log(parseInt(reputation.substring(0, 4))) / Math.log(10); 195 | this.setState({ 196 | name, 197 | reputation: Math.max(((reputation.length - 1) + (log - parseInt(log))) - 9, 0) * 9 + 25, 198 | postCount: post_count, 199 | profile 200 | }) 201 | });*/ 202 | 203 | this.fetchFollowCount(username).then(({following_count, follower_count}) => { 204 | this.setState({ 205 | followingCount: following_count, 206 | followerCount: follower_count 207 | }) 208 | }); 209 | } 210 | 211 | render() { 212 | const { 213 | name, 214 | reputation, 215 | profile, 216 | postCount, 217 | followingCount, 218 | followerCount 219 | } = this.state; 220 | 221 | return ( 222 | 223 |
224 | 225 | {name} 226 | 227 |
228 | 229 | 230 | 231 | 233 | 234 | 235 | 236 | 237 | {postCount} 238 | posts 239 | 240 | 241 | {followingCount} 242 | follower 243 | 244 | 245 | {followerCount} 246 | following 247 | 248 | 249 | 250 | 254 | 258 | 259 | 260 | 261 | 262 | {profile.name} ({reputation.toFixed(2)}) 263 | {profile.about} 264 | {profile.website} 265 | 266 | 267 | 268 | 274 | 280 | 286 | 292 | 293 | 294 | {/* 아래 코드 추가 */} 295 | { this.renderSection() } 296 | 297 |
298 | ); 299 | } 300 | } 301 | 302 | const style = StyleSheet.create({ 303 | container: { 304 | flex: 1, 305 | alignItems: 'center', 306 | justifyContent: 'center', 307 | } 308 | }); -------------------------------------------------------------------------------- /src/screens/ProfileTab/ProfileTabContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ProfileTabPresenter from './ProfileTabPresenter'; 3 | 4 | export default class ProfileTabContainer extends Component{ 5 | render() { 6 | return ; 7 | } 8 | } -------------------------------------------------------------------------------- /src/screens/ProfileTab/ProfileTabPresenter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | import { Icon } from 'native-base'; 4 | 5 | const ProfileTabPresenter = () => ( 6 | 7 | ProfileTab 8 | 9 | ) 10 | 11 | const style = StyleSheet.create({ 12 | container: { 13 | flex: 1, 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | } 17 | }); 18 | 19 | export default ProfileTabPresenter; -------------------------------------------------------------------------------- /src/screens/ProfileTab/index.js: -------------------------------------------------------------------------------- 1 | import ProfileTabContainer from './ProfileTabContainer'; 2 | export default ProfileTabContainer; -------------------------------------------------------------------------------- /src/screens/SearchTab/SearchTabContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SearchTabPresenter from './SearchTabPresenter'; 3 | 4 | export default class SearchTabContainer extends Component{ 5 | render() { 6 | return ; 7 | } 8 | } -------------------------------------------------------------------------------- /src/screens/SearchTab/SearchTabPresenter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | import { Icon } from 'native-base'; 4 | 5 | const SearchTabPresenter = () => ( 6 | 7 | LikesTab 8 | 9 | ) 10 | 11 | const style = StyleSheet.create({ 12 | container: { 13 | flex: 1, 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | } 17 | }); 18 | 19 | export default SearchTabPresenter; -------------------------------------------------------------------------------- /src/screens/SearchTab/index.js: -------------------------------------------------------------------------------- 1 | import SearchTabContainer from './SearchTabContainer'; 2 | export default SearchTabContainer; -------------------------------------------------------------------------------- /src/steem.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | // import { Client } from './dsteem'; 3 | import axios from 'axios'; 4 | import Remarkable from 'remarkable'; 5 | const md = new Remarkable({ html: true, linkify: false, breaks: false }); 6 | // import removeMarkdown from 'remove-markdown'; 7 | 8 | const DEFAULT_SERVER = 'https://api.steemit.com'; 9 | // let client = new Client(DEFAULT_SERVER); 10 | const client = axios.create({ 11 | baseURL: DEFAULT_SERVER, 12 | method: 'post', 13 | headers: {'Content-Type': 'application/json; charset=utf-8'}, 14 | }); 15 | 16 | function call(api, method, params = []) { 17 | const request = { 18 | id: '0', 19 | jsonrpc: '2.0', 20 | method: 'call', 21 | params: [api, method, params], 22 | }; 23 | const body = JSON.stringify(request, (key, value) => { 24 | if (typeof value === 'object' && value.type === 'Buffer') { 25 | return Buffer.from(value.data).toString('hex'); 26 | } 27 | return value; 28 | }); 29 | // console.log(body); 30 | return client.post('', body) 31 | .then(({ 32 | status, 33 | statusText, 34 | data 35 | }) => { 36 | // console.log(data) 37 | if(status === 200) { 38 | if(data.error) new Error(`${error.code}:${error.message}`); 39 | if(data.result.error) new Error(`${data.result.error.code}:${data.result.error.message}`); 40 | return data.result; 41 | } else { 42 | throw new Error(`${status}:${statusText}`) 43 | } 44 | }); 45 | } 46 | 47 | function getDiscussions(by, query) { 48 | return call('condenser_api', `get_discussions_by_${by}`, [query]); 49 | } 50 | 51 | export const getFeeds = (query, user) => { 52 | return getDiscussions('feed', query) 53 | .then(posts => { 54 | // console.log(query, posts.length); 55 | return posts.map(post => { 56 | let summary = md.render(post.body).replace(/<\/?[^>]+(>|$)/g, '').replace(/https?:\/\/[^\s]+/g, "").replace(/(^(\n|\r|\s)*)>([\s\S]*?).*\s*/g, "").replace(/\s+/g, " ").replace(/^\s*|\s*$/g, "").slice(0, 200); 57 | return { 58 | ...post, 59 | summary 60 | }; 61 | }) 62 | }); 63 | }; 64 | 65 | export const getFollowing = (username, startFollowing, limit = 100) => { 66 | return call('condenser_api', 'get_following', [username, startFollowing, 'blog', limit]) 67 | .then(followings => followings.map(({ following }) => following)); 68 | } 69 | -------------------------------------------------------------------------------- /src/steemConnect.js: -------------------------------------------------------------------------------- 1 | import sc2 from 'steemconnect'; 2 | import { SC2_APP_ID, SC2_CALLBACK_URL } from './config'; 3 | 4 | const api = sc2.Initialize({ 5 | app: SC2_APP_ID, 6 | callbackURL: SC2_CALLBACK_URL, 7 | // accessToken: tokens.access_token, 8 | scope: ['vote','comment','delete_comment','comment_options','custom_json','claim_reward_balance','offline'] 9 | }); 10 | 11 | export default api; --------------------------------------------------------------------------------