diff --git a/App.js b/App.js
new file mode 100644
index 0000000..7a2af99
--- /dev/null
+++ b/App.js
@@ -0,0 +1,16 @@
+import AuthLoading from './src/AuthLoading'
+import Messages from './src/Messages'
+import Login from './src/Login'
+import { createSwitchNavigator } from 'react-navigation';
+
+export default createSwitchNavigator({
+ Messages: {
+ screen: Messages,
+ },
+ Login: {
+ screen: Login,
+ },
+ AuthLoading: {
+ screen: AuthLoading
+ },
+}, {initialRouteName: 'AuthLoading'});
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..01e6538
--- /dev/null
+++ b/index.js
@@ -0,0 +1,6 @@
+import { AppRegistry, YellowBox } from 'react-native';
+import App from './App';
+
+YellowBox.ignoreWarnings(['Warning: isMounted(...) is deprecated', 'Module RCTImageLoader']);
+
+AppRegistry.registerComponent('gotify', () => App);
diff --git a/src/AuthLoading.js b/src/AuthLoading.js
new file mode 100644
index 0000000..b73bae8
--- /dev/null
+++ b/src/AuthLoading.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import {
+ ActivityIndicator,
+ AsyncStorage,
+ StatusBar,
+ StyleSheet,
+ View,
+} from 'react-native';
+import SharedPreferences from 'react-native-shared-preferences';
+
+export default class AuthLoadingScreen extends React.Component {
+ componentDidMount() {
+ SharedPreferences.getItem('@global:token', (token) => {
+ this.props.navigation.navigate(token ? 'Messages' : 'Login');
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Login.js b/src/Login.js
new file mode 100644
index 0000000..c810241
--- /dev/null
+++ b/src/Login.js
@@ -0,0 +1,154 @@
+import React from 'react';
+import { View, Text, TextInput, Button, ToastAndroid } from 'react-native';
+import SharedPreferences from 'react-native-shared-preferences';
+import Icon from 'react-native-vector-icons/MaterialIcons'
+import DeviceInfo from 'react-native-device-info';
+import * as axios from "axios";
+
+const urlRegex = new RegExp("^(http|https)://", "i");
+const defaultClientName = DeviceInfo.getManufacturer() + ' ' + DeviceInfo.getDeviceId();
+export default class Login extends React.Component {
+ state = {tryConnect: null, url: null, error: null, version: null, name: '', pass: '', loggedIn: null, client: defaultClientName};
+ componentDidMount() {
+ SharedPreferences.getItem("@global:url", (url) => {
+ if (url) {
+ this.setState({...this.state, url: url}, this.checkUrl);
+ }
+ });
+ }
+
+ handleChange = (name, merge) => {
+ return (val) => {
+ const old = {...this.state, ...(merge || {})};
+ old[name] = val;
+ this.setState(old);
+ }
+ };
+
+ reachedButNot = () => {
+ this.setState({...this.state, tryConnect: false, error: 'server exists, but it is not a valid gotify instance'})
+ };
+
+ notReachable = () => {
+ this.setState({...this.state, tryConnect: false, error: 'could not reach ' + this.state.url})
+ };
+
+ createClient = () => {
+ const {client, url, pass, name} = this.state;
+ axios.post(url + 'client', {name: client}, {auth:{username: name, password: pass}}).then((resp) => {
+ SharedPreferences.setItem('@global:token', resp.data.token);
+ ToastAndroid.show("Created client " + client + " for user " + name, ToastAndroid.SHORT);
+ this.props.navigation.navigate('AuthLoading')
+ }).catch(() => {
+ ToastAndroid.show("Could not create client", ToastAndroid.SHORT);
+ })
+ };
+
+ version(url, errorCallback, reachedButNoApi) {
+ axios.get(url + 'version').then((resp) => {
+ if (resp && resp.status === 200) {
+ const {data} = resp;
+ if (data
+ && typeof data === 'object'
+ && 'version' in data
+ && 'buildDate' in data
+ && 'commit' in data
+ ) {
+ SharedPreferences.setItem('@global:url', url);
+ this.setState({...this.state, url: url, tryConnect: true, error: null, version: data});
+ return;
+ }
+ }
+ (reachedButNoApi ? reachedButNoApi : errorCallback)();
+ }).catch(errorCallback);
+ }
+
+ checkUrl = () => {
+ let {url} = this.state;
+ if (!url.endsWith('/')) {
+ url += '/';
+ }
+
+ if (urlRegex.test(url)) {
+ if(url.match(/^http:\/\//i)){
+ this.version(url.replace(/^http:\/\//i,"https://"), () => {
+ this.version(url, this.notReachable, this.reachedButNot)
+ });
+ } else {
+ this.version(url, this.notReachable,this.reachedButNot)
+ }
+ } else {
+ this.setState({...this.state, error: 'url must either start with http:// or https://'})
+ }
+ };
+
+ checkUser = () => {
+ const {url, pass, name} = this.state;
+ axios.get(url + 'current/user', {auth:{username: name, password: pass}}).then((resp) => {
+ this.setState({...this.state, loggedIn: true})
+ }).catch((resp) => {
+ this.setState({...this.state, loggedIn: false})
+ })
+ };
+
+ render() {
+ const {tryConnect, error, version, url, client, loggedIn} = this.state;
+
+ return (
+
+
+ Gotify
+
+
+
+
+ {error && {error}}
+ {version && Gotify v{version.version}}
+ {tryConnect === true && (
+
+
+
+
+
+
+
+ {loggedIn === true && (
+
+
+
+ valid user
+
+
+
+
+
+
+ )}
+ {loggedIn === false && (
+
+ wrong name / pass
+
+ )}
+
+
+ )}
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Messages.js b/src/Messages.js
new file mode 100644
index 0000000..3f06783
--- /dev/null
+++ b/src/Messages.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import {Button, Text, View} from "react-native";
+import SharedPreferences from 'react-native-shared-preferences';
+
+export default class Messages extends React.Component {
+ render() {
+ SharedPreferences.getAll(console.log);
+ return (
+
+ TODO
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/native/LogReader.js b/src/native/LogReader.js
new file mode 100644
index 0000000..e69de29