Compare commits

..

No commits in common. "main" and "Goober_0_6_2" have entirely different histories.

64 changed files with 3054 additions and 4030 deletions

5
.gitignore vendored
View file

@ -23,7 +23,10 @@ node_modules/
tmp/ tmp/
temp/ temp/
hooks/ hooks/
platforms/
plugins/ plugins/
plugins/android.json
plugins/ios.json
www/ www/
$RECYCLE.BIN/ $RECYCLE.BIN/
@ -33,5 +36,3 @@ UserInterfaceState.xcuserstate
# other bits # other bits
pnut-oauth.ts pnut-oauth.ts
src/pages/login/pnutauth.ts
platforms/

View file

@ -3,48 +3,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [0.6.2] -
### Added
- iOS icons
### Fixed
- Paste from clipboard into login fields
### Changed
- Replaced login page and workflow
### Removed
- Cordova platform build
## [0.7.0]
### Fixed
- Sharing post preserves links
- Native android build tree
### Added
- About page
- Profile page
- Follow, mute, and block actions
- Setting to hide images in timeline
- Copy post to clipboard
### Changed
- Image thumbnails are smaller and include title & description
- Icons added to main menu
- Updated to Android cordova 7.1.2
## [0.6.3]
### Added
- Open post in browser window
### Changed
- Embedded images show thumbnail by default
- Tapping or clicking an image opens source up in browser
### Removed
- Unused cordova plugins for keyboard, console, and statusbar
## [0.6.2] - 2018-05-07
### Fixed ### Fixed
- Missing status bar when app is loaded - Missing status bar when app is loaded
@ -126,15 +85,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
### Added ### Added
- Intial pre-release for Android - Intial pre-release for Android
[Unreleased]: https://gitlab.dreamfall.space/thrrgilag/Goober/compare/0.7.0...HEAD [0.6.2]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_6_2
[0.7.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/0.7.0 [0.6.1]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_6_1
[0.6.3]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/0.6.3 [0.6.0]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_6_0
[0.6.2]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_6_2 [0.5.0]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_5_0
[0.6.1]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_6_1 [0.4.0]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_4_0
[0.6.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_6_0 [0.3.0]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_3_0
[0.5.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_5_0 [0.2.0]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_2_0
[0.4.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_4_0 [0.1.1]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_1_1
[0.3.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_3_0 [0.1.0]: https://code.monkeystew.net/thrrgilag/Goober/src/Goober_0_1_0
[0.2.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_2_0
[0.1.1]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_1_1
[0.1.0]: https://gitlab.dreamfall.space/thrrgilag/Goober/tags/Goober_0_1_0

View file

@ -7,29 +7,32 @@ ALIGNED = $(OUT_DIR)/app-release-unsigned-aligned.apk
APK = $(OUT_DIR)/goober.apk APK = $(OUT_DIR)/goober.apk
KEYSTORE = ~/android-keystore.jks KEYSTORE = ~/android-keystore.jks
LOC_ADDR = $(shell hostname -i)
PUB_ADDR = $(shell hostname -I|awk '{print $$1}')
lab: lab:
ionic serve -lc ionic serve -lc
clean: wwwclean device:
ionic cordova run android -lc --address $(PUB_ADDR) --device
wwwclean: emulator:
rm -r www ionic cordova run android -lc --address $(LOC_ADDR) --emulator
distclean: $(UNALIGNED):
rm -r node_modules platforms plugins www ionic cordova build android --release --prod
init: $(ALIGNED): $(UNALIGNED)
npm install cd $(OUT_DIR)
ionic cordova platform add browser $(ANDROID_HOME)/build-tools/*/zipalign -v -p 4 $< $@
ut: $(APK): $(ALIGNED)
npm run ionic:build cd $(OUT_DIR)
rm www/manifest.json $(ANDROID_HOME)/build-tools/*/apksigner sign --ks $(KEYSTORE) --out $@ $<
cd ubuntutouch && clickable ls $@
pwa: release: $(APK)
ionic build --prod
cp resources/*.png www/assets/icon clean:
cp -r www ~/opt/Goober rm $(OUT_DIR)/*.apk

View file

@ -1,12 +1,9 @@
# Goober, a mobile app for pnut.io # Goober, a mobile app for pnut.io
Copyright 2017 - 2018 Morgan McMillian Copyright 2017 Morgan McMillian
Goober is a cross platform mobile client for pnut.io built using the Ionic framework (http://ionicframework.com/). Goober is a cross platform mobile client for pnut.io built using the Ionic framework (http://ionicframework.com/).
## Contributing
**This project is no longer under active development or support. If you decide to fork and continue development let me know. I might transfer this project or link to the new effort whichever is appropriate.**
## LICENSE ## LICENSE
@ -43,6 +40,10 @@ npm install -g ionic cordova
* Install Gradle (needed if you only installed the command line tools) * Install Gradle (needed if you only installed the command line tools)
* https://gradle.org/ * https://gradle.org/
#### Windows
* Install VisualStudio
* https://www.visualstudio.com/
## Other build dependencies ## Other build dependencies
@ -59,9 +60,7 @@ ionic cordova platform add android
``` ```
## Build and run using ionic framework ## Build and run
Copy src/providers/pnut-oauth.ts.sample to src/providers/pnut-oauth.ts and include the client ID provided by pnut.io. See the [developer documentation](https://pnut.io/docs/api/implementation/overview) for more details.
#### Browser #### Browser
```bash ```bash

View file

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<widget id="com.monkeystew.goober_m" version="0.8.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> <widget id="com.monkeystew.goober_m" version="0.6.2" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Goober</name> <name>Goober</name>
<description>Goober, a mobile app for pnut.io</description> <description>Goober, a mobile app for pnut.io</description>
<author email="gilag@monkeystew.com" href="https://monkeystew.org">Morgan McMillian</author> <author email="gilag@monkeystew.com" href="https://monkeystew.org">Morgan McMillian</author>
@ -84,19 +84,22 @@
<icon height="110" src="resources/icon-110.png" width="110" /> <icon height="110" src="resources/icon-110.png" width="110" />
<icon height="144" src="resources/icon-144.png" width="144" /> <icon height="144" src="resources/icon-144.png" width="144" />
</platform> </platform>
<allow-navigation href="http://10.0.0.212:8100" />
<plugin name="cordova-plugin-filechooser" spec="^1.0.1" /> <plugin name="cordova-plugin-filechooser" spec="^1.0.1" />
<plugin name="cordova-plugin-share-content" spec="^1.0.0" /> <plugin name="cordova-plugin-share-content" spec="^1.0.0" />
<plugin name="ionic-plugin-keyboard" spec="^2.2.1" />
<plugin name="cordova-plugin-file" spec="^6.0.1" /> <plugin name="cordova-plugin-file" spec="^6.0.1" />
<plugin name="cordova-plugin-file-transfer" spec="^1.7.1" /> <plugin name="cordova-plugin-file-transfer" spec="^1.7.1" />
<plugin name="com-darryncampbell-cordova-plugin-intent" spec="0.0.19" /> <plugin name="com-darryncampbell-cordova-plugin-intent" spec="0.0.19" />
<plugin name="cordova-plugin-inappbrowser" spec="^2.0.2" /> <plugin name="cordova-plugin-inappbrowser" spec="^2.0.2" />
<plugin name="cordova-plugin-whitelist" spec="^1.3.3" /> <plugin name="cordova-plugin-whitelist" spec="^1.3.3" />
<plugin name="cordova-plugin-statusbar" spec="^2.4.1" />
<plugin name="cordova-plugin-splashscreen" spec="^5.0.2" /> <plugin name="cordova-plugin-splashscreen" spec="^5.0.2" />
<plugin name="cordova-plugin-device" spec="^2.0.1" /> <plugin name="cordova-plugin-device" spec="^2.0.1" />
<plugin name="cordova-plugin-console" spec="^1.1.0" />
<plugin name="cordova-android-support-gradle-release" spec="^1.4.2"> <plugin name="cordova-android-support-gradle-release" spec="^1.4.2">
<variable name="ANDROID_SUPPORT_VERSION" value="27.+" /> <variable name="ANDROID_SUPPORT_VERSION" value="27.+" />
</plugin> </plugin>
<plugin name="cordova-plugin-filepath" spec="^1.3.0" /> <plugin name="cordova-plugin-filepath" spec="^1.3.0" />
<plugin name="cordova-sqlite-storage" spec="2.5.1" /> <engine name="android" spec="7.1.0" />
<allow-navigation href="http://192.168.1.72:8100" />
</widget> </widget>

4793
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,56 +12,58 @@
"ionic:serve": "ionic-app-scripts serve" "ionic:serve": "ionic-app-scripts serve"
}, },
"dependencies": { "dependencies": {
"@angular/common": "5.2.9", "@angular/common": "5.2.5",
"@angular/compiler": "5.2.9", "@angular/compiler": "5.2.5",
"@angular/compiler-cli": "5.2.9", "@angular/compiler-cli": "5.2.5",
"@angular/core": "5.2.9", "@angular/core": "5.2.5",
"@angular/forms": "5.2.9", "@angular/forms": "5.2.5",
"@angular/http": "5.2.9", "@angular/http": "5.2.5",
"@angular/platform-browser": "5.2.9", "@angular/platform-browser": "5.2.5",
"@angular/platform-browser-dynamic": "5.2.9", "@angular/platform-browser-dynamic": "5.2.5",
"@ionic-native/core": "4.9.0", "@ionic-native/core": "4.5.3",
"@ionic-native/device": "4.9.0", "@ionic-native/device": "^4.5.3",
"@ionic-native/file": "4.9.0", "@ionic-native/file": "^4.5.3",
"@ionic-native/file-chooser": "4.9.0", "@ionic-native/file-chooser": "^4.5.3",
"@ionic-native/file-path": "4.9.0", "@ionic-native/file-path": "^4.5.3",
"@ionic-native/file-transfer": "4.9.0", "@ionic-native/file-transfer": "^4.5.3",
"@ionic-native/splash-screen": "4.9.0", "@ionic-native/splash-screen": "4.5.3",
"@ionic-native/status-bar": "4.9.0", "@ionic-native/status-bar": "4.5.3",
"@ionic/storage": "2.1.3", "@ionic/storage": "2.1.3",
"com-darryncampbell-cordova-plugin-intent": "^1.1.1", "com-darryncampbell-cordova-plugin-intent": "0.0.19",
"cordova-android-support-gradle-release": "^1.4.7", "cordova-android": "7.1.0",
"cordova-browser": "6.0.0", "cordova-android-support-gradle-release": "^1.4.2",
"cordova-plugin-console": "^1.1.0",
"cordova-plugin-device": "^2.0.2", "cordova-plugin-device": "^2.0.2",
"cordova-plugin-file": "^6.0.1", "cordova-plugin-file": "^6.0.1",
"cordova-plugin-file-transfer": "^1.7.1", "cordova-plugin-file-transfer": "^1.7.1",
"cordova-plugin-filechooser": "^1.0.1", "cordova-plugin-filechooser": "^1.0.1",
"cordova-plugin-filepath": "^1.4.2", "cordova-plugin-filepath": "^1.3.0",
"cordova-plugin-inappbrowser": "^3.0.0", "cordova-plugin-inappbrowser": "^2.0.2",
"cordova-plugin-share-content": "^1.0.0", "cordova-plugin-share-content": "^1.0.0",
"cordova-plugin-splashscreen": "^5.0.2", "cordova-plugin-splashscreen": "^5.0.2",
"cordova-plugin-statusbar": "^2.4.2",
"cordova-plugin-telerik-imagepicker": "^2.1.8", "cordova-plugin-telerik-imagepicker": "^2.1.8",
"cordova-plugin-whitelist": "^1.3.3", "cordova-plugin-whitelist": "^1.3.3",
"cordova-sqlite-storage": "^2.5.1",
"ionic-angular": "3.9.2", "ionic-angular": "3.9.2",
"ionicons": "4.2.4", "ionic-plugin-keyboard": "^2.2.1",
"ionicons": "3.0.0",
"moment": "^2.18.1", "moment": "^2.18.1",
"ng2-cordova-oauth": "0.0.8", "ng2-cordova-oauth": "0.0.8",
"ngx-clipboard": "^11.1.9", "pnut-butter": "^0.19.0",
"pnut-butter": "^0.21.0",
"run": "^1.4.0", "run": "^1.4.0",
"rxjs": "5.5.8", "rxjs": "5.5.6",
"sw-toolbox": "3.6.0", "sw-toolbox": "3.6.0",
"zone.js": "0.8.26" "zone.js": "0.8.20"
}, },
"devDependencies": { "devDependencies": {
"@ionic/app-scripts": "3.1.10", "@ionic/app-scripts": "3.1.8",
"ionic": "3.20.0", "ionic": "3.20.0",
"typescript": "2.8.3" "typescript": "2.7.2"
}, },
"description": "An Ionic project", "description": "An Ionic project",
"cordova": { "cordova": {
"plugins": { "plugins": {
"ionic-plugin-keyboard": {},
"cordova-plugin-share-content": {}, "cordova-plugin-share-content": {},
"cordova-plugin-filechooser": {}, "cordova-plugin-filechooser": {},
"cordova-plugin-file": {}, "cordova-plugin-file": {},
@ -69,16 +71,17 @@
"com-darryncampbell-cordova-plugin-intent": {}, "com-darryncampbell-cordova-plugin-intent": {},
"cordova-plugin-inappbrowser": {}, "cordova-plugin-inappbrowser": {},
"cordova-plugin-whitelist": {}, "cordova-plugin-whitelist": {},
"cordova-plugin-statusbar": {},
"cordova-plugin-splashscreen": {}, "cordova-plugin-splashscreen": {},
"cordova-plugin-device": {}, "cordova-plugin-device": {},
"cordova-plugin-console": {},
"cordova-android-support-gradle-release": { "cordova-android-support-gradle-release": {
"ANDROID_SUPPORT_VERSION": "27.+" "ANDROID_SUPPORT_VERSION": "27.+"
}, },
"cordova-plugin-filepath": {}, "cordova-plugin-filepath": {}
"cordova-sqlite-storage": {}
}, },
"platforms": [ "platforms": [
"browser" "android"
] ]
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,5 +1,6 @@
import { Component, ViewChild } from '@angular/core'; import { Component, ViewChild } from '@angular/core';
import { Nav, Platform } from 'ionic-angular'; import { Nav, Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen'; import { SplashScreen } from '@ionic-native/splash-screen';
import { Storage } from '@ionic/storage'; import { Storage } from '@ionic/storage';
import { Device } from '@ionic-native/device'; import { Device } from '@ionic-native/device';
@ -7,11 +8,6 @@ import { Device } from '@ionic-native/device';
import { LoginPage } from '../pages/login/login'; import { LoginPage } from '../pages/login/login';
import { StreamPage } from '../pages/stream/stream'; import { StreamPage } from '../pages/stream/stream';
import { SettingsPage } from '../pages/settings/settings'; import { SettingsPage } from '../pages/settings/settings';
import { AboutPage } from '../pages/about/about';
import { ProfilePage } from '../pages/profile/profile';
import { pnutauth } from '../pages/login/pnutauth';
import { IUser } from '../models/IUser';
import * as pnut from 'pnut-butter'; import * as pnut from 'pnut-butter';
@ -22,23 +18,23 @@ export class MyApp {
@ViewChild(Nav) nav: Nav; @ViewChild(Nav) nav: Nav;
rootPage: any = StreamPage; rootPage: any = StreamPage;
pages: Array<{title: string, icon: string, component: any, params: Object}>;
profile: IUser;
constructor(public platform: Platform, public splashScreen: SplashScreen, pages: Array<{title: string, component: any, params: Object}>;
scope: Array<string> = ['basic','stream','write_post','files'];
constructor(public platform: Platform, public statusBar: StatusBar, public splashScreen: SplashScreen,
private storage: Storage, private device: Device) { private storage: Storage, private device: Device) {
this.initializeApp(); this.initializeApp();
// used for an example of ngFor and navigation // used for an example of ngFor and navigation
this.pages = [ this.pages = [
{ title: 'Timeline', icon: 'home', component: StreamPage, params: {stream: 'personal'} }, { title: 'Timeline', component: StreamPage, params: {stream: 'personal'} },
{ title: 'Mentions', icon: 'at', component: StreamPage, params: {stream: 'mentions'} }, { title: 'Mentions', component: StreamPage, params: {stream: 'mentions'} },
{ title: 'Global', icon: 'globe', component: StreamPage, params: {stream: 'global'} }, { title: 'Global', component: StreamPage, params: {stream: 'global'} },
{ title: 'Bookmarks', icon: 'bookmarks', component: StreamPage, params: {stream: 'bookmarks'} }, { title: 'Bookmarks', component: StreamPage, params: {stream: 'bookmarks'} },
{ title: 'Profile', icon: 'person', component: ProfilePage, params: {}}, { title: 'Settings', component: SettingsPage, params: {}},
{ title: 'Settings', icon: 'settings', component: SettingsPage, params: {}}, { title: 'Logout', component: {}, params: {}},
{ title: 'About', icon: 'information-circle', component: AboutPage, params: {}},
{ title: 'Logout', icon: 'exit', component: {}, params: {}},
]; ];
} }
@ -46,6 +42,7 @@ export class MyApp {
initializeApp() { initializeApp() {
console.log('--- initializeApp ---'); console.log('--- initializeApp ---');
this.platform.ready().then(() => { this.platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available. // Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need. // Here you can do any higher level native things you might need.
@ -60,6 +57,7 @@ export class MyApp {
this.initialPage('personal'); this.initialPage('personal');
}); });
this.statusBar.styleLightContent();
this.splashScreen.hide(); this.splashScreen.hide();
// console.log('---'); // console.log('---');
// console.log(this.device.platform); // console.log(this.device.platform);
@ -73,37 +71,31 @@ export class MyApp {
pnut.token = val; pnut.token = val;
this.storage.get('scope').then((sval) => { this.storage.get('scope').then((sval) => {
if (JSON.stringify(sval) !== JSON.stringify(pnutauth.scope)) { if (JSON.stringify(sval) !== JSON.stringify(this.scope)) {
this.nav.setRoot(LoginPage, {}); this.nav.setRoot(LoginPage, {'scope': this.scope});
} else { } else {
this.nav.setRoot(StreamPage, {stream: timeline}); this.nav.setRoot(StreamPage, {stream: timeline});
} }
// this.nav.setRoot(StreamPage, {stream: timeline}); // this.nav.setRoot(StreamPage, {stream: timeline});
}).catch(err => { }).catch(err => {
this.nav.setRoot(LoginPage, {}); this.nav.setRoot(LoginPage, {'scope': this.scope});
}); });
// this.nav.setRoot(StreamPage, {stream: timeline}); // this.nav.setRoot(StreamPage, {stream: timeline});
} }
}).catch(err => { }).catch(err => {
console.log('ERROR: ' + err); console.log('ERROR: ' + err);
this.nav.setRoot(LoginPage, {}); this.nav.setRoot(LoginPage, {'scope': this.scope});
}); });
} }
async openPage(page) { openPage(page) {
// Reset the content nav to have just this page // Reset the content nav to have just this page
// we wouldn't want the back button to show in this scenario // we wouldn't want the back button to show in this scenario
if (page.title === 'Logout') { if (page.title === 'Logout') {
// this.storage.remove('token'); // this.storage.remove('token');
this.storage.clear(); this.storage.clear();
this.nav.setRoot(LoginPage, {}); this.nav.setRoot(LoginPage, {'scope': this.scope});
} else if (page.title === 'Settings' || page.title === 'About') { } else if (page.title === 'Settings') {
this.nav.push(page.component, page.params);
} else if (page.title === 'Profile') {
await pnut.user('me').then(res => {
this.profile = res.data as IUser;
});
page.params = {user: this.profile, me: this.profile.username};
this.nav.push(page.component, page.params); this.nav.push(page.component, page.params);
} else { } else {
this.nav.setRoot(page.component, page.params); this.nav.setRoot(page.component, page.params);

View file

@ -8,8 +8,7 @@
<ion-content> <ion-content>
<ion-list> <ion-list>
<button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)"> <button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)">
<ion-icon name="{{p.icon}}"></ion-icon> {{p.title}}
<span class="menuText">{{p.title}}</span>
</button> </button>
</ion-list> </ion-list>
</ion-content> </ion-content>
@ -17,4 +16,4 @@
</ion-menu> </ion-menu>
<!-- Disable swipe-to-go-back because it's poor UX to combine STGB with side menus --> <!-- Disable swipe-to-go-back because it's poor UX to combine STGB with side menus -->
<ion-nav [root]="rootPage" #content swipeBackEnabled="false"></ion-nav> <ion-nav [root]="rootPage" #content swipeBackEnabled="false"></ion-nav>

View file

@ -1,21 +1,14 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core'; import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular'; import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { HttpClientModule } from '@angular/common/http';
import { MyApp } from './app.component'; import { MyApp } from './app.component';
import { LoginPage } from '../pages/login/login'; import { LoginPage } from '../pages/login/login';
import { StreamPage } from '../pages/stream/stream'; import { StreamPage, NewPostModal, PostMenu } from '../pages/stream/stream';
import { PostMenu } from '../pages/stream/post-menu';
import { NewPostModal } from '../pages/stream/new-post';
import { ThreadPage } from '../pages/thread/thread'; import { ThreadPage } from '../pages/thread/thread';
import { SettingsPage } from '../pages/settings/settings'; import { SettingsPage } from '../pages/settings/settings';
import { AboutPage } from '../pages/about/about';
import { ProfilePage } from '../pages/profile/profile';
import { ProfileMenu } from '../pages/profile/profile-menu';
import { UserListPage } from '../pages/user-list/user-list';
import { PostComponent } from '../components/post/post';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen'; import { SplashScreen } from '@ionic-native/splash-screen';
import { IonicStorageModule } from '@ionic/storage'; import { IonicStorageModule } from '@ionic/storage';
import { Device } from '@ionic-native/device'; import { Device } from '@ionic-native/device';
@ -24,7 +17,6 @@ import { FilePath } from '@ionic-native/file-path';
import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer'; import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer';
import { TimeagoPipe } from '../pipes/timeago/timeago'; import { TimeagoPipe } from '../pipes/timeago/timeago';
import { ParserPipe } from '../pipes/parser/parser'; import { ParserPipe } from '../pipes/parser/parser';
import { ClipboardModule } from 'ngx-clipboard';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -33,22 +25,15 @@ import { ClipboardModule } from 'ngx-clipboard';
StreamPage, StreamPage,
ThreadPage, ThreadPage,
SettingsPage, SettingsPage,
AboutPage,
ProfilePage,
UserListPage,
TimeagoPipe, TimeagoPipe,
NewPostModal, NewPostModal,
PostMenu, PostMenu,
ProfileMenu, ParserPipe
ParserPipe,
PostComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
HttpClientModule,
IonicModule.forRoot(MyApp), IonicModule.forRoot(MyApp),
IonicStorageModule.forRoot(), IonicStorageModule.forRoot(),
ClipboardModule,
], ],
bootstrap: [IonicApp], bootstrap: [IonicApp],
entryComponents: [ entryComponents: [
@ -57,14 +42,11 @@ import { ClipboardModule } from 'ngx-clipboard';
StreamPage, StreamPage,
ThreadPage, ThreadPage,
SettingsPage, SettingsPage,
AboutPage,
ProfilePage,
UserListPage,
NewPostModal, NewPostModal,
PostMenu, PostMenu
ProfileMenu
], ],
providers: [ providers: [
StatusBar,
SplashScreen, SplashScreen,
Device, Device,
FileChooser, FileChooser,

View file

@ -14,8 +14,3 @@
// To declare rules for a specific mode, create a child rule // To declare rules for a specific mode, create a child rule
// for the .md, .ios, or .wp mode classes. The mode class is // for the .md, .ios, or .wp mode classes. The mode class is
// automatically applied to the <body> element in the app. // automatically applied to the <body> element in the app.
.menuText {
left: 45px;
position: absolute;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,95 +0,0 @@
<ion-item color="{{ post.you_are_mentioned ? 'mention' : '' }}" (click)="showProfile(post.user)">
<ion-avatar item-start >
<img src="{{ post.user.content.avatar_image.link }}">
</ion-avatar>
<h2>{{ post.user.name }}</h2>
<p>@{{ post.user.username }}</p>
<ion-note item-end>
<div text-right>
{{ post.created_at | timeago }}<br/>
{{ post.source.name }}
</div>
</ion-note>
</ion-item>
<ion-card-content>
<div *ngIf="post.is_deleted; else renderBlock"></div>
<ng-template #renderBlock >
<div [innerHTML]="post.content.html | parser"></div>
<div *ngIf="post.raw">
<div *ngFor="let r of post.raw">
<div *ngIf="r.type == 'nl.chimpnut.blog.post'">
<hr>
<div>{{ r.value.body }}</div>
</div>
</div>
</div>
</ng-template>
</ion-card-content>
<div *ngIf="post.raw">
<ion-list *ngFor="let r of post.raw">
<div *ngIf="r.type == 'io.pnut.core.oembed'">
<div *ngIf="hideImg; then hidebtn else thumbbtn"></div>
<ng-template #thumbbtn>
<ion-item>
<ion-thumbnail item-start>
<img src="{{ r.value.thumbnail_url || r.value.url }}" (click)="showImage(r.value.url)">
</ion-thumbnail>
<h2>{{ r.value.title }}</h2>
<p>{{ r.value.description }}</p>
</ion-item>
</ng-template>
<ng-template #hidebtn>
<ion-item>
<button ion-button icon-start (click)="showImage(r.value.url)">
<ion-icon name="image"></ion-icon>
{{ r.value.title }}
</button>
<p>{{ r.value.description }}</p>
</ion-item>
</ng-template>
</div>
</ion-list>
</div>
<div *ngIf="post.reposted_by_string">
<ion-item><ion-note>{{ post.reposted_by_string }}</ion-note></ion-item>
</div>
<ion-row>
<ion-col>
<button ion-button icon-left clear small block (click)="showReplyPost(post,'reply')">
<ion-icon name="ios-undo"></ion-icon>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="showReplyPost(post,'quote')">
<ion-icon name="quote"></ion-icon>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="repost(post.id, post.you_reposted)">
<ion-icon name="repeat"></ion-icon>
<div *ngIf="post.counts.reposts > 0">{{ post.counts.reposts }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="bookmark(post.id, post.you_bookmarked)">
<ion-icon name="star"></ion-icon>
<div *ngIf="post.counts.bookmarks > 0">{{ post.counts.bookmarks }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="fetchThread(post.thread_id)">
<ion-icon name="chatboxes"></ion-icon>
<div *ngIf="post.counts.replies > 0">{{ post.counts.replies }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="presentPostMenu($event, post)">
<ion-icon name="more"></ion-icon>
</button>
</ion-col>
</ion-row>

View file

@ -1,14 +0,0 @@
post {
.item-md ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
.item-wp ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
.item-ios ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
}

View file

@ -1,120 +0,0 @@
import { Component, Input } from '@angular/core';
import { NavController, NavParams, ModalController, ToastController, PopoverController } from 'ionic-angular';
import { ProfilePage } from '../../pages/profile/profile';
import { ThreadPage } from '../../pages/thread/thread';
import { LoginPage } from '../../pages/login/login';
import { NewPostModal } from '../../pages/stream/new-post';
import { PostMenu } from '../../pages/stream/post-menu';
import * as pnut from 'pnut-butter';
/**
* Generated class for the PostComponent component.
*
* See https://angular.io/api/core/Component for more info on Angular
* Components.
*/
@Component({
selector: 'post',
templateUrl: 'post.html'
})
export class PostComponent {
@Input() public post: Object;
@Input() public hideImg: boolean;
@Input() public ccOnReply: boolean;
@Input() public myUsername: string;
constructor(public navCtrl: NavController, public navParams: NavParams,
public modalCtrl: ModalController, public toastCtrl: ToastController,
public popoverCtrl: PopoverController) {}
fetchThread(threadid) {
pnut.thread(threadid, {include_deleted: 0, include_raw: 1, count: 140}).then(res => {
if (res.meta.code === 401) {
// this.storage.clear();
this.navCtrl.setRoot(LoginPage);
} else {
this.navCtrl.push(ThreadPage, {posts: res.data, me: this.myUsername});
}
});
}
showImage(url) {
window.open(url, '_system');
}
showProfile(user) {
this.navCtrl.push(ProfilePage, {user: user, me: this.myUsername});
}
showReplyPost(postData, repType) {
let newPostModal = this.modalCtrl.create(NewPostModal, {
type: repType,
post: postData,
me: this.myUsername,
cc: this.ccOnReply});
newPostModal.present();
}
repost(postid, reposted) {
if (reposted) {
pnut.deleteRepost(postid).then(res => {
this.updatePost(res.data.id);
this.presentToast("Repost updated.");
}).catch(err => {
console.log(err);
});
} else {
pnut.repost(postid).then(res => {
this.updatePost(res.data.id);
this.presentToast("Repost updated.");
}).catch(err => {
console.log(err);
});
}
}
bookmark(postid, bookmarked) {
if (bookmarked) {
pnut.deleteBookmark(postid).then(res => {
this.updatePost(res.data.id);
this.presentToast("Bookmark updated.");
}).catch(err => {
console.log(err);
});
} else {
pnut.bookmark(postid).then(res => {
this.updatePost(res.data.id);
this.presentToast("Bookmark updated.");
}).catch(err => {
console.log(err);
});
}
}
updatePost(postid) {
pnut.post(postid, {include_raw: 1}).then(res => {
this.post = res.data;
}).catch(err => {
console.log(err);
});
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
presentPostMenu(myEvent, postData) {
let popover = this.popoverCtrl.create(PostMenu, {
post: postData,
me: this.myUsername});
popover.present({ev: myEvent});
}
}

View file

@ -2,31 +2,26 @@
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Goober</title> <title>Ionic App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no"> <meta name="msapplication-tap-highlight" content="no">
<link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico"> <link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico">
<link rel="apple-touch-icon" href="assets/icon/icon-iphone.png">
<link rel="apple-touch-icon" sizes="152x152" href="assets/icon/icon-ipad.png">
<link rel="apple-touch-icon" sizes="180x180" href="assets/icon/icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="167x167" href="assets/icon/icon-ipad-retina.png">
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#4e8ef7"> <meta name="theme-color" content="#4e8ef7">
<!-- cordova.js required for cordova apps --> <!-- cordova.js required for cordova apps -->
<script src="cordova.js"></script> <script src="cordova.js"></script>
<!-- un-comment this code to enable service worker
<script> <script>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js') navigator.serviceWorker.register('service-worker.js')
.then(() => console.log('service worker installed')) .then(() => console.log('service worker installed'))
.catch(err => console.error('Error', err)); .catch(err => console.error('Error', err));
} }
</script> </script>-->
<link href="build/main.css" rel="stylesheet"> <link href="build/main.css" rel="stylesheet">

View file

@ -1,13 +1,13 @@
{ {
"name": "Goober", "name": "Ionic",
"short_name": "Goober", "short_name": "Ionic",
"start_url": "index.html", "start_url": "index.html",
"display": "standalone", "display": "standalone",
"icons": [{ "icons": [{
"src": "assets/icon.png", "src": "assets/imgs/logo.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
}], }],
"background_color": "#4e8ef7", "background_color": "#4e8ef7",
"theme_color": "#4e8ef7" "theme_color": "#4e8ef7"
} }

View file

@ -1,33 +0,0 @@
import { IUserContent } from './user/IUserContent';
export interface IUser {
badge?: {
id: string,
name: string;
};
content: IUserContent;
counts: {
bookmarks: number,
clients: number,
followers: number,
following: number,
posts: number,
users: number;
};
created_at: string;
follows_you: boolean;
id: string;
locale: string;
name: string;
timezone: string;
type: string;
username: string;
you_blocked: boolean;
you_can_follow: boolean;
you_follow: boolean;
you_muted: boolean;
verified: {
domain: string,
link: string;
};
}

View file

@ -1,6 +0,0 @@
export interface IUserAvatarImage {
is_default: boolean;
height: number;
link: string;
width: number;
}

View file

@ -1,11 +0,0 @@
import { IUserAvatarImage } from './IUserAvatarImage';
import { IUserCoverImage } from './IUserCoverImage';
export interface IUserContent {
avatar_image: IUserAvatarImage;
cover_image: IUserCoverImage;
entities?: {};
html: string;
markdown_text: string;
text: string;
}

View file

@ -1,6 +0,0 @@
export interface IUserCoverImage {
link: string;
is_default: boolean;
width: number;
heigth: number;
}

View file

@ -1,43 +0,0 @@
<!--
Generated template for the AboutPage page.
See http://ionicframework.com/docs/components/#navigation for more info on
Ionic pages and navigation.
-->
<ion-header>
<ion-navbar>
<ion-title>About</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-row center>
<ion-col text-center>
<img src="assets/icon/icon.png" height="64" width="64">
</ion-col>
</ion-row>
<ion-row center>
<ion-col text-center>
<h1>Goober {{ version }}</h1>
A mobile client for <a href="https://pnut.io" target="_system">pnut.io</a>.
</ion-col>
</ion-row>
<ion-row center>
<ion-col text-center>
<p>made by Morgan McMillian (<a href="https://pnut.io/@thrrgilag" target="_system">@thrrgilag</a>).</p>
<p>Goober is free and open source software licensed under the
Apache License 2.0.</p>
<p><a href="http://www.apache.org/licenses/LICENSE-2.0" target="_system">
http://www.apache.org/licenses/LICENSE-2.0</a></p>
<p>&nbsp;</p>
<p><button ion-button (click)="browse('https://gitlab.dreamfall.space/thrrgilag/Goober/wikis/home')">Project Site</button></p>
</ion-col>
</ion-row>
</ion-content>

View file

@ -1,3 +0,0 @@
page-about {
}

View file

@ -1,30 +0,0 @@
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
/**
* Generated class for the AboutPage page.
*
* See https://ionicframework.com/docs/components/#navigation for more info on
* Ionic pages and navigation.
*/
@Component({
selector: 'page-about',
templateUrl: 'about.html',
})
export class AboutPage {
private version: string = '0.8.0';
constructor(public navCtrl: NavController, public navParams: NavParams) {
}
ionViewDidLoad() {
console.log('ionViewDidLoad AboutPage');
}
browse(url) {
window.open(url, '_system');
}
}

View file

@ -20,12 +20,17 @@
<h2>Goober</h2> <h2>Goober</h2>
<p block>A mobile client for pnut.io</p> <p block>A mobile client for pnut.io</p>
</div><p>&nbsp;</p> </div><p>&nbsp;</p>
<div *ngIf="!showToken">
<ion-label stacked>Username</ion-label> <p>Tap the Log In button to open browser window and enter your pnut.io creditionals and authorize Goober.</p>
<ion-input [(ngModel)]="username" type="text"></ion-input> <div *ngIf="oob">
<ion-label stacked>Password</ion-label> <p>Afterwards, copy the token provided, close the pop up window, and paste the token into the input field shown and tap Save Token.</p>
<ion-input [(ngModel)]="password" type="password"></ion-input> </div>
<button ion-button block (click)="login()">Log In</button><p>
<button ion-button block (click)="login()">Log In</button> </div>
<div *ngIf="showToken">
<p>Paste the token provided into this field and then tap Save Token.</p>
<ion-input [(ngModel)]="token" type="text" placeholder="Token"></ion-input>
<p><button ion-button block (click)="saveToken()">Save Token</button></p>
</div>
</ion-content> </ion-content>

View file

@ -1,3 +1,3 @@
page-login { page-login {
user-select: auto !important;
} }

View file

@ -1,54 +1,81 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular'; import { NavController, NavParams } from 'ionic-angular';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Storage } from '@ionic/storage'; import { Storage } from '@ionic/storage';
import { Device } from '@ionic-native/device';
import { StreamPage } from '../stream/stream'; import { StreamPage } from '../stream/stream';
import { pnutauth } from './pnutauth';
import { OauthCordova } from 'ng2-cordova-oauth/platform/cordova';
import { OauthBrowser } from 'ng2-cordova-oauth/platform/browser';
import { PnutAuth } from '../../providers/pnut-oauth';
import * as pnut from 'pnut-butter'; import * as pnut from 'pnut-butter';
/**
* Generated class for the LoginPage page.
*
* See http://ionicframework.com/docs/components/#navigation for more info
* on Ionic pages and navigation.
*/
@Component({ @Component({
selector: 'page-login', selector: 'page-login',
templateUrl: 'login.html', templateUrl: 'login.html',
}) })
export class LoginPage { export class LoginPage {
private username: string; private oauth: any;
private password: string; private pnutProvider: any;
private oob: boolean = false;
public showToken: boolean = false;
private token: string;
private scope: Array<string> = [];
constructor(public navCtrl: NavController, public navParams: NavParams, constructor(public navCtrl: NavController, public navParams: NavParams, private storage: Storage,
private storage: Storage, private http: HttpClient) { private device: Device) {
this.scope = navParams.data.scope;
if (this.device.platform === "Android" || this.device.platform === "amazon-fireos") {
this.oauth = new OauthCordova();
this.pnutProvider = new PnutAuth({
appScope: this.scope,
redirectUri: 'http://localhost/callback'
});
} else if (this.device.platform === "blackberry10") {
this.oauth = new OauthBrowser();
this.pnutProvider = new PnutAuth({
appScope: this.scope,
redirectUri: 'https://zoidberg.monkeystew.net/'
});
} else {
this.oauth = new OauthBrowser();
this.pnutProvider = new PnutAuth({
appScope: this.scope,
redirectUri: 'urn:ietf:wg:oauth:2.0:oob'
});
this.oob = true;
}
} }
login() { login() {
this.oauth.logInVia(this.pnutProvider).then(success => {
interface LoginResponse { console.log('RESULT: ' + JSON.stringify(success));
access_token: string; this.storage.set('token', success['access_token']);
} this.storage.set('scope', this.scope);
pnut.token = success['access_token'];
let headers = new HttpHeaders() this.navCtrl.setRoot(StreamPage, {stream: 'personal'});
.set('Content-Type', 'application/x-www-form-urlencoded'); }, error => {
console.log(error);
let params = new HttpParams() if (this.oob) {
.set('client_id', pnutauth.clientId) this.showToken = true;
.set('password_grant_secret', pnutauth.clientSecret) }
.set('username', this.username)
.set('password', this.password)
.set('grant_type', 'password')
.set('scope', pnutauth.scope);
this.http.post<LoginResponse>(pnutauth.url, params, {headers: headers}).subscribe(res => {
console.log('authorized');
this.storage.set('scope', pnutauth.scope);
this.storage.set('token', res.access_token);
pnut.token = res.access_token;
this.navCtrl.setRoot(StreamPage, {stream: "personal"});
}, err => {
console.log("error: " + JSON.stringify(err));
}); });
}
saveToken() {
this.storage.set('scope', this.scope);
this.storage.set('token', this.token);
pnut.token = this.token;
this.navCtrl.setRoot(StreamPage, {stream: 'personal'});
} }
} }

View file

@ -1,6 +0,0 @@
export const pnutauth = {
url: "https://api.pnut.io/v0/oauth/access_token",
scope: "basic,stream,write_post,files",
clientId: "",
clientSecret: ""
}

View file

@ -1,76 +0,0 @@
import { Component } from '@angular/core';
import { ViewController, NavParams, ToastController } from 'ionic-angular';
import { Events } from 'ionic-angular';
import * as pnut from 'pnut-butter';
@Component({
template: `
<ion-list>
<button ion-item (click)="browse()">Open in browser</button>
<button ion-item [disabled]="myUsername == username">Block</button>
<button ion-item [disabled]="myUsername == username">Mute</button>
</ion-list>
`
})
export class ProfileMenu {
private userid: string;
private username: string;
private you_muted: boolean;
private you_blocked: boolean;
private myUsername: string;
constructor(public navParams: NavParams, public viewCtrl: ViewController,
public toastCtrl: ToastController, public events: Events) {
this.userid = this.navParams.data.userid;
this.username = this.navParams.data.username;
this.you_muted = this.navParams.data.you_muted;
this.you_blocked = this.navParams.data.you_blocked;
this.myUsername = this.navParams.data.me;
}
browse() {
window.open('https://pnut.io/@' + this.username, '_system');
this.close();
}
mute() {
if (this.you_muted) {
pnut.unmute(this.userid).then(res => {
this.presentToast('User unmuted');
});
} else {
pnut.mute(this.userid).then(res => {
this.presentToast('User muted');
});
}
this.close();
}
block() {
if (this.you_blocked) {
pnut.unblock(this.userid).then(res => {
this.presentToast('User unblocked');
});
} else {
pnut.block(this.userid).then(res => {
this.presentToast('User blocked');
});
}
this.close();
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
close() {
this.viewCtrl.dismiss();
}
}

View file

@ -1,86 +0,0 @@
<!--
Generated template for the ProfilePage page.
See http://ionicframework.com/docs/components/#navigation for more info on
Ionic pages and navigation.
-->
<ion-header>
<ion-navbar>
<ion-title>{{ user.username }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="presentProfileMenu($event)">
<ion-icon name="more"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<img src="{{ user.content.cover_image.link }}">
<ion-item>
<ion-avatar item-start>
<img src="{{ user.content.avatar_image.link }}">
</ion-avatar>
<h2>{{ user.name }}</h2>
<p>@{{ user.username }}</p>
<ion-col item-end text-right>
<button ion-button [disabled]="myUsername == user.username" (click)="followUser()">{{ user.you_follow ? "Unfollow" : "Follow" }}</button>
<ion-note>{{ user.follows_you ? "Follows you" : ""}}</ion-note>
</ion-col>
</ion-item>
<div padding [innerHTML]="user.content.html | parser"></div>
<ion-row padding>
<ion-col>
<ion-row>
<button ion-button full clear>{{ user.counts.posts }}<br/>posts</button>
</ion-row>
<ion-row>
<button ion-button full (click)="showUserList('Followers')">{{ user.counts.followers }}<br/>followers</button>
</ion-row>
</ion-col>
<ion-col>
<ion-row>
<button ion-button full clear>{{ user.counts.bookmarks }}<br/>stars</button>
</ion-row>
<ion-row>
<button ion-button full (click)="showUserList('Following')">{{ user.counts.following }}<br/>following</button>
</ion-row>
</ion-col>
</ion-row>
<ion-segment [(ngModel)]="activeTab">
<ion-segment-button value="posts">Posts</ion-segment-button>
<ion-segment-button value="bookmarks">Stars</ion-segment-button>
</ion-segment>
<div [ngSwitch]="activeTab">
<div *ngSwitchCase="'posts'">
<ion-list>
<ion-card *ngFor="let post of posts" color="{{ post.you_are_mentioned ? 'mention' : '' }}">
<post [post]="post" [hideImg]="hideImg" [ccOnReply]="ccOnReply" [myUsername]="myUsername"></post>
</ion-card>
</ion-list>
</div>
<div *ngSwitchCase="'bookmarks'">
<ion-list>
<ion-card *ngFor="let post of bookmarks" color="{{ post.you_are_mentioned ? 'mention' : '' }}">
<post [post]="post" [hideImg]="hideImg" [ccOnReply]="ccOnReply" [myUsername]="myUsername"></post>
</ion-card>
</ion-list>
</div>
<ion-infinite-scroll (ionInfinite)="fetchOlderPosts($event, activeTab)">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</div>
</ion-content>

View file

@ -1,14 +0,0 @@
page-profile {
.item-md ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
.item-wp ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
.item-ios ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
}

View file

@ -1,159 +0,0 @@
import { Component } from '@angular/core';
import { NavController, NavParams, PopoverController, ToastController } from 'ionic-angular';
import { UserListPage } from '../user-list/user-list';
import { ProfileMenu } from './profile-menu'
import { IUser } from '../../models/IUser';
import * as pnut from 'pnut-butter';
/**
* Generated class for the ProfilePage page.
*
* See https://ionicframework.com/docs/components/#navigation for more info on
* Ionic pages and navigation.
*/
@Component({
selector: 'page-profile',
templateUrl: 'profile.html',
})
export class ProfilePage {
private user: IUser;
private posts: Array<Object> = [];
private bookmarks: Array<Object> = [];
private before_id_post: string;
private before_id_stars: string;
private myUsername: string;
public activeTab: string = 'posts';
constructor(public navCtrl: NavController, public navParams: NavParams,
public popoverCtrl: PopoverController, public toastCtrl: ToastController) {
if (this.navParams.data.user) {
this.user = this.navParams.data.user;
this.myUsername = this.navParams.data.me;
console.log('user: ' + this.user.username);
console.log('me: ' + this.myUsername);
} else {
console.log('err, I need user data!!!');
}
}
ionViewDidLoad() {
console.log('ionViewDidLoad ProfilePage');
let params = {
include_deleted: 0,
include_raw: 1,
count: 40
};
pnut.postsFrom(this.user.id, params).then(res => {
this.posts = this.parseData(res.data);
this.before_id_post = res.meta.min_id;
});
pnut.bookmarks(this.user.id, params).then(res => {
this.bookmarks = this.parseData(res.data);
this.before_id_stars = res.meta.min_id;
});
}
fetchOlderPosts(infiniteScroll, stream) {
let before_id = this.before_id_post;
let fetcher = pnut.postsFrom;
if (stream === 'bookmarks') {
before_id = this.before_id_stars;
fetcher = pnut.bookmarks;
}
let params = {
include_deleted: 0,
include_raw: 1,
before_id: before_id,
count: 40
};
fetcher(this.user.id, params).then(res => {
if (res.data.length > 0) {
if (stream === 'posts') {
this.posts.push.apply(this.posts, this.parseData(res.data));
this.before_id_post = res.meta.min_id;
} else if (stream === 'bookmarks') {
this.bookmarks.push.apply(this.bookmarks, this.parseData(res.data));
this.before_id_stars = res.meta.min_id;
}
}
infiniteScroll.complete();
}).catch(err => {
console.log(err);
});
}
parseData(data) {
var pdata = [];
for (var i = 0; i < data.length; i++) {
if (!data[i].is_deleted) {
if (data[i]['repost_of']) {
data[i] = data[i]['repost_of']
var reposted_by_string = "";
let rplen = 0;
if (typeof data[i]['reposted_by'] !== "undefined") {
rplen = data[i]['reposted_by'].length;
}
for (var j = 0; j < rplen; j++) {
reposted_by_string = reposted_by_string + data[i]['reposted_by'][j]['username'] + ", ";
}
}
if (data[i].content) {
for (var k = 0; k < data[i]['content']['entities']['mentions'].length; k++) {
var men = data[i]['content']['entities']['mentions'][k]['text'];
if (this.myUsername === men) {
data[i]['you_are_mentioned'] = true;
}
}
}
pdata.push(data[i]);
}
}
return pdata;
}
showUserList(list) {
this.navCtrl.push(UserListPage, {
userid: this.user.id,
username: this.myUsername,
list: list});
}
followUser() {
if (this.user.you_follow) {
pnut.unfollow(this.user.id).then(res => {
this.user = res.data;
this.presentToast('User unfollowed');
});
} else {
pnut.follow(this.user.id).then(res => {
this.user = res.data;
this.presentToast('User followed');
});
}
}
presentProfileMenu(myEvent) {
let popover = this.popoverCtrl.create(ProfileMenu, {
me: this.myUsername,
userid: this.user.id,
username: this.user.username,
you_muted: this.user.you_muted,
you_blocked: this.user.you_blocked});
popover.present({ev: myEvent});
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
}

View file

@ -40,13 +40,5 @@
<ion-toggle [(ngModel)]="set_cc" (ionChange)="updateCc()"></ion-toggle> <ion-toggle [(ngModel)]="set_cc" (ionChange)="updateCc()"></ion-toggle>
</ion-item> </ion-item>
<ion-item>
<ion-label>
<h2>Hide images</h2>
<p>Hide images in posts</p>
</ion-label>
<ion-toggle [(ngModel)]="set_hideimg" (ionChange)="updateHideImg()"></ion-toggle>
</ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>

View file

@ -18,7 +18,6 @@ export class SettingsPage {
private set_unified: boolean; private set_unified: boolean;
private set_cc: boolean; private set_cc: boolean;
private set_default: string; private set_default: string;
private set_hideimg: boolean;
constructor(public navCtrl: NavController, private storage: Storage, public navParams: NavParams, constructor(public navCtrl: NavController, private storage: Storage, public navParams: NavParams,
public events: Events) { public events: Events) {
@ -42,12 +41,6 @@ export class SettingsPage {
}).catch(err => { }).catch(err => {
console.log('ERROR: ' + err); console.log('ERROR: ' + err);
}); });
this.storage.get('hideimg').then((val) => {
this.set_hideimg = val;
}).catch(err => {
console.log('ERROR: ' + err);
});
} }
ionViewDidLeave() { ionViewDidLeave() {
@ -66,8 +59,4 @@ export class SettingsPage {
this.storage.set('timeline', this.set_default); this.storage.set('timeline', this.set_default);
} }
updateHideImg() {
this.storage.set('hideimg', this.set_hideimg);
}
} }

View file

@ -1,48 +1,20 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title>New Post</ion-title>
<ion-buttons start> <ion-buttons start>
<button ion-button (click)="dismiss()"> <button ion-button (click)="dismiss()">
<span ion-text color="primary">Cancel</span> <span ion-text color="primary" showWhen="ios">Cancel</span>
<ion-icon name="md-close" showWhen="android,windows"></ion-icon>
</button> </button>
</ion-buttons> </ion-buttons>
<ion-buttons end>
<ion-title>{{ title }}</ion-title> <button ion-button>
<span ion-text color="primary" showWhen="ios">Post</span>
<ion-icon name="send" showWhen="android,windows"></ion-icon>
</ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-card> <p>Blarp</p>
<ion-card-content>
<ion-item>
<ion-textarea [(ngModel)]="ptext" autocomplete="true" spellcheck="true" clearInput="true" rows="8"></ion-textarea>
</ion-item>
</ion-card-content>
<ion-row justify-content-end>
<ion-col offset-0>
<!-- <button ion-button (click)="attachImage()">
<ion-icon name="attach"></ion-icon>
</button> -->
</ion-col>
<ion-col col-2><div text-center>{{textCount()}}</div></ion-col>
<ion-col col-2>
<button ion-button (click)="send()">
<ion-icon name="send"></ion-icon>
</button>
</ion-col>
</ion-row>
<progress id='p' *ngIf="showProgress"></progress>
<ion-list>
<ion-item *ngFor="let f of files">
<ion-thumbnail item-start>
<img src="{{ f.link }}">
</ion-thumbnail>
<p>{{ f.name }}</p>
<button ion-button color="dark" clear item-end (click)="unattach(f.id)">
<ion-icon name="remove-circle"></ion-icon>
</button>
</ion-item>
</ion-list>
</ion-card>
</ion-content> </ion-content>

View file

@ -1,195 +0,0 @@
import { Component } from '@angular/core';
import { ViewController, NavParams, ToastController, Events } from 'ionic-angular';
import { Storage } from '@ionic/storage';
import { FileChooser } from '@ionic-native/file-chooser';
import { FilePath } from '@ionic-native/file-path';
import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer';
import * as pnut from 'pnut-butter';
@Component({
selector: 'modal-newpost',
templateUrl: 'new-post.html',
})
export class NewPostModal {
title: string;
replyid: string;
ptext: string = "";
showProgress: boolean = false;
files: Array<Object> = [];
fname: string = "";
fpath: string = "";
options: Object = {};
myUsername: string;
authToken: string;
longpost: Object = {};
raw: {type: string, value: Object}[] = [];
constructor(public navParams: NavParams, public viewCtrl: ViewController, public toastCtrl: ToastController,
private fileChooser: FileChooser, private storage: Storage, public events: Events, private filePath: FilePath,
private transfer: FileTransfer) {
console.log(JSON.stringify(this.navParams));
this.myUsername = navParams.data.me;
if (navParams.data.type === 'reply') {
this.replyid = navParams.data.post.id;
this.options = {replyTo: this.replyid};
if (navParams.data.post.user.username !== this.myUsername) {
this.ptext = "@" + navParams.data.post.user.username + " ";
} else {
this.ptext = ""
}
if (navParams.data.post.content.entities) {
if (navParams.data.post.content.entities.mentions.length > 0) {
this.ptext = this.ptext + this.parseMentions(navParams.data.post.content.entities.mentions);
}
}
this.title = "Reply to " + navParams.data.post.user.username
} else if (navParams.data.type === 'quote') {
this.ptext = " >> @" + navParams.data.post.user.username + ": " + navParams.data.post.content.text;
this.title = "New Post";
} else {
this.title = "New Post";
}
this.storage.get('token').then((val) => {
this.authToken = val;
});
}
dismiss() {
this.viewCtrl.dismiss();
}
send() {
if (this.ptext.length > 254) {
this.longpost = {
'title': '',
'body': this.ptext,
'tstamp': new Date().valueOf()
}
this.ptext = this.ptext.substr(0, 40) + "... - http://chimpnut.nl/u/";
this.ptext = this.ptext + this.navParams.data.me + "/lp/{object_id} - #longpost";
this.raw.push({
type: "nl.chimpnut.blog.post",
value: this.longpost
});
}
this.options['raw'] = this.raw;
pnut.createPost(this.ptext, this.options).then(res => {
console.log('-success-');
console.log(JSON.stringify(res));
this.presentToast("Status posted.");
this.events.publish('stream:reload', {});
}).catch(err => {
console.log('-error posting-');
console.log(JSON.stringify(err));
});
this.viewCtrl.dismiss();
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
parseMentions(mentions) {
let mtext = ""
for(var i = 0; i < mentions.length; i++) {
let mu = mentions[i].text;
if (mu !== this.myUsername) {
mtext += "@" + mu + " ";
}
}
if (mtext.length > 0) {
if (this.navParams.data.cc) {
mtext = "\n/" + mtext;
}
return mtext;
} else {
return "";
}
}
textCount() {
let counttext = ""
let count = 254 - this.ptext.length
if (count < 1) {
counttext = "longpost"
} else {
counttext = String(count)
}
return counttext
}
attachImage() {
// console.log('file chooser');
const fileTransfer: FileTransferObject = this.transfer.create();
this.fileChooser.open().then(uri => {
console.log('File URI: ' + uri);
this.filePath.resolveNativePath(uri).then(filePath => {
this.fpath = filePath;
this.fname = filePath.split('/').pop();
let options: FileUploadOptions = {
fileKey: 'content',
fileName: this.fname,
params: {
name: this.fname,
type: 'com.monkeystew.goober_m',
is_public: true
},
headers: {'Authorization': 'Bearer ' + this.authToken}
}
this.showProgress = true;
fileTransfer.upload(this.fpath, 'https://api.pnut.io/v0/files', options).then((response) => {
let rdata = JSON.parse(response.response);
let oembed = {
'+io.pnut.core.file': {
file_id: rdata.data.id,
file_token: rdata.data.file_token,
format: 'oembed'
}
}
// console.log(JSON.stringify(oembed));
this.raw.push({
type: "io.pnut.core.oembed",
value: oembed
});
this.files.push({
name: this.fname,
link: rdata.data.link,
id: this.raw.length - 1
});
this.showProgress = false;
}).catch((err) => { // fileTransfer
this.showProgress = false;
let edata = JSON.parse(err.body);
this.presentToast(edata.meta.error_message);
});
}).catch(err => { // filePath
console.log('-error getting filepath-');
console.log(err);
});
}).catch(err => { // fileChooser
console.log(err);
});
}
unattach(id) {
console.log('removing item ' + id);
this.raw.splice(id, 1);
this.files.splice(id, 1);
}
}

View file

@ -1,106 +0,0 @@
import { Component } from '@angular/core';
import { ViewController, NavParams, ToastController } from 'ionic-angular';
import { Events } from 'ionic-angular';
import { Device } from '@ionic-native/device';
import { ClipboardService } from 'ngx-clipboard';
import * as pnut from 'pnut-butter';
@Component({
template: `
<ion-list>
<button ion-item *ngIf="showShareBtn" (click)="share()">Share</button>
<button ion-item (click)="browse()">Open in Browser</button>
<button ion-item (click)="copy()">Copy to clipboard</button>
<button ion-item (click)="copyPostURL()">Copy link to post</button>
<button ion-item *ngIf="showDelBtn" (click)="delete()">Delete</button>
</ion-list>
`
})
export class PostMenu {
showDelBtn: boolean = false;
showShareBtn: boolean = false;
postURL: string;
constructor(public navParams: NavParams, public viewCtrl: ViewController, public toastCtrl: ToastController,
public events: Events, private clipboardSrv: ClipboardService, private device: Device) {
this.postURL = 'https://posts.pnut.io/' + this.navParams.data.post.id;
if (navParams.data.me == navParams.data.post.user.username) {
this.showDelBtn = true;
} else {
this.showDelBtn = false;
}
if (this.device.platform === "Android" || this.device.platform === "amazon-fireos") {
this.showShareBtn = true;
}
}
browse() {
window.open(this.postURL, '_system');
this.close();
}
share() {
(<any>window).shareContentPlugin.share(this.parsePost(), function(e) {
console.log('sharing post:');
console.log(JSON.stringify(e));
}, function(e) {
console.log('sharing failed:');
console.log(JSON.stringify(e));
});
this.close();
}
copy() {
this.clipboardSrv.copyFromContent(this.parsePost());
this.presentToast('Post copied');
this.close();
}
parsePost() {
let text = this.navParams.data.post.content.text;
let links = this.navParams.data.post.content.entities.links;
if (links.length > 0) {
for (var i = 0; i < links.length; i++) {
text += "\n";
if (typeof links[i].title !== "undefined") {
text += links[i].title + " - ";
}
text += links[i].link;
}
}
return text;
}
copyPostURL() {
this.clipboardSrv.copyFromContent(this.postURL);
this.presentToast('Post link copied');
this.close();
}
delete() {
pnut.deletePost(this.navParams.data.post.id).then(res => {
console.log(res);
this.presentToast('Post Deleted');
this.events.publish('stream:reload', {});
}).catch( err => {
console.log('-error-');
console.log(err);
});
this.close()
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
close() {
this.viewCtrl.dismiss();
}
}

View file

@ -24,7 +24,78 @@
<ion-list> <ion-list>
<ion-card *ngFor="let post of posts" color="{{ post.you_are_mentioned ? 'mention' : '' }}"> <ion-card *ngFor="let post of posts" color="{{ post.you_are_mentioned ? 'mention' : '' }}">
<post [post]="post" [hideImg]="hideImg" [ccOnReply]="ccOnReply" [myUsername]="myUsername"></post> <ion-item color="{{ post.you_are_mentioned ? 'mention' : '' }}">
<ion-avatar item-start>
<img src="{{ post.user.content.avatar_image.link }}">
</ion-avatar>
<h2>{{ post.user.name }}</h2>
<p>@{{ post.user.username }}</p>
<ion-note item-end>
<div text-right>
{{ post.created_at | timeago }}<br/>
{{ post.source.name }}
</div>
</ion-note>
</ion-item>
<ion-card-content>
<div *ngIf="post.is_deleted; else renderBlock"></div>
<ng-template #renderBlock >
<div [innerHTML]="post.content.html | parser"></div>
<div *ngIf="post.raw">
<div *ngFor="let r of post.raw">
<div *ngIf="r.type == 'nl.chimpnut.blog.post'">
<hr>
<div>{{ r.value.body }}</div>
</div>
</div>
</div>
</ng-template>
</ion-card-content>
<div *ngIf="post.raw">
<div *ngFor="let r of post.raw">
<div *ngIf="r.type == 'io.pnut.core.oembed'">
<img src="{{ r.value.url }}">
</div>
</div>
</div>
<div *ngIf="post.reposted_by_string">
<ion-item><ion-note>{{ post.reposted_by_string }}</ion-note></ion-item>
</div>
<ion-row>
<ion-col>
<button ion-button icon-left clear small block (click)="showReplyPost(post)">
<ion-icon name="ios-undo"></ion-icon>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="showQuotedPost(post)">
<ion-icon name="quote"></ion-icon>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="repost(post.id, post.you_reposted)">
<ion-icon name="repeat"></ion-icon>
<div *ngIf="post.counts.reposts > 0">{{ post.counts.reposts }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="bookmark(post.id, post.you_bookmarked)">
<ion-icon name="star"></ion-icon>
<div *ngIf="post.counts.bookmarks > 0">{{ post.counts.bookmarks }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="fetchThread(post.thread_id)">
<ion-icon name="chatboxes"></ion-icon>
<div *ngIf="post.counts.replies > 0">{{ post.counts.replies }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="presentPostMenu($event, post)">
<ion-icon name="more"></ion-icon>
</button>
</ion-col>
</ion-row>
</ion-card> </ion-card>
</ion-list> </ion-list>

View file

@ -1,14 +1,14 @@
page-stream { page-stream {
.item-md ion-avatar img { .item-md ion-avatar img {
border-radius: 10px; border-radius: 0;
background-color: #e9e9e9; background-color: #e9e9e9;
} }
.item-wp ion-avatar img { .item-wp ion-avatar img {
border-radius: 10px; border-radius: 0;
background-color: #e9e9e9; background-color: #e9e9e9;
} }
.item-ios ion-avatar img { .item-ios ion-avatar img {
border-radius: 10px; border-radius: 0;
background-color: #e9e9e9; background-color: #e9e9e9;
} }
} }

View file

@ -1,9 +1,12 @@
import { Component, ViewChild, ChangeDetectorRef } from '@angular/core'; import { Component, ViewChild, ChangeDetectorRef } from '@angular/core';
import { NavController, NavParams, ModalController, Content } from 'ionic-angular'; import { ViewController, NavController, NavParams, ModalController, Content, ToastController, PopoverController } from 'ionic-angular';
import { ThreadPage } from '../thread/thread';
import { Storage } from '@ionic/storage'; import { Storage } from '@ionic/storage';
import { FileChooser } from '@ionic-native/file-chooser';
import { FilePath } from '@ionic-native/file-path';
import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer';
import { Events } from 'ionic-angular'; import { Events } from 'ionic-angular';
import { LoginPage } from '../login/login'; import { LoginPage } from '../login/login';
import { NewPostModal } from '../stream/new-post';
import * as pnut from 'pnut-butter'; import * as pnut from 'pnut-butter';
@ -31,20 +34,16 @@ export class StreamPage {
showScrollBtn: boolean = false; showScrollBtn: boolean = false;
showUnified: boolean; showUnified: boolean;
ccOnReply: boolean = false; ccOnReply: boolean = false;
hideImg: boolean = false;
constructor(public navCtrl: NavController, public navParams: NavParams, constructor(public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController,
public modalCtrl: ModalController, private storage: Storage, private changeDetectorRef: ChangeDetectorRef, public toastCtrl: ToastController, private storage: Storage,
private changeDetectorRef: ChangeDetectorRef, public events: Events) { public popoverCtrl: PopoverController, public events: Events) {
// console.log(JSON.stringify(navParams));
this.storage.get('cc').then((val) => { this.storage.get('cc').then((val) => {
this.ccOnReply = val; this.ccOnReply = val;
}); });
this.storage.get('hideimg').then((val) => {
this.hideImg = val;
});
this.storage.get('unified').then((val) => { this.storage.get('unified').then((val) => {
this.showUnified = val; this.showUnified = val;
@ -208,6 +207,17 @@ export class StreamPage {
return pdata; return pdata;
} }
fetchThread(threadid) {
pnut.thread(threadid, {include_deleted: 0, include_raw: 1, count: 140}).then(res => {
if (res.meta.code === 401) {
this.storage.clear();
this.navCtrl.setRoot(LoginPage);
} else {
this.navCtrl.push(ThreadPage, {posts: res.data, me: this.myUsername});
}
});
}
fetchMyPosts() { fetchMyPosts() {
console.log('-- fetching mentions --'); console.log('-- fetching mentions --');
this.fetcher('me', {include_raw: 1, count: 40}).then(res => { this.fetcher('me', {include_raw: 1, count: 40}).then(res => {
@ -224,13 +234,386 @@ export class StreamPage {
}); });
} }
bookmark(postid, bookmarked) {
if (bookmarked) {
pnut.deleteBookmark(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Bookmark updated.");
}).catch(err => {
console.log(err);
});
} else {
pnut.bookmark(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Bookmark updated.");
}).catch(err => {
console.log(err);
});
}
}
repost(postid, reposted) {
if (reposted) {
pnut.deleteRepost(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Repost updated.");
}).catch(err => {
console.log(err);
});
} else {
pnut.repost(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Repost updated.");
}).catch(err => {
console.log(err);
});
}
}
updatePost(postid) {
pnut.post(postid, {include_raw: 1}).then(res => {
for (var i = 0; i < this.posts.length; i++) {
if (this.posts[i]['id'] === postid) {
this.posts[i] = res.data;
break;
}
}
}).catch(err => {
console.log(err);
});
}
showNewPost() { showNewPost() {
let newPostModal = this.modalCtrl.create(NewPostModal, {me: this.myUsername}); let newPostModal = this.modalCtrl.create(NewPostModal, {me: this.myUsername});
newPostModal.present(); newPostModal.present();
} }
showReplyPost(postData) {
let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'reply', post: postData, me: this.myUsername, cc: this.ccOnReply});
newPostModal.present();
}
showQuotedPost(postData) {
console.log(postData);
let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'quote', post: postData, me: this.myUsername, cc: this.ccOnReply});
newPostModal.present();
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
presentPostMenu(myEvent, postData) {
let popover = this.popoverCtrl.create(PostMenu, {post: postData, me: this.myUsername});
popover.present({ev: myEvent});
}
scrollToTop() { scrollToTop() {
this.content.scrollToTop(); this.content.scrollToTop();
} }
} }
@Component({
// templateUrl: 'new-post.html'
selector: 'modal-newpost',
template: `
<ion-header>
<ion-toolbar>
<ion-buttons start>
<button ion-button (click)="dismiss()">
<span ion-text color="primary">Cancel</span>
</button>
</ion-buttons>
<ion-title>{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-card>
<ion-card-content>
<ion-item>
<ion-textarea [(ngModel)]="ptext" autocomplete="true" spellcheck="true" clearInput="true" rows="8"></ion-textarea>
</ion-item>
</ion-card-content>
<ion-row justify-content-end>
<ion-col offset-0>
<button ion-button (click)="attachImage()">
<ion-icon name="attach"></ion-icon>
</button>
</ion-col>
<ion-col col-2><div text-center>{{textCount()}}</div></ion-col>
<ion-col col-2>
<button ion-button (click)="send()">
<ion-icon name="send"></ion-icon>
</button>
</ion-col>
</ion-row>
<progress id='p' *ngIf="showProgress"></progress>
<ion-list>
<ion-item *ngFor="let f of files">
<ion-thumbnail item-start>
<img src="{{ f.link }}">
</ion-thumbnail>
<p>{{ f.name }}</p>
<button ion-button color="dark" clear item-end (click)="unattach(f.id)">
<ion-icon name="remove-circle"></ion-icon>
</button>
</ion-item>
</ion-list>
</ion-card>
</ion-content>
`
})
export class NewPostModal {
title: string;
replyid: string;
ptext: string = "";
showProgress: boolean = false;
files: Array<Object> = [];
fname: string = "";
fpath: string = "";
options: Object = {};
myUsername: string;
authToken: string;
longpost: Object = {};
raw: {type: string, value: Object}[] = [];
constructor(public navParams: NavParams, public viewCtrl: ViewController, public toastCtrl: ToastController,
private fileChooser: FileChooser, private storage: Storage, public events: Events, private filePath: FilePath,
private transfer: FileTransfer) {
console.log(JSON.stringify(this.navParams));
this.myUsername = navParams.data.me;
if (navParams.data.type === 'reply') {
this.replyid = navParams.data.post.id;
this.options = {replyTo: this.replyid};
if (navParams.data.post.user.username !== this.myUsername) {
this.ptext = "@" + navParams.data.post.user.username + " ";
} else {
this.ptext = ""
}
if (navParams.data.post.content.entities) {
if (navParams.data.post.content.entities.mentions.length > 0) {
this.ptext = this.ptext + this.parseMentions(navParams.data.post.content.entities.mentions);
}
}
this.title = "Reply to " + navParams.data.post.user.username
} else if (navParams.data.type === 'quote') {
this.ptext = " >> @" + navParams.data.post.user.username + ": " + navParams.data.post.content.text;
this.title = "New Post";
} else {
this.title = "New Post";
}
this.storage.get('token').then((val) => {
this.authToken = val;
});
}
dismiss() {
this.viewCtrl.dismiss();
}
send() {
if (this.ptext.length > 254) {
this.longpost = {
'title': '',
'body': this.ptext,
'tstamp': new Date().valueOf()
}
this.ptext = this.ptext.substr(0, 40) + "... - http://chimpnut.nl/u/";
this.ptext = this.ptext + this.navParams.data.me + "/lp/{object_id} - #longpost";
this.raw.push({
type: "nl.chimpnut.blog.post",
value: this.longpost
});
}
this.options['raw'] = this.raw;
pnut.createPost(this.ptext, this.options).then(res => {
console.log('-success-');
console.log(JSON.stringify(res));
this.presentToast("Status posted.");
this.events.publish('stream:reload', {});
}).catch(err => {
console.log('-error posting-');
console.log(JSON.stringify(err));
});
this.viewCtrl.dismiss();
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
parseMentions(mentions) {
let mtext = ""
for(var i = 0; i < mentions.length; i++) {
let mu = mentions[i].text;
if (mu !== this.myUsername) {
mtext += "@" + mu + " ";
}
}
if (mtext.length > 0) {
if (this.navParams.data.cc) {
mtext = "\n/" + mtext;
}
return mtext;
} else {
return "";
}
}
textCount() {
let counttext = ""
let count = 254 - this.ptext.length
if (count < 1) {
counttext = "longpost"
} else {
counttext = String(count)
}
return counttext
}
attachImage() {
// console.log('file chooser');
const fileTransfer: FileTransferObject = this.transfer.create();
this.fileChooser.open().then(uri => {
console.log('File URI: ' + uri);
this.filePath.resolveNativePath(uri).then(filePath => {
this.fpath = filePath;
this.fname = filePath.split('/').pop();
let options: FileUploadOptions = {
fileKey: 'content',
fileName: this.fname,
params: {
name: this.fname,
type: 'com.monkeystew.goober_m',
is_public: true
},
headers: {'Authorization': 'Bearer ' + this.authToken}
}
this.showProgress = true;
fileTransfer.upload(this.fpath, 'https://api.pnut.io/v0/files', options).then((response) => {
let rdata = JSON.parse(response.response);
let oembed = {
'+io.pnut.core.file': {
file_id: rdata.data.id,
file_token: rdata.data.file_token,
format: 'oembed'
}
}
// console.log(JSON.stringify(oembed));
this.raw.push({
type: "io.pnut.core.oembed",
value: oembed
});
this.files.push({
name: this.fname,
link: rdata.data.link,
id: this.raw.length - 1
});
this.showProgress = false;
}).catch((err) => {
this.showProgress = false;
let edata = JSON.parse(err.body);
this.presentToast(edata.meta.error_message);
});
}).catch(err => {
console.log('-error getting filepath-');
console.log(err);
});
}).catch(err => {
console.log(err);
});
}
unattach(id) {
console.log('removing item ' + id);
this.raw.splice(id, 1);
this.files.splice(id, 1);
}
}
@Component({
template: `
<ion-list>
<button ion-item (click)="share()">Share</button>
<button ion-item *ngIf="showDelBtn" (click)="delete()">Delete</button>
</ion-list>
`
})
export class PostMenu {
showDelBtn: boolean = false;
constructor(public navParams: NavParams, public viewCtrl: ViewController, public toastCtrl: ToastController,
public events: Events) {
if (navParams.data.me == navParams.data.post.user.username) {
this.showDelBtn = true;
} else {
this.showDelBtn = false;
}
}
share() {
(<any>window).shareContentPlugin.share(this.navParams.data.post.content.text, function(e) {
console.log('sharing post:');
console.log(JSON.stringify(e));
}, function(e) {
console.log('sharing failed:');
console.log(JSON.stringify(e));
});
this.close();
}
delete() {
pnut.deletePost(this.navParams.data.post.id).then(res => {
console.log(res);
this.presentToast('Post Deleted');
this.events.publish('stream:reload', {});
}).catch( err => {
console.log('-error-');
console.log(err);
});
this.close()
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
});
toast.present();
}
close() {
this.viewCtrl.dismiss();
}
}

View file

@ -15,11 +15,66 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-list> <ion-list>
<ion-card *ngFor="let post of posts"> <ion-card *ngFor="let post of posts">
<post [post]="post" [hideImg]="hideImg" [ccOnReply]="ccOnReply" [myUsername]="myUsername"></post> <ion-item>
<ion-avatar item-start>
<img src="{{ post.user.content.avatar_image.link }}">
</ion-avatar>
<h2>{{ post.user.name }}</h2>
<p>@{{ post.user.username }}</p>
<ion-note item-end right>
<div text-right>
{{ post.created_at | timeago }}<br/>
{{ post.source.name }}
</div>
</ion-note>
</ion-item>
<ion-card-content>
<div *ngIf="post.is_deleted; else renderBlock"></div>
<ng-template #renderBlock >
<div [innerHTML]="post.content.html | parser"></div>
</ng-template>
</ion-card-content>
<div *ngIf="post.raw">
<div *ngFor="let r of post.raw">
<div *ngIf="r.type == 'io.pnut.core.oembed'">
<img src="{{ r.value.url }}">
</div>
</div>
</div>
<ion-row>
<ion-col>
<button ion-button icon-left clear small block (click)="showReplyPost(post)">
<ion-icon name="ios-undo"></ion-icon>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="showQuotedPost(post)">
<ion-icon name="quote"></ion-icon>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="bookmark(post.id, post.you_bookmarked)">
<ion-icon name="star"></ion-icon>
<div *ngIf="post.counts.bookmarks > 0">{{ post.counts.bookmarks }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="repost(post.id, post.you_reposted)">
<ion-icon name="repeat"></ion-icon>
<div *ngIf="post.counts.reposts > 0">{{ post.counts.reposts }}</div>
</button>
</ion-col>
<ion-col>
<button ion-button icon-left clear small block (click)="presentPostMenu($event, post)">
<ion-icon name="more"></ion-icon>
</button>
</ion-col>
</ion-row>
</ion-card> </ion-card>
</ion-list> </ion-list>

View file

@ -1,14 +1,14 @@
page-thread { page-thread {
.item-md ion-avatar img { .item-md ion-avatar img {
border-radius: 10px; border-radius: 0;
background-color: #e9e9e9; background-color: #e9e9e9;
} }
.item-wp ion-avatar img { .item-wp ion-avatar img {
border-radius: 10px; border-radius: 0;
background-color: #e9e9e9; background-color: #e9e9e9;
} }
.item-ios ion-avatar img { .item-ios ion-avatar img {
border-radius: 10px; border-radius: 0;
background-color: #e9e9e9; background-color: #e9e9e9;
} }
} }

View file

@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular'; import { NavController, NavParams, ModalController, ToastController, PopoverController } from 'ionic-angular';
import { Storage } from '@ionic/storage'; import { NewPostModal, PostMenu } from '../stream/stream';
import * as pnut from 'pnut-butter'; import * as pnut from 'pnut-butter';
@ -19,22 +19,89 @@ export class ThreadPage {
title: string; title: string;
posts: Array<Object> = []; posts: Array<Object> = [];
myUsername: string; myUsername: string;
hideImg: boolean = false;
ccOnReply: boolean = false;
constructor(public navCtrl: NavController, public navParams: NavParams, constructor(public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController,
private storage: Storage) { public popoverCtrl: PopoverController, public toastCtrl: ToastController) {
this.posts = this.navParams.data.posts; this.posts = this.navParams.data.posts;
this.myUsername = this.navParams.data.me; this.myUsername = this.navParams.data.me;
}
this.storage.get('hideimg').then((val) => { showReplyPost(postData) {
this.hideImg = val; let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'reply', post: postData});
newPostModal.present();
}
showQuotedPost(postData) {
console.log(postData);
let newPostModal = this.modalCtrl.create(NewPostModal, {type: 'quote', post: postData});
newPostModal.present();
}
bookmark(postid, bookmarked) {
if (bookmarked) {
pnut.deleteBookmark(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Bookmark updated.");
}).catch(err => {
console.log(err);
});
} else {
pnut.bookmark(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Bookmark updated.");
}).catch(err => {
console.log(err);
});
}
}
repost(postid, reposted) {
if (reposted) {
pnut.deleteRepost(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Repost updated.");
}).catch(err => {
console.log(err);
});
} else {
pnut.repost(postid).then(res => {
console.log(res);
this.updatePost(res.data.id);
this.presentToast("Repost updated.");
}).catch(err => {
console.log(err);
});
}
}
updatePost(postid) {
pnut.post(postid, {include_raw: 1}).then(res => {
for (var i = 0; i < this.posts.length; i++) {
if (this.posts[i]['id'] === postid) {
this.posts[i] = res.data;
break;
}
}
}).catch(err => {
console.log(err);
}); });
}
this.storage.get('cc').then((val) => { presentPostMenu(myEvent, postData) {
this.ccOnReply = val; let popover = this.popoverCtrl.create(PostMenu, {post: postData, me: this.myUsername});
popover.present({ev: myEvent});
}
presentToast(text) {
let toast = this.toastCtrl.create({
position: 'top',
message: text,
duration: 2000
}); });
toast.present();
} }
} }

View file

@ -1,35 +0,0 @@
<!--
Generated template for the UserListPage page.
See http://ionicframework.com/docs/components/#navigation for more info on
Ionic pages and navigation.
-->
<ion-header>
<ion-navbar>
<ion-title>{{ list }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list *ngFor="let user of users">
<ion-item (click)="showProfile(user)">
<ion-avatar item-start>
<img src="{{ user.content.avatar_image.link }}">
</ion-avatar>
<h2>{{ user.name }}</h2>
<p>@{{ user.username }}</p>
<!-- <ion-col item-end text-right>
<button ion-button disabled>{{ user.you_follow ? "Unfollow" : "Follow" }}</button>
<ion-note>{{ user.follows_you ? "Follows you" : ""}}</ion-note>
</ion-col> -->
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="fetchMoreUsers($event)">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>

View file

@ -1,14 +0,0 @@
page-user-list {
.item-md ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
.item-wp ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
.item-ios ion-avatar img {
border-radius: 10px;
background-color: #e9e9e9;
}
}

View file

@ -1,71 +0,0 @@
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { ProfilePage } from '../../pages/profile/profile';
import * as pnut from 'pnut-butter';
/**
* Generated class for the UserListPage page.
*
* See https://ionicframework.com/docs/components/#navigation for more info on
* Ionic pages and navigation.
*/
@Component({
selector: 'page-user-list',
templateUrl: 'user-list.html',
})
export class UserListPage {
private list: string;
private userid: string;
private users: Array<Object> = [];
private myUsername: string;
private before_id: string;
private fetcher: any;
constructor(public navCtrl: NavController, public navParams: NavParams) {
this.list = this.navParams.data.list;
this.userid = this.navParams.data.userid;
this.myUsername = this.navParams.data.username;
}
ionViewDidLoad() {
console.log('ionViewDidLoad UserListPage');
let params = {
include_deleted: 0,
include_raw: 1,
count: 40
};
if (this.list == "Followers") {
this.fetcher = pnut.followers;
} else if (this.list == "Following") {
this.fetcher = pnut.following;
}
this.fetcher(this.userid, params).then(res => {
this.users = res.data;
this.before_id = res.meta.min_id;
});
}
showProfile(user) {
this.navCtrl.push(ProfilePage, {user: user, me: this.myUsername});
}
fetchMoreUsers(infiniteScroll) {
let params = {
include_deleted: 0,
include_raw: 1,
before_id: this.before_id,
count: 40
};
this.fetcher(this.userid, params).then(res => {
if (res.data.length > 0) {
this.users.push.apply(this.users, res.data);
this.before_id = res.meta.min_id;
}
infiniteScroll.complete();
});
}
}

View file

@ -16,7 +16,7 @@ export class ParserPipe implements PipeTransform {
transform(value: string, ...args): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { transform(value: string, ...args): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
let hregex = /href="([\S]+)"/g; let hregex = /href="([\S]+)"/g;
if (typeof value == "undefined") value = ""; // value = value.replace(hregex, "class=\"ex-link\" href=");
value = value.replace(hregex, "onClick=\"window.open('$1', '_system', 'location=yes')\""); value = value.replace(hregex, "onClick=\"window.open('$1', '_system', 'location=yes')\"");
return this._sanitizer.bypassSecurityTrustHtml(value); return this._sanitizer.bypassSecurityTrustHtml(value);
} }

View file

@ -0,0 +1,12 @@
import { OAuthProvider } from 'ng2-cordova-oauth/provider';
export class PnutAuth extends OAuthProvider {
protected authUrl: string = 'https://pnut.io/oauth/authenticate';
protected defaults: Object = {
responseType: 'token',
clientId: '' // Insert your client ID and rename this file to pnut-oauth.ts
};
}

View file

@ -1 +0,0 @@
build

View file

@ -1 +0,0 @@
../LICENSE

View file

@ -1 +0,0 @@
../README.md

View file

@ -1,7 +0,0 @@
{
"template": "pure",
"kill": "webapp-container",
"ignore": [
".git"
]
}

View file

@ -1,9 +0,0 @@
{
"template": "ubuntu-webapp",
"policy_groups": [
"webview",
"audio",
"networking"
],
"policy_version": 16.04
}

View file

@ -1,7 +0,0 @@
[Desktop Entry]
Name=Goober
Exec=webapp-container --app-id="goober.thrrgilag" $@ www/index.html
Icon=icon.png
Terminal=false
Type=Application
X-Ubuntu-Touch=true

View file

@ -1 +0,0 @@
../resources/icon-144.png

View file

@ -1,15 +0,0 @@
{
"name": "goober.thrrgilag",
"description": "A pnut.io client",
"architecture": "all",
"title": "Goober",
"hooks": {
"goober": {
"apparmor": "goober.apparmor",
"desktop": "goober.desktop"
}
},
"version": "0.8.0",
"maintainer": "Morgan McMillian <gilag@monkeystew.com>",
"framework" : "ubuntu-sdk-16.04"
}

View file

@ -1 +0,0 @@
../www